Repository navigation
fix(rust): resolve module-qualified calls (revives #2135) - #4944
nothariharan wants to merge 2 commits into
Conversation
…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. The qualifier is verified right-to-left against the candidate's file path: the last segment must BE the defining module (<seg>.rs or <seg>/mod.rs), every remaining segment must match the successive parent directory, stopping at crate/super/self. Candidates are restricted to Rust sources, and a qualifier bound by a use from outside the crate (use std::fs; fs::read()) is skipped at extraction via the file's use roots. Exactly one survivor or bail. Uppercase qualifiers (Type::method / Enum::Variant) keep their existing skipped behavior. Emitted as EXTRACTED (confidence 1.0): the module is named explicitly in source — the same reasoning as the qualified-class-method passes (Graphify-Labs#1446/Graphify-Labs#1533). The verification lives in one shared helper (rust_scoped_call_survivors) used by both cross-file passes, so scoped entries can never reach bare-name resolution in either. Real-world Rust crate (~200 files, 14k edges): +138 EXTRACTED, -388 INFERRED (the #908 class shrinking), no regressions on bare-name-called control symbols. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f local names Review follow-ups on the rebased Graphify-Labs#2135. - `crate::util::f()` stopped matching at `crate` without checking which crate the candidate is in, so in a workspace it bound to another crate's util.rs. A candidate must now share the caller's crate root (nearest Cargo.toml, else the directory above `src`). - `crate::b::helper()` in a.rs went through the in-file bare-name lookup first and bound to a.rs's own helper. A module-path call binds in-file only for `self::f()` or an inline `mod m { .. }` declared in that file. - Module names starting with `_` were rejected (`'_'.islower()` is False); only an UpperCamelCase segment means a type. - Methods are skipped as candidates: a module path names a free function.
|
Thanks for the pull request, @nothariharan. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 3 changed function(s) (2 sampled, not proven, 1 not verified).
Formal verification. PR-changed functions: 2/3 verified (0 proven, 2 may-equivalent, 0 distinguished) · 1 not verified (1 vacuous).
Not verified on this run: resolve\_cross\_file\_raw\_calls (vacuous: never exercised).
Graphify review — findings
Adds resolution of Rust module-qualified calls (module::function()), which were previously dropped at extraction and left idiomatic intra-crate callers with no dependents. In extract_rust, a scoped call binds in-file only for self::f() or an inline mod m { .. }, otherwise it is queued with its qualifier (dropped when the first segment is imported from anything other than crate/super/self), and the unknown-root default of crate-local is where extern or std paths could still slip through. In extract(), rust_scoped_call_survivors checks the qualifier right-to-left against each candidate's .rs path and emits an EXTRACTED calls edge only when exactly one candidate survives, so ambiguous or unmatched calls stay unresolved and never reach the bare-name tie-breakers that the #908 guard protects against.
Worth a look
- Unknown path roots default to crate-local, so extern/std calls can bind to local files —
graphify/extractors/rust.py:989· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Single-segment qualifier binds an imported std module to a same-named local file —
graphify/symbol_resolution.py:390· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Scoped inline-module calls bind by bare function name —
graphify/extractors/rust.py:957· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Scoped-call branch in extract() is not gated to Rust —
graphify/extract.py:11102· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3671 functions depend on the 432 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 1099 callers, 65 callees - worse:
resolve_cross_file_raw_calls()— 9 callers, 6 callees
Verification — 3671 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 3409 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
239 of 490 test file(s) selected (49%) via static blast radius.
tests/test_4874_jvm_resolution.py— impacttests/test_affected_overrides.py— impacttests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_blade_extractor.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_c_include_resolution.py— impacttests/test_cache.py— impacttests/test_case_only_decl_ids_4904.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cobol_extractor.py— impacttests/test_confidence_score_output.py— impacttests/test_cpp_method_declarations.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cpp_template_type_params.py— impacttests/test_cpp_type_alias.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_external_call_guards.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_delegate_declarations.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_foreach_loop_variable.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_chain_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_method_owner_labels.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_overloads.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_tuple_type_refs.py— impacttests/test_csharp_type_resolution.py— impacttests/test_csharp_underscore_collisions.py— impacttests/test_csharp_var_call_return_type.py— impacttests/test_csharp_verbatim_interpolation.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_edge_also_seen.py— impacttests/test_ejs_extractor.py— impacttests/test_elixir_defdelegate.py— impacttests/test_elixir_import_resolution.py— impact- … and 189 more
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
CHANGELOG.md§ 0.9.85 (2026-10-10) (lines 342-364): references changed symbolswhereCHANGELOG.md§ 0.9.64 (2026-09-18) (lines 649-660): references changed symbolswhereCHANGELOG.md§ 0.9.6 (2026-07-04) (lines 1404-1429): references changed symbolswheregraphify/skill-agents.md§ Step 1 - Ensure graphify is installed (lines 69-116): references changed symbolswheregraphify/skill-agents.md§ Part A - Structural extraction for code files (lines 182-210): references changed symbolswheregraphify/skill-agents.md§ Part B - Semantic extraction (parallel subagents) (lines 211-419): references changed symbolswheregraphify/skill-aider.md§ Step 1 - Ensure graphify is installed (lines 63-110): references changed symbolswheregraphify/skill-amp.md§ Step 1 - Ensure graphify is installed (lines 69-116): references changed symbolswheregraphify/skill-amp.md§ Part A - Structural extraction for code files (lines 182-210): references changed symbolswheregraphify/skill-amp.md§ Part B - Semantic extraction (parallel subagents) (lines 211-419): references changed symbolswhere
…and 10 more.
Formal verification
No difference found (not proven): No behavior difference found in extract (not a proof).
The verifier ran both versions of extract on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in extract\_rust (not a proof).
The verifier ran both versions of extract\_rust on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify resolve\_cross\_file\_raw\_calls.
The verifier did not have enough to check resolve\_cross\_file\_raw\_calls, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 1 grounded finding(s) anchored inline below; 1 more finding(s) on lines outside this diff (see the check run).
| return survivors | ||
|
|
||
|
|
||
| def resolve_cross_file_raw_calls( |
There was a problem hiding this comment.
resolve_cross_file_raw_calls()
fans out to 6 callees (efferent coupling); 9 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Shipped in 0.9.101 (published to PyPI), with your authorship preserved. Revives #2135. Thanks @nothariharan. |
Revives #2135 by @okinoxis, rebased onto current v8 with authorship kept, plus review follow-ups.
Problem
A Rust call written with a module path (
crate::formatter::format_line(),formatter::format_line(),super::formatter::format_line()) never became acallsedge, soexplainshowed 0 callers. A user reported this on a 100k-line Rust repo, and I reproduced it on v8 32e5efe.Change
std::fs::readvs a localfs.rs, and Rustb::helper()vs Pythonb.py), with 11 tests. Rebase note: v8 now has its ownuse_roots(sets, aware of crate-localmods), so I dropped the PR's copy and ported its scoped-call step onto v8's.Cargo.toml, else the directory abovesrc), socrate::util::f()can't reach another workspace crate;crate::b::helper()ina.rsno longer binds toa.rs's ownhelper. A module-path call binds in-file only forself::f()or an inlinemod m { };_-prefixed module names are accepted;Evidence
tests/test_rust_scoped_call_resolution.py. The new ones fail with the follow-up reverted, except the method guard.test_ext_tagged_id_clash,test_code_only_update_all_hosts[devin/aider], the memoization getcwd tests, and a flakytest_partial_extraction_warning). Ruff is clean, and the pyright count is unchanged from v8.Not covered:
m::f()inside a macro (see the macro-argument PR). The scoped resolution is also still duplicated acrossextract.pyandsymbol_resolution.py. That was already the case in #2135, and a shared edge builder would be a follow-up.Supersedes #2135.