fix: resolve scoped/static (Class::method) call targets for PHP and Rust#568
fix: resolve scoped/static (Class::method) call targets for PHP and Rust#568NaeemHaque wants to merge 1 commit into
Class::method) call targets for PHP and Rust#568Conversation
Scoped/static calls — PHP `Mailer::send()`, Rust `Notifier::notify()`, `Self::method()` — were stored as CALLS edges with the intermediate `Class::method` target form, which matches neither the qualified-name (`<file>::Class.method`) nor the bare-name lookup path. The edges dangled, so callers_of / get_impact_radius / tests_for silently under-reported these callers (notably every Rust associated-fn/constructor call). Add a conservative post-build resolver (scoped_resolver.py) that rewrites resolvable `Class::method` targets to the canonical node qualified name, mirroring the existing Spring/Temporal/ReScript resolvers, wired into both full build and incremental update. It only rewrites unambiguous (class, method) matches or import-disambiguated ones; external/unknown targets like `Vec::new` are left untouched so no false edge is created. self/static/Self/ this resolve to the enclosing class; parent to its base. Resolved edges are tagged confidence_tier=INFERRED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
itxaiohanglover
left a comment
There was a problem hiding this comment.
Clean implementation — follows the existing resolver pattern (_run_temporal_resolver etc.) consistently. The _SCOPED_CALL_EXTS comment explaining why only PHP and Rust are included is helpful.
One minor question: in incremental_update, the condition checks spring_changed for temporal stats but scoped_changed for scoped stats — is that intentional? If spring and scoped are independent, looks correct. Just confirming the temporal → spring coupling is existing behavior.
|
Hey @itxaiohanglover — thanks for taking the time to review this! Yes, that's intentional. Temporal shares Spring's check on purpose, and scoped has its own. The temporal one is existing behavior — I just left it as-is. |
|
Good work on this! I had commented on #567 about The incremental update guard with |
|
That's already covered. |
Summary
Scoped/static calls (
Mailer::send()in PHP,Mailer::send(x)/Self::method()in Rust) were stored as CALLS edges with the intermediateClass::methodtarget, which matches neither the qualified-name lookup (<file>::Class.method) nor the bare-name lookup. The edges dangled, socallers_of,get_impact_radius, andtests_forsilently missed these callers — especially impactful in Rust, whereType::method()/Self::method()is the dominant call form.Fixes #567.
What this does
Adds a post-build resolver (
code_review_graph/scoped_resolver.py) in the same style as the existing Spring/Temporal/ReScript resolvers. It rewrites resolvableClass::methodCALLS targets to the canonical node qualified name, wired into both full build and incremental update (gated to.php/.rs).It is deliberately conservative so it never fabricates an edge:
Class::methodtargets are resolved. Multi-segment Rust module paths (a::b::c) are left alone — resolving them by their last two segments would point at unrelated types.App\Mailer→Mailer).self/Selfresolve to the enclosing class.(class, method)node, or disambiguates via the caller file'sIMPORTS_FROMedges. Ambiguous or unknown targets (external types likeVec::new) are left untouched.confidence_tier = INFERREDto distinguish them from directly-extracted ones.Known limitation: same-name collisions across files with no import statement have nothing to disambiguate on and stay unresolved. Strictly better than today, not 100%.
Scope is PHP and Rust — the two languages that actually produce a dangling
Class::methodedge. C++/Ruby produce no CALLS edge for scoped calls (a separate extraction gap), and R's::is external-package access; both are out of scope here.Tests
tests/test_scoped_resolver.py(14 tests): PHP cross-file and namespaced calls, Rust cross-file andSelf::, the multi-segment false-edge guard, external-target-left-untouched, import disambiguation (resolved + ambiguous), incremental re-resolution, idempotency, andINFERREDtagging.uv run pytest— all pass (full suite green)uv run ruff check code_review_graph/— clean