Skip to content

fix(rust): resolve module-qualified calls via qualifier-verified cross-file resolution#2135

Open
okinoxis wants to merge 1 commit into
Graphify-Labs:v8from
okinoxis:rust-scoped-call-resolution
Open

fix(rust): resolve module-qualified calls via qualifier-verified cross-file resolution#2135
okinoxis wants to merge 1 commit into
Graphify-Labs:v8from
okinoxis:rust-scoped-call-resolution

Conversation

@okinoxis

Copy link
Copy Markdown

Problem

For Rust, scoped calls (module::function(...)) are dropped at extraction when the callee is not defined in the same file (extractors/rust.py). Since the module-qualified call is the DOMINANT idiomatic intra-crate call form, this makes the call graph blind to most cross-file Rust edges: a real production caller of scan_substrate::accumulate_observations(...) showed 0 dependents while grep found the call site immediately. Every downstream consumer (blast-radius queries, dependents listings) silently under-reports, which pushes users back to grep — defeating graph-first navigation exactly where it matters.

The drop was intentional: resolving scoped calls by BARE last-segment lookup across crate boundaries produced spurious INFERRED edges (#908). The guard is right; the lost signal is not.

Solution

Keep the #908 guard, but stop discarding the qualifier — use it as the verification key:

  1. extractors/rust.py — an unresolved scoped call is enqueued into raw_calls with is_scoped_call: True and its scope_qualifier (the path field text of the scoped_identifier), instead of being dropped.
  2. extract.py (shared cross-file pass) — a new branch handles is_scoped_call BEFORE bare-name resolution and never falls through to it:
  3. symbol_resolution.py — the same qualifier-verified branch in resolve_cross_file_raw_calls, so scoped entries can never reach that path's bare-name resolution either (defense in depth; pair-dedup makes double emission impossible).

Results (real-world Rust crate, ~200 source files, 14k-edge graph)

  • The motivating case resolves: scan_substrate::accumulate_observations → its caller, EXTRACTED at the exact source line.
  • Graph-wide: +138 EXTRACTED edges (qualifier-verified), −388 INFERRED edges (risky bare-name resolutions either promoted with verification or dropped — the #908 class shrinking), net +159 edges all with provenance.
  • No regressions on control symbols (bare-name-called functions keep their dependents).

Suggested tests

  1. Two modules a.rs/b.rs, b::helper() called from a → one EXTRACTED edge to b.rs's helper, none to a same-named helper in c.rs.
  2. mod.rs form: call util::helper() with util/mod.rs defining helper → resolves.
  3. Ambiguity guard: helper defined in BOTH b.rs and b/mod.rs (pathological) → no edge (two survivors).
  4. Type::method() / Enum::Variant (uppercase qualifier) → unchanged behavior (no new edge).
  5. crate::x::helper() / super::helper() / self::helper() → skipped (no spurious edge).
  6. Turbofish m::<T>-adjacent shapes → qualifier normalization strips generics.

Known limitations (documented, out of scope here)

  • crate::/super::-qualified paths still unresolved (needs module-tree walking).
  • use alias resolution (use a::b as c; c::f()) not attempted.
  • bin→lib fully-qualified paths still unresolved (crate-name qualifier ≠ file stem).

…s-file resolution

Unresolved scoped calls (module::function()) were dropped at extraction,
making the dominant idiomatic intra-crate call form invisible to the graph
(a real production caller showed 0 dependents while grep found it).

Keep the #908 bare-name guard, but stop discarding the qualifier — use it
as the verification key: enqueue scoped calls with their qualifier path,
then resolve strictly against it in the shared cross-file pass (defining
file must BE the named module: <seg>.rs or <seg>/mod.rs; exactly one
survivor or bail). Uppercase qualifiers (Type::method / Enum::Variant) and
crate/super/self keep their existing skipped behavior. Emitted as EXTRACTED
(confidence 1.0) — the module is named explicitly in source, same reasoning
as the qualified-class-method passes (Graphify-Labs#1446/Graphify-Labs#1533). Defense-in-depth: the
same branch in resolve_cross_file_raw_calls so scoped entries never reach
bare-name resolution.

Real-world Rust crate (~200 files, 14k edges): +138 EXTRACTED,
-388 INFERRED (the #908 class shrinking), no regressions on
bare-name-called control symbols.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant