fix(rust): qualify generic/lifetime impl methods by the implementing type, not the trait (#1588) - #1596
Open
colbymchenry wants to merge 1 commit into
Open
fix(rust): qualify generic/lifetime impl methods by the implementing type, not the trait (#1588)#1596colbymchenry wants to merge 1 commit into
colbymchenry wants to merge 1 commit into
Conversation
…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
This was referenced Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1588.
What was wrong
The receiver of an
implblock — the name that qualifies its methods, owns thecontainsedge, and sources theimplementsedge — was found positionally: the last baretype_identifierchild of theimpl_item. That works forimpl Source for FileSource. But once the implementing type carries parameters it parses as ageneric_type, and the only bare identifier left is the trait's:Two consequences, both reproduced on
main:BufSource::readdid not exist in the graph, soresolveMethodOnType("BufSource", "read")and "who callsBufSource::read" had no answer, and every generic implementation of a trait collapsed onto the same trait-qualified name.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 (
rustImplTypeNameinlanguages/rust.ts,impl_type_namein the kernel), applied toimpl_item.type:Footype_identifierFooFoo<T>/Foo<'a>generic_type→ itstypefieldFoom::Fooscoped_type_identifier→ itsnamefieldFoo(was: no receiver)&Foo/&'a mut Fooreference_type→ itstypefieldFoo(A, B),dyn Tr,*const T,u32, fn typesThe
implementsback-reference readsimpl_item.trait(full text, sofmt::DisplayandFrom<u32>keep their spelling) and bails when the field is absent (inherent impl). Everything else — the no-scope impl quirk, the source-ordercontainsowner scan, method extraction — is untouched; thecontainsedge 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
.rsfiles,mainbuild vs this branch)Iterator::*methodsinterface-impledges originating outside any trait declaration (the phantom fan-outs)interface-impledges originating at a real trait declarationcallsedgesSo the synthesizer lost every phantom edge and gained 19 legitimate fan-outs to implementations it could not previously see as implementations.
containsedges went 5237 → 5224: the 13 removed were trait→impl-method edges produced by the mis-qualification.The issue's repro now gives
BufSource::readat line 12,BufSource -> Source, and both synthesized edges registered at the declaration (line 2) — identical on the kernel path and withCODEGRAPH_KERNEL=0. (The remainingUsesFile::go -> BufSource::readexact-match guess there is the separateself.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;implementsrefs come from the implementing type for every shape; thecontainsedge lands on the type; tuple /dynimpls keep producing plain functions with noimplementsref.__tests__/resolution.test.ts(end-to-end):Source::readnames only the declaration; dispatch fans out to bothFileSource::readandBufSource::read, every synthesized edge registered at line 2; neither impl body sprouts a synthesized call.__tests__/fixtures/kernel-parity/torture.rsgrows 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.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