Skip to content

fix(rust): qualify generic/lifetime impl methods by the implementing type, not the trait (#1588) - #1596

Open
colbymchenry wants to merge 1 commit into
mainfrom
fix/1588-rust-impl-type-qualification
Open

fix(rust): qualify generic/lifetime impl methods by the implementing type, not the trait (#1588)#1596
colbymchenry wants to merge 1 commit into
mainfrom
fix/1588-rust-impl-type-qualification

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

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:

impl Source for FileSourceFileSource::readimpl<T> Source for BufSource<T>Source::read(should be BufSource::read)
impl<'a> Iterator for Parents<'a>Iterator::nextimpl Trait for &FooTrait::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.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK

…type, not the trait (#1588)

`impl<T> Source for BufSource<T>` recorded its `read` as `Source::read`: the
receiver rule took the LAST bare `type_identifier` child of the impl_item,
and once the implementing type carries parameters it parses as a
`generic_type`, leaving the trait's identifier as the only bare one. The
method was then unaddressable by its type, collided with the trait's own
declaration, and — carrying the trait's name — the interface-impl
synthesizer treated the impl body as a second declaration and fanned out
dispatch edges from it (a body like `{ 0 }` with no call at all). A lifetime
alone triggered it (`impl<'a> Iterator for Parents<'a>`), as did a reference
implementing type (`impl Trait for &Foo`).

Both extractors now read the grammar's named fields: the implementing type
from `impl_item.type` (generic_type → its bare name, scoped → last segment,
reference → the inner type; tuple / dyn / pointer / primitive → no receiver,
exactly as before) and the trait from `impl_item.trait`. The native kernel
mirrored the old rule bug-for-bug for parity; it changes in lockstep here, so
the parity fixture grows the generic / lifetime / reference / scoped /
generic-trait impl shapes and the design notes drop the "preserve" marker.

ripgrep (110 files): nodes 4029 → 4029; trait-mis-qualified impl methods
61 → 0; synthesized dispatch edges originating outside any trait declaration
38 → 0 while genuine ones rose 33 → 52; plain calls edges unchanged (9098).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
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.

Rust: methods of a generic impl are qualified by the trait, not by the implementing type

1 participant