fix(core): scope Rust impl methods to their type, dedup class-scoping walk - #72
fix(core): scope Rust impl methods to their type, dedup class-scoping walk#72pradeepmouli wants to merge 1 commit into
Conversation
… walk tree-sitter-rust's impl_item has no "name" field (only body/trait/type/ type_parameters, verified against its real node-types.json), so every Rust impl method -- inherent or trait -- fell back to a flat, unscoped file::method id. Two types in the same file with a same-named method (e.g. both implementing Display, or both having new()) silently collapsed into one symbol, with the second dropped entirely by store_parquet.rs's sym_seen dedup guard. Fix: rust/entities.scm now captures the impl's own `type:` field as @method.parent; entities.rs consumes it via the same decompose-query mechanism already used for compound INHERITS-edge bases (generic/ qualified impl targets), falling back to the existing ancestor walk when absent -- same capture-with-fallback idiom as @func.params/ @func.return_type. Also consolidates find_parent_class (entities.rs) and find_enclosing_class (relations.rs), which had drifted into two independently-buggy copies of the same class-scoping walk (one had struct_specifier and the C++ out-of-line-method branch, the other had Elixir's defmodule and Pascal's declClass/declIntf) -- now one shared function in extract/mod.rs, used by both. Known residual gap, not fixed here (tracked as #125): a single type with two same-named methods from different sources (e.g. an inherent impl and a trait impl of the same type) still collide, since both resolve to the same parent name. That needs the separate disambiguation work in #126. Part of the symbol-identity-and-scoping-hardening spec's Phase 1 (docs/superpowers/specs/2026-08-30-symbol-identity-and-scoping-hardening-design.md).
murari316
left a comment
There was a problem hiding this comment.
Automated review (Fable, adversarially verified). Recommendation: request-changes.
The PR fixes a real bug — Rust impl methods were file-scoped (file::method) because tree-sitter-rust's impl_item has no "name" field, so same-named methods on different types collapsed into one symbol. It adds an explicit @method.parent capture on the impl's type: field, threads a decompose_query through extract_entities to reduce compound types (generics/scoped paths) to a base identifier, and consolidates two independently-drifted walk helpers (find_parent_class/find_enclosing_class) plus resolve_inherit_text into shared extract/mod.rs functions. The core fix and the dedup are sound and none of the repo's DB/watcher/ignore invariants are touched, but the fix introduces an untested ghost-symbol regression: the unanchored standalone (function_item) @func.def pattern still matches impl methods with a flat id, and the new type-scoped id no longer collides with it, so dedup no longer collapses the pair.
[MAJOR] correctness — crates/infigraph-languages/languages/rust/entities.scm:4
The PR introduces a ghost duplicate symbol for Rust impl methods. The standalone, unanchored pattern (function_item name: (identifier) @func.name) @func.def (unchanged by this PR) also matches every function inside an impl block's declaration_list. That match carries no @method.parent capture, so extract_entities falls back to find_parent_class, which walks to impl_item, finds no "name" field (impl_item only has body/trait/type/type_parameters), and returns None — yielding a flat file::method Function symbol. Pre-PR, the @method.def match produced the same flat id and the HashMap dedup (keyed on Symbol.id, entities.rs) collapsed the pair into one symbol; post-PR the @method.def match gets the new scoped file::Type::method id, the ids diverge, and both survive. Net effect: one ghost flat Function symbol per unique impl-method name per file, alongside the correctly scoped Method symbols — polluting search results, get_symbols_in_file, dead-code detection (the ghost has no incoming edges), and name-based call resolution which may bind to the flat id. Suggested fix (two halves, both needed): (1) in the consolidated find_parent_class (extract/mod.rs), resolve impl_item via child_by_field_name("type") (run through resolve_compound_node_text or take the base identifier) so the @func.def match gets the same scoped id — this also fixes Rust self. receiver resolution in relations.rs for free; (2) add a Function→Method kind upgrade to the dedup's and_modify (mirroring the existing Function→Test rule), otherwise the merged symbol keeps whichever kind was inserted first and may stay Function. Do NOT fix by anchoring the func pattern to source_file — that would stop matching mod-nested free functions.
Failure scenario: Index any Rust repo (including infigraph itself) with the PR applied. For impl Alpha { fn hello() {} } and impl Greet for Beta { fn hello() {} } in src/main.rs, extraction now emits THREE symbols named hello: src/main.rs::Alpha::hello (Method), src/main.rs::Beta::hello (Method), and a ghost src/main.rs::hello (Function, spanning one of the two method bodies). detect_dead_code flags the ghost as unreferenced; symbol counts and search results inflate for every impl method in every indexed Rust file.
[MAJOR] test-coverage — crates/infigraph-core/src/extract/entities.rs:1836
The regression test structurally cannot catch the ghost-duplicate bug above, because rust_entity_query() hand-builds a query containing ONLY the impl-method pattern — the shipped rust/entities.scm's standalone (function_item) @func.def pattern (whose interaction with the new scoped ids causes the regression) is absent, so the test filters on kind==Method and sees exactly the 2 symbols it expects. It also drifts silently if the shipped .scm changes. The helper's own doc comment cites a dev-dependency cycle as the reason, but the .scm files are plain text: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../infigraph-languages/languages/rust/entities.scm")) (and same for inherit_decompose.scm) loads the real shipped queries with no crate dependency at all. Fix: build the Query from the include_str! text, and add an assertion that NO flat src/main.rs::hello symbol exists (any kind). Additionally, the test's outer doc comment contradicts the code — it claims "Uses the real bundled Rust language pack (entities.scm + inherit_decompose_query), not a hand-rolled query" while the helper comment two lines later says "this test hand-builds the query rather than loading the registry"; the first claim is false and should be corrected regardless.
Failure scenario: CI passes (both new tests green) while every real Rust indexing run through extract_file — which compiles the full entities.scm including the standalone function pattern — emits the ghost flat Function symbols described in finding 1. The exact scenario the PR fixes is only half-tested: distinct scoped ids are asserted, absence of the old flat id is not.
[NIT] documentation — crates/infigraph-core/src/extract/entities.rs:221
The comment above the parent resolution says "Prefer an explicit @method.parent/@func.parent capture", but the capture match arm only handles "method.parent" — a grammar author who adds @func.parent per the comment gets it silently ignored (falls through the _ => {} arm) and their functions fall back to the ancestor walk. Either handle "func.parent" in the match (one extra arm setting the same parent_capture) or drop "@func.parent" from the comment and the module doc.
Failure scenario: A future language pack adds @func.parent to its entities.scm following the doc comment; the capture is ignored, functions stay unscoped, and nothing warns — the same silent-file-scoping failure mode this PR fixes for Rust methods.
Generated with Claude Code (Fable). Findings verified by an adversarial refute pass; false positives removed.
Summary
tree-sitter-rust'simpl_itemnode has nonamefield — onlybody,trait,type, andtype_parameters(verified against the crate's realnode-types.json, not assumed).find_parent_class's genericchild_by_field_name("name")walk silently returnsNonefor it, so every Rust impl method — inherent or trait — fell back to a flat, unscopedfile::methodid instead offile::Type::method.Concretely, this meant any two types in the same file with a same-named method (e.g. both implementing
Display, or both having anew()) silently collapsed into a single graph symbol, with the second one dropped entirely bystore_parquet.rs'ssym_seendedup guard — not just mis-scoped, actually missing from the graph.Reproduced empirically before fixing: indexing a fixture with
struct Alpha(impl Alpha { fn hello(&self) -> String }) andstruct Beta(impl Greet for Beta { fn hello(&self) -> String }, identical signature) produced only onehellosymbol in the graph —Beta::hellowas silently absent.Fix
rust/entities.scm's impl-method pattern now also captures the impl's owntype:field as@method.parent(the Self type, e.g.Barinimpl Foo for Bar).entities.rsconsumes@method.parentthrough the same decompose-query mechanism (inherit_decompose_query) that relation extraction already uses for compoundINHERITS-edge bases (generic/qualified impl targets) — reused, not duplicated. Falls back to the existing ancestor walk when the capture is absent, following the same capture-with-fallback idiom already established for@func.params/@func.return_type.find_parent_class(entities.rs) andfind_enclosing_class(relations.rs), which had drifted into two independently-buggy copies of the same class-scoping walk (one hadstruct_specifierand the C++ out-of-line-method branch, the other had Elixir'sdefmoduleand Pascal'sdeclClass/declIntf) — now one shared function inextract/mod.rs, used by both.No schema change; purely additive to
entities.scm/entities.rs. Does not touchrelations.scm.Known residual gap (not fixed by this PR)
A single type with two same-named methods from different sources — e.g. an inherent impl and a trait impl of the same type (
impl Bar { fn x() {} }+impl SomeTrait for Bar { fn x() {} }) — still collide, since both resolve@method.parentto the same type name. That's a genuine overload-disambiguation problem, not a scoping bug, and needs separate work (tracked in my fork, not part of this PR's scope). The added testtest_rust_same_type_inherent_and_trait_impl_method_still_collidedocuments this explicitly as a known, tracked gap rather than letting it pass silently as "fixed."Testing
entities.rs's test module: one confirms the fix (Alpha::hello/Beta::hellonow survive as distinct, correctly-scoped symbols), one explicitly documents the known residual gap above.tree-sitter-rustas aninfigraph-coredev-dependency to hand-build the test'sQuerydirectly (mirroring the existing Kotlin/Dart test pattern) — usinginfigraph_languages::bundled_registry()from insideinfigraph-core's own inline tests hits a dev-dependency-cycle type mismatch (two distinct compiled instances ofParserBackend), confirmed via compiler error, not guessed around.upstream/main(not reusing my fork's branch results):cargo build --release -p infigraph-cli -p infigraph-mcpsucceeds, full workspacecargo test --workspace --libpasses (738 tests, 0 failures),cargo fmt --all -- --checkclean.clippy:cargo clippy --all-targets -- -D warningson bareupstream/main(before this PR's changes) currently fails on an unrelated pre-existing lint incrates/infigraph-core/src/embed/mod.rs(chunks_exact_to_as_chunks) — confirmed this file isn't touched by this PR's diff at all, so it's local-toolchain-vs-CI drift (per this repo's ownCLAUDE.md:dtolnay/rust-toolchain@stablefloats, so a newer local clippy can surface lints CI's pinned version doesn't), not something this PR introduces or is responsible for fixing.🤖 Generated with Claude Code