Skip to content

fix(rust): expand grouped use declarations into import bindings - #4135

Closed
krishhgg wants to merge 1 commit into
Graphify-Labs:v8from
krishhgg:fix/rust-grouped-use
Closed

krishhgg wants to merge 1 commit into
Graphify-Labs:v8from
krishhgg:fix/rust-grouped-use

Conversation

@krishhgg

@krishhgg krishhgg commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

use crate::util::{parse, render}; produces one import edge to util; neither binding gets its own edge. A root group such as use {a::b, c}; produces no edge.

This expands each grouped use into the bindings of the equivalent plain declarations. It uses v8's existing import-target rule, preserving path text, aliases, comments, spacing, glob imports and nested self bindings. Each edge comes from the file node at the binding's line. Plain declarations and call edges stay unchanged.

The group edge temporarily feeds the existing call-evidence pass. The per-binding edges do not feed it. After call resolution, the group edge and temporary markers are removed. AST cache schema advances from 6 to 7.

This replaces the earlier broader resolver in this PR. That resolver is preserved locally for separate work; this change only expands syntax and does not read Cargo.toml or resolve module paths.

Cache coordination: #4208 also advances schema 6 to 7. If it ships first, this branch needs a fresh rebase and another schema increment.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariant: grouped imports produce exactly the import nodes and edges that current v8 produces for the equivalent plain declarations, including its textual behavior. Existing plain-use and call output stays unchanged.

  • Read CONTRIBUTING.md.
  • Reproduced the issue and identified the invariant.
  • Kept the fix scoped to grouped-use expansion.
  • Added regression and boundary tests.
  • Synchronized this description with the implementation.
  • Documented limitations.

Limitations:

  • v8 resolves import stubs by name. Expanding grouped syntax exposes more bindings to that existing rule, including wrong same-name matches. This PR makes no precision guarantee beyond equivalence to plain imports.
  • Function-body use declarations remain unsupported.
  • Windows was not tested.

How was this tested?

Verified against v0.9.80, 6478eb7.

uv run --offline pytest tests/test_rust_use_trees.py -q
  current v8: 26 failed, 4 passed
  this PR: 30 passed

uv run --offline pytest tests -q -p no:cacheprovider
  current v8: 6612 passed, 139 skipped
  this PR: 6642 passed, 139 skipped, no failures

uv run --offline ruff check .
  clean
uv run --offline pyright
  637 errors, 4 warnings, identical diagnostics to current v8

graphify update <repo> --no-cluster
  ripgrep: Rust import edges 247 -> 864; 3241 call edges unchanged
  serde_json: Rust import edges 248 -> 382; 887 call edges unchanged
  all source nodes and non-import edges preserved

SCIP import comparison
  ripgrep: 11/556 -> 175/556; wrong scored pairs 1 -> 4
  serde_json: 5/147 -> 8/147; wrong scored pairs 3 -> 13

Tests compare extracted and persisted grouped/plain output and check watch and CLI incremental parity across edits. Astra also compiled and checked independent comment, alias, nested-prefix and glob cases. All temporary edge markers disappear before persisted output.

Graphify-specific checklist

  • Regenerated skill artifacts. No fragments changed.
  • Kept AST extraction deterministic.
  • Reviewed parser and cache behavior.
  • Included no API keys or local graph artifacts.
  • Disclosed AI contributions in commit metadata.

@krishhgg
krishhgg requested a review from safishamsi as a code owner October 6, 2026 00:38
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Thanks for the pull request, @krishhgg. 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.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. PR-changed functions: 0/4 verified (0 proven, 0 may-equivalent, 0 distinguished) · 4 not verified (2 vacuous, 1 unsupported, 1 witness not reproduced).

Not verified on this run: dispatch\_command (vacuous: never exercised), extract (vacuous: never exercised), extract\_rust (witness not reproduced), \_rebuild\_code (unsupported).


Graphify review — findings

Adds Rust use resolution: extract_rust now records each module scope's declarations, use bindings, globs, extern crates and #[path] attributes. After the shared call pass, resolve_rust_use_targets replaces the provisional per-declaration import edges with one edge per binding, pointing at the module file or item it names; if resolution fails, it logs a warning and skips. Incremental runs also pull affected Rust files back in via refresh_rust_paths, and _AST_CACHE_SCHEMA moves to 6, so existing AST caches re-extract.

Worth a look

  • Valid raw-string #[path] attributes are treated as unreadable — graphify/extractors/rust.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review

Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 3123 functions depend on the 603 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 781 callers, 51 callees
  • new: _rebuild_code() — 156 callers, 57 callees
  • new: detect() — 121 callers, 16 callees
  • new: save_semantic_cache() — 65 callers, 9 callees
  • new: load_cached() — 56 callers, 7 callees
  • new: extract_js() — 87 callers, 4 callees
  • new: file_hash() — 54 callers, 6 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • …and 60 more — each is listed as a finding

Verification — 3123 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: 3049 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

192 of 335 test file(s) selected (57%) via static blast radius.

  • tests/test_affected_cli.py — impact
  • tests/test_agents_platform.py — impact
  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_blade_extractor.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_cache.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cobol_extractor.py — impact
  • tests/test_codebuddy.py — impact
  • tests/test_cpp_method_declarations.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_external_call_guards.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_tuple_type_refs.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_dedup_shrink_refuses_force_write.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_devin.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_elixir_import_resolution.py — impact
  • tests/test_elixir_unqualified_call_scope.py — impact
  • tests/test_erlang_extractor.py — impact
  • tests/test_explain_cli.py — impact
  • tests/test_export_direction.py — impact
  • tests/test_external_stub_endpoints.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_extract_cli.py — impact
  • tests/test_extract_php_closures.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • … and 142 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)

…and 10 more.

Formal verification

Could not verify: extract\_rust: a possible difference could NOT be confirmed.

The solver proposed a distinguishing input for extract\_rust, but re-running both versions on it did NOT reproduce a difference. Treat as unconfirmed, not a behavior change.

Guarantee:

Note: Solver-proposed witness refuted on replay — not a hard failure.

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, 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: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify extract.

The verifier did not have enough to check extract, 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: all 90 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_rebuild\_code.

The verifier did not have enough to check \_rebuild\_code, 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: nondeterministic output (both versions disagreed with themselves on the witness) — not a behaviour change; a function whose output is not a function of its inputs (random / time / uuid / os.urandom, or state carried across calls) cannot be soundly distinguished by execution, so this tier abstains for the whole function

· 3 grounded finding(s) anchored inline below; 65 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/rust_resolution.py Outdated
return current


def resolve_rust_use_targets(paths, per_file: list[dict | None], nodes: list[dict],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — resolve_rust_use_targets()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/rust_resolution.py Outdated
first_defs[real] = {item_id: entry[1] for item_id, entry in found.items()}
return first_defs[real].get(nid)

def node_for(target: tuple) -> str | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — node_for()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/rust_resolution.py Outdated
] + unique


def refresh_rust_paths(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — refresh_rust_paths()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Give each binding of a use tree the import edge that v8 produces for
its equivalent plain use declaration. Preserve aliases, path comments,
separator whitespace, nested prefixes and self/glob binding behavior.

Keep the old group edge as call evidence until the call pass finishes;
exclude per-binding edges from that pass, then remove private markers.
Plain imports and all call edges retain their current-v8 behavior.
AST cache schema 6 -> 7 rejects results written before expansion.

Replace the earlier broad resolver in Graphify-Labs#4135 with this syntax-only fix.
The resolver is preserved on local branch wip/rust-use-resolver.

On current v8, all 30 new tests and the full 6642-test suite pass.
Fresh ripgrep builds have 864 import edges instead of 247, with every
call unchanged. Existing name-based import rewiring can still produce
wrong matches, just as it does for equivalent plain declarations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
@krishhgg
krishhgg force-pushed the fix/rust-grouped-use branch from e368390 to 2669ee4 Compare October 7, 2026 23:13
@krishhgg krishhgg changed the title fix(rust): resolve grouped and aliased use declarations fix(rust): expand grouped use declarations into import bindings Oct 7, 2026
safishamsi pushed a commit that referenced this pull request Oct 11, 2026
Give each binding of a use tree the import edge that v8 produces for
its equivalent plain use declaration. Preserve aliases, path comments,
separator whitespace, nested prefixes and self/glob binding behavior.

Keep the old group edge as call evidence until the call pass finishes;
exclude per-binding edges from that pass, then remove private markers.
Plain imports and all call edges retain their current-v8 behavior.
AST cache schema 6 -> 7 rejects results written before expansion.

Replace the earlier broad resolver in #4135 with this syntax-only fix.
The resolver is preserved on local branch wip/rust-use-resolver.

On current v8, all 30 new tests and the full 6642-test suite pass.
Fresh ripgrep builds have 864 import edges instead of 247, with every
call unchanged. Existing name-based import rewiring can still produce
wrong matches, just as it does for equivalent plain declarations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
(cherry picked from commit 2669ee4)
@safishamsi

Copy link
Copy Markdown
Member

Landed in 0.9.86 (now on PyPI), authorship preserved. Resolved the cache-schema conflict by bumping _AST_CACHE_SCHEMA to 8 (HEAD had already reached 7), so Rust files re-extract on upgrade. Thank you, @krishhgg!

@safishamsi safishamsi closed this Oct 11, 2026
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.

2 participants