fix(rust): qualify generic/lifetime impl methods by the implementing type, not the trait (#1588) (#1596)
Fixes #1588. ## What was wrong The receiver of an `impl` block — the name that qualifies its methods, owns the `contains` edge, and sources the `implements` edge — was found positionally: the **last bare `type_identifier` child** of the `impl_item`. That works for `impl Source for FileSource`. But once the implementing type carries parameters it parses as a `generic_type`, and the only bare identifier left is the **trait's**: ```rust impl Source for FileSource → FileSource::read ✓ impl<T> Source for BufSource<T> → Source::read ✗ (should be BufSource::read) impl<'a> Iterator for Parents<'a> → Iterator::next ✗ impl Trait for &Foo → Trait::method ✗ ``` Two consequences, both reproduced on `main`: - `BufSource::read` did not exist in the graph, so `resolveMethodOnType("BufSource", "read")` and "who calls `BufSource::read`" had no answer, and every generic implementation of a trait collapsed onto the same trait-qualified name. - Because the impl's method carried the trait's qualified name, the interface-impl synthesizer treated the impl **body** as a second trait declaration and emitted a dispatch edge from it (`Source::read -> FileSource::read`, registered at the generic impl's line — a body of `{ 0 }` containing no call at all). The native kernel (`rustlang.rs`) mirrored the positional rule deliberately, bug-for-bug, to hold byte-parity with the TS walker — its header said "preserve, never fix via the grammar's trait:/type: fields". So the fix has to land on both sides at once. ## What this does Both extractors now read the grammar's **named fields** instead of scanning children. One shared rule (`rustImplTypeName` in `languages/rust.ts`, `impl_type_name` in the kernel), applied to `impl_item.type`: | implementing type | node | receiver | |---|---|---| | `Foo` | `type_identifier` | `Foo` | | `Foo<T>` / `Foo<'a>` | `generic_type` → its `type` field | `Foo` | | `m::Foo` | `scoped_type_identifier` → its `name` field | `Foo` (was: no receiver) | | `&Foo` / `&'a mut Foo` | `reference_type` → its `type` field | `Foo` | | `(A, B)`, `dyn Tr`, `*const T`, `u32`, fn types | anything else | none — extracted as plain functions, exactly as before | The `implements` back-reference reads `impl_item.trait` (full text, so `fmt::Display` and `From<u32>` keep their spelling) and bails when the field is absent (inherent impl). Everything else — the no-scope impl quirk, the source-order `contains` owner scan, method extraction — is untouched; the `contains` edge simply lands on the implementing type now instead of the trait. The kernel header comment, the parity test's description, and the two design docs that documented the quirk as "preserve" are updated to say what changed. ## Measured on ripgrep (110 `.rs` files, `main` build vs this branch) | | main | this PR | |---|---|---| | nodes / methods | 4029 / 2202 | 4029 / 2202 | | impl methods qualified by a **trait** name (node outside that trait's extent) | 61 | **0** | | `Iterator::*` methods | 2 | 0 | | duplicate method qualified names | 77 | 42 | | synthesized `interface-impl` edges originating **outside** any trait declaration (the phantom fan-outs) | 38 | **0** | | synthesized `interface-impl` edges originating at a real trait declaration | 33 | **52** | | plain (non-heuristic) `calls` edges | 9098 | 9098 | So the synthesizer lost every phantom edge and *gained* 19 legitimate fan-outs to implementations it could not previously see as implementations. `contains` edges went 5237 → 5224: the 13 removed were trait→impl-method edges produced by the mis-qualification. The issue's repro now gives `BufSource::read` at line 12, `BufSource -> Source`, and both synthesized edges registered at the declaration (line 2) — identical on the kernel path and with `CODEGRAPH_KERNEL=0`. (The remaining `UsesFile::go -> BufSource::read` exact-match guess there is the separate `self.field.method()` receiver problem, #1585, which stacks on this.) ## Tests - `__tests__/extraction.test.ts` (Rust Extraction): method qualified names for generic / lifetime / reference / scoped / generic-trait impls; the trait's qualified name names exactly one node; `implements` refs come from the implementing type for every shape; the `contains` edge lands on the type; tuple / `dyn` impls keep producing plain functions with no `implements` ref. - `__tests__/resolution.test.ts` (end-to-end): `Source::read` names only the declaration; dispatch fans out to **both** `FileSource::read` and `BufSource::read`, every synthesized edge registered at line 2; neither impl body sprouts a synthesized call. - `__tests__/fixtures/kernel-parity/torture.rs` grows all the new impl shapes; `kernel-rustlang-parity` (LF + CRLF) passes against the rebuilt kernel. - `CODEGRAPH_KERNEL_EXPECT=1 npx vitest run __tests__/kernel-*.test.ts` — all 15 suites, 147 tests pass. - Full `npm test`: 3180 passed, 9 skipped, 1 failed — `mcp-daemon.test.ts > daemon idle-times-out after the last client disconnects`, a 30 s timing test that passed on re-run in isolation (the machine was running four parallel suites and kernel builds at the time); unrelated to extraction. Re-index after upgrading to pick up the corrected names. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
This commit is contained in:
@@ -148,9 +148,11 @@ them are the ORIGINAL plan and carry expectations that measurement later correct
|
||||
tokio node sections IDENTICAL, small precision-positive edge churn only,
|
||||
full suite green), walker `codegraph-kernel/src/rustlang.rs` (survey
|
||||
artifact: rust-lang-kernel-port-checklist.md — isAsync dead-code,
|
||||
impl-pushes-no-scope, trait-receiver bug on `impl Trait for Generic<T>`,
|
||||
phantom const identifiers, use-binding triple emission, all preserved
|
||||
bug-for-bug). Gates: parity sweeps **0 diffs** on ripgrep (101/101,
|
||||
impl-pushes-no-scope, trait-receiver bug on `impl Trait for Generic<T>`
|
||||
(fixed on both sides together in #1588 — receiver now comes from the
|
||||
impl_item's `type` field), phantom const identifiers, use-binding
|
||||
triple emission, all preserved bug-for-bug). Gates: parity sweeps
|
||||
**0 diffs** on ripgrep (101/101,
|
||||
0 deferred) / tokio (790/790, 0 deferred) / rust-analyzer (1217/1488,
|
||||
0 diffs; 271 deferrals are token-macro-table sources — `T![~]`, `[$]` —
|
||||
that error on BOTH arms, grammar-inherent like fmt's C++ 42%); full-init
|
||||
|
||||
@@ -89,18 +89,21 @@ Hooks PRESENT (port each exactly):
|
||||
- **getVisibility (rust.ts:74)** — direct child of type `visibility_modifier`:
|
||||
text `.includes('pub')` → `'public'` else `'private'`; no modifier →
|
||||
`'private'` (so `pub(crate)`/`pub(super)` are all `'public'`).
|
||||
- **getReceiverType (rust.ts:83)** — walk PARENT chain to the nearest
|
||||
`impl_item`; there: filter DIRECT namedChildren of type `type_identifier`;
|
||||
if ≥1, return the LAST one's source text (`source.substring(startIndex,
|
||||
endIndex)` — UTF-16 units). If none, find the first `generic_type` child and
|
||||
return its inner `type_identifier` text; else undefined. Never an impl parent
|
||||
→ undefined. QUIRK/BUG, PRESERVE: for `impl Trait for Generic<T>` the only
|
||||
direct type_identifier is the TRAIT (probe: `impl Render for Container<T>` →
|
||||
typeIdents=[`Render`] → receiver = **`Render`**, the trait name — methods get
|
||||
qualifiedName `Render::render` and a contains edge from the trait node if one
|
||||
exists in-file). `impl fmt::Display for Fields` is fine
|
||||
(scoped_type_identifier isn't type_identifier → [Fields]). `impl<T>
|
||||
Container<T>` → no direct type_identifiers → generic branch → `Container`.
|
||||
- **getReceiverType (rust.ts)** — walk PARENT chain to the nearest
|
||||
`impl_item`; there, read the grammar's `type` field through
|
||||
`rustImplTypeName` (kernel: `impl_type_name`): `type_identifier`/`identifier`
|
||||
→ text; `generic_type` → its `type` field (bare name, never the args);
|
||||
`scoped_type_identifier`/`scoped_identifier` → its `name` field (last
|
||||
segment); `reference_type` → its `type` field; anything else (tuple, `dyn`,
|
||||
pointer, primitive, fn type) → undefined. Never an impl parent → undefined.
|
||||
**Changed in #1588 on both sides together**: the original rule took the LAST
|
||||
direct `type_identifier` child, so for `impl Trait for Generic<T>` /
|
||||
`Parents<'a>` / `&Foo` the only bare identifier was the TRAIT's (probe:
|
||||
`impl Render for Container<T>` → receiver **`Render`** → methods
|
||||
`Render::render`, colliding with the trait declaration and feeding the
|
||||
interface-impl synthesizer a phantom declaration). Now `Container`.
|
||||
`impl fmt::Display for Fields` → `Fields`; `impl<T> Container<T>` →
|
||||
`Container`; `impl Tr for m::Foo` → `Foo` (was: no receiver).
|
||||
Note `<T>` type_parameters is its own child, its inner T is NOT a direct
|
||||
impl child.
|
||||
- **extractImport (rust.ts:120)** — signature = trimmed full `use …;` text.
|
||||
@@ -187,8 +190,9 @@ undefined; **no isConst means `const_item`/`static_item` extract as kind
|
||||
present AND not class-like — finds the FIRST node in `this.nodes` with
|
||||
`name === receiverType && filePath === this.filePath && kind ∈
|
||||
{struct,class,enum,trait}`. Source-order dependent: an impl ABOVE its struct
|
||||
gets no contains edge. `impl Trait for Generic<T>` (receiver=trait bug) links
|
||||
to the TRAIT node if it's in-file.** Then type annotations, decorators
|
||||
gets no contains edge. Since #1588 `impl Trait for Generic<T>` links to the
|
||||
implementing TYPE's node (it used to link to the TRAIT node, the receiver
|
||||
bug).** Then type annotations, decorators
|
||||
(no-op), body walk with the method pushed.
|
||||
- **Nested `fn` inside an impl-method's body**: visitFunctionBody:5245 →
|
||||
named → extractFunction → getReceiverType walks parents THROUGH the outer fn
|
||||
|
||||
Reference in New Issue
Block a user