Repository navigation
fix(rust): scope a use to the crate it names (#4699) - #4712
Deepjyoti-Sarmah wants to merge 7 commits into
Conversation
…hify-Labs#4699) The corpus rewire bound a sourceless `Value` stub to the only `Value` in the scan even when the file said `use serde_json::Value`, so a field named `value` could absorb an external import and report cycles the source does not have. The extractor already knows the first path segment of every `use` binding; it now carries that on the stub as `_rust_use_roots`, and the rewire only considers candidates that live in a crate the `use` names. A crate outside the scan is in no scanned crate, so the stub stays unbound: `use a::Value` reaches `a`'s definition and not a namesake in `c`. A file with no Cargo.toml above it keeps v8's label-only binding, because there is no crate to scope it by. Manifests are read through cargo_introspect.package_name, which reuses the module's tomllib loader and its Python 3.10 fallback. The marker is part of the per-file AST payload, so the cache schema is bumped.
…hify-Labs#4699) Each fixture writes a Cargo.toml per crate, since that is what makes the crate scoping possible. The failing cases are the reported one (external `use` plus an annotation), a grouped external `use`, an annotation with no `use` at all, and a workspace `use` that should reach the named crate rather than a namesake. The guards keep an external `use` with no reference, a `std` import, `crate::` paths, and two same-named types without a `use` behaving as they did.
…4699) The crate pass leaves a Rust stub unscoped when there is no Cargo.toml to scope it by (a loose .rs scan) or when its `use` names a crate the corpus does not contain. Those stubs still bound by label alone, and the edge was reported as EXTRACTED even though the binding is a guess. An edge that rewires a Rust stub is now INFERRED unless an in-crate `use` of the name (`_rust_use_name`) vouches for it. The wrong edge is still gone where the crate pass can scope it; this only stops presenting a guess as extracted in the cases it cannot.
…Labs#4699) With no Cargo.toml there is no crate to scope the external `use` by, so the rewire still binds by label; the test pins that this guess is INFERRED. The guard pins that a `crate::` `use` keeps its binding EXTRACTED.
…ify-Labs#4699) The crate scoping only allowed the crate named by the `use`, so `use axum::extract::FromRequestParts` (the type lives in axum-core and axum re-exports it) stayed unbound and the correct edge was lost. The same pass left a correct `use axum_core::..` at INFERRED, because only an in-crate `use` counted as a voucher. A bare `pub use` now records the crate it re-exports from on its import edge; the rewire follows those mappings across crates and allows the defining crate after the directly named one, which still wins. `pub(crate)`/`pub(super)` stay inside their crate. A stub whose `use` names a scanned crate counts as vouched, so its edge keeps EXTRACTED. The per-file edge payload changed, so the cache schema moves to 24.
…bs#4699) Covers `use reexporting_crate::Item` reaching the defining crate, an unrelated namesake staying out, `use super::Item` through a re-export (the axum connect_info shape), `pub(crate) use` not leaking out of its crate, and the confidence staying EXTRACTED for a scanned-crate `use`.
|
Thanks for the pull request, @Deepjyoti-Sarmah. 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 unsupported).
Not verified on this run: \_rewire\_unique\_stub\_nodes (unsupported).
Graphify review — findings
Scopes Rust use stub rewiring to the crate the import names, so use other_crate::Thing binds to a definition in other_crate instead of any same-labelled node in the corpus. Crate membership comes from the nearest Cargo.toml via the new package_name, which falls back to a regex when no TOML parser is available. Files with no manifest keep the old label-only binding. A pub use chain lets an import of a re-exporting crate reach the defining one, but the named crate always wins when it has the definition, and the AST cache schema bumps to 24 so .rs extractions re-run. The hardcoded ["crate"] roots on stubs, the use of Cargo package names rather than Rust crate names, and the alias-suffix handling on single-segment imports all decide whether workspace imports bind to the right crate.
Worth a look
- Rust use stubs hardcode
_rust_use_rootsto["crate"], so imports of other workspace crates stop resolving —graphify/extract.py:10371· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Rust crate scoping uses Cargo package names instead of Rust crate names —
graphify/extract.py:3949· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Unrelated non-Rust definitions can block a Rust import scoped to a workspace crate —
graphify/extract.py:3988· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Use root keeps the
as aliassuffix for single-segment aliased imports —graphify/extractors/rust.py:793· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3696 functions depend on the 484 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 1002 callers, 60 callees - worse:
_rewire_unique_stub_nodes()— 14 callers, 16 callees
Verification — 3696 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: 3447 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
233 of 444 test file(s) selected (52%) via static blast radius.
tests/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_cache_stale_import_target.py— impacttests/test_cargo_introspect.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_charmap_encoding.py— impacttests/test_chunking.py— impacttests/test_cjs_module_extension.py— impacttests/test_cobol_extractor.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_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_object_creation.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_ejs_extractor.py— impacttests/test_elixir_defdelegate.py— impacttests/test_elixir_import_resolution.py— impacttests/test_elixir_keyword_def_calls.py— impacttests/test_elixir_qualified_calls.py— impacttests/test_elixir_unqualified_call_scope.py— impact- … and 183 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.13 (2026-07-12) (lines 1152-1171): references changed symbols_rewire_unique_stub_nodesCHANGELOG.md§ 0.8.46 (2026-06-23) (lines 1418-1429): references changed symbols_rewire_unique_stub_nodesCHANGELOG.md§ 0.8.41 (2026-06-17) (lines 1463-1474): references changed symbols_rewire_unique_stub_nodesdocs/superpowers/plans/2026-05-04-incremental-updates-dedup.md§ Task 4: Incremental updates — semantic cache + manifest in __main__.py (lines 603-911): references changed symbols_rungraphify/skill-agents.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 626-735): references changed symbolskeepgraphify/skill-amp.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 626-735): references changed symbolskeepgraphify/skill-claw.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 629-738): references changed symbolskeepgraphify/skill-codex.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 630-739): references changed symbolskeepgraphify/skill-copilot.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 629-738): references changed symbolskeepgraphify/skill-droid.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 626-735): references changed symbolskeep
…and 10 more.
Formal verification
Could not verify: Could not verify \_rewire\_unique\_stub\_nodes.
The verifier did not have enough to check \_rewire\_unique\_stub\_nodes, 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: not verifiable: the input domain has 81 values but only 11 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
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.
· 1 grounded finding(s) anchored inline below; 1 more finding(s) on lines outside this diff (see the check run).
|
|
||
|
|
||
| def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None: | ||
| def _rewire_unique_stub_nodes( |
There was a problem hiding this comment.
_rewire_unique_stub_nodes()
fans out to 16 callees (efferent coupling); 14 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
…cope # Conflicts: # graphify/cache.py
What does this PR do?
Fixes #4699. A Rust
usecould bind to a same-named definition in another crate. Ontokio-rs/axum@86aa615,examples/customize-extractor-error/src/custom_extractor.rssaysuse serde_json::Value, and both its import and aValuereference bound to a variant inaxum-core/src/response/into_response_parts.rs. The graph showed a cross-crate dependency the source never had.Three causes, all in the Rust extractor and the corpus rewire:
_rewire_unique_stub_nodesbound a sourceless stub to the only node with that label anywhere in the corpus. The crate theusenames was not part of the decision._rust_use_name) only for paths rooted in this crate, so an externalusecarried no scope.EXTRACTED.Before (v8, the reported file):
After:
The re-export handling keeps edges the scoping would otherwise drop.
axum-extra/src/extract/cached.rs(use axum::extract::FromRequestParts) andaxum/src/extract/connect_info.rs(use super::{.., FromRequestParts}) still reachaxum-core/src/extract/mod.rs, and ause axum_core::..edge staysEXTRACTED.Type of change
Verification & Invariants
A Rust name binds only inside a crate its
usenames. A crate outside the scan binds to nothing. A cross-crate re-export counts as the named crate. A binding made by label alone isINFERRED.Merge Danger
Door: two-way. The per-file AST payload gained two markers, so the cache schema moves from 22 to 24 and cached
.rsextractions re-run on the next build. A revert brings back the label-only binding on the next build. No migration, no config change.Blast Radius: Rust cross-file resolution.
On
tokio-rs/axum@86aa615(302.rsfiles):imports_fromreferencesuseof the source file)The node count rises because a scoped stub that binds to nothing stays as a sourceless node instead of being dropped. Cross-crate edges rise because a name with several same-named definitions can now resolve to the one in the crate it names instead of staying ambiguous. Bug-signature edges drop from 22 to 13. The 20 cycles do not change; the reported
Valueedge was not part of one.origin_fileends in.rs, so no other extractor changes. The full-suite failure set is identical to v8.graph.jsonkeeps its old Rust edges until the next build. A rebuild produces the scoped result.Cargo.tomlfiles, with no environment or ordering input.Limitations
Cargo.tomlabove it keeps v8's label-only binding and is markedINFERRED. Scoping needs a crate.pub use core::*;is not followed; only a namedpub useis.foo = { package = "bar" }inCargo.tomlis not mapped, souse foo::..stays unbound.How was this tested?
New tests in
tests/test_rust_crate_use_scope.pyrunextract()on small Cargo projects and compare node ids, edge targets, and confidence. None of them calls a private helper.useplus an annotation does not reach a local crate (enum variant)useplus an annotation does not reach a local crate (struct)usedoes not reach a local crateusedoes not reach a local crateuse a::Valuereachesa, not a namesake incuse osiris_types::Configdoes not reachserver'sConfiguse osiris_types::Configreachesosiris-typeswhen both define ituseof a re-exporting crate reaches the defining crate, not an unrelated namesakepub(crate) usedoes not leak out of its crateINFERRED, notEXTRACTEDusewith no reference,std,crate::, same-name types without auseuse super::Itemthrough a re-export reaches the defining crateusekeepsEXTRACTEDGraphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments. (N/A: no skill fragments touched.)