In Rust, a method inside an impl that carries parameters — generic or lifetime — gets its qualified_name from the trait rather than from the implementing type. Those methods are then unaddressable by their type, and they collide with the trait's own declaration.
impl Source for FileSource → FileSource::read correct
impl<T> Source for BufSource<T> → Source::read should be BufSource::read
A lifetime alone is enough (impl<'a> Iterator for Parents<'a> → Iterator::next).
Minimal reproduction
// src/lib.rs
pub trait Source {
fn read(&mut self) -> usize;
}
pub struct FileSource { pub n: usize }
impl Source for FileSource {
fn read(&mut self) -> usize { self.n }
}
pub struct BufSource<T> { pub inner: T }
impl<T> Source for BufSource<T> {
fn read(&mut self) -> usize { 0 }
}
pub struct UsesFile { pub src: FileSource }
impl UsesFile {
pub fn go(&mut self) -> usize { self.src.read() }
}
pub struct UsesBuf { pub src: BufSource<u8> }
impl UsesBuf {
pub fn go(&mut self) -> usize { self.src.read() }
}
SELECT qualified_name, start_line FROM nodes WHERE kind = 'method' ORDER BY start_line;
Actual:
Source::read 2 <- the trait declaration
FileSource::read 7
Source::read 12 <- impl<T> Source for BufSource<T>, should be BufSource::read
UsesFile::go 18
UsesBuf::go 23
BufSource::read is absent from the graph, and Source::read names both the trait declaration and BufSource's implementation.
Two consequences
Methods cannot be found by type. resolveMethodOnType("BufSource", "read") matches nothing, and "who calls BufSource::read" has no answer.
Dispatch synthesis produces edges the source does not contain. From the same index:
Source::read -> FileSource::read line 2 heuristic synthesizedBy=interface-impl
Source::read -> FileSource::read line 12 heuristic synthesizedBy=interface-impl
The first, from the trait declaration, is intended. The second originates at line 12 — the body of impl<T> Source for BufSource<T>, which is { 0 } and contains no call at all. Carrying the name Source::read made the synthesizer treat that impl body as the declaration, so it was given a call edge to a different implementation.
Impact
The shape is everyday Rust — any impl carrying parameters triggers it, a lifetime alone included:
// ripgrep crates/ignore/src/dir.rs:733
impl<'a> Iterator for Parents<'a> { // its next becomes Iterator::next
// tokio tokio-stream/src/empty.rs:40
impl<T> Stream for Empty<T> { // its poll_next becomes Stream::poll_next
It also compounds: every other generic implementation of Stream in that crate lands on the same Stream::poll_next, with nothing left to tell them apart.
Suspected cause
The implementing type looks to be taken as the last type_identifier child of the impl block. When that type carries parameters it parses as a generic_type, so the only bare type_identifier left is the trait's. The grammar exposes trait and type as named fields on impl_item, which could be read directly.
We have not dug further; this is only a rough direction.
Environment
Reproduced on the v1.5.0 release (npm @colbymchenry/codegraph@1.5.0) and on a local build of current main (81e1f4a). macOS arm64, Node v24.7.0.
In Rust, a method inside an
implthat carries parameters — generic or lifetime — gets itsqualified_namefrom the trait rather than from the implementing type. Those methods are then unaddressable by their type, and they collide with the trait's own declaration.A lifetime alone is enough (
impl<'a> Iterator for Parents<'a>→Iterator::next).Minimal reproduction
Actual:
BufSource::readis absent from the graph, andSource::readnames both the trait declaration andBufSource's implementation.Two consequences
Methods cannot be found by type.
resolveMethodOnType("BufSource", "read")matches nothing, and "who callsBufSource::read" has no answer.Dispatch synthesis produces edges the source does not contain. From the same index:
The first, from the trait declaration, is intended. The second originates at line 12 — the body of
impl<T> Source for BufSource<T>, which is{ 0 }and contains no call at all. Carrying the nameSource::readmade the synthesizer treat that impl body as the declaration, so it was given a call edge to a different implementation.Impact
The shape is everyday Rust — any
implcarrying parameters triggers it, a lifetime alone included:It also compounds: every other generic implementation of
Streamin that crate lands on the sameStream::poll_next, with nothing left to tell them apart.Suspected cause
The implementing type looks to be taken as the last
type_identifierchild of theimplblock. When that type carries parameters it parses as ageneric_type, so the only baretype_identifierleft is the trait's. The grammar exposestraitandtypeas named fields onimpl_item, which could be read directly.We have not dug further; this is only a rough direction.
Environment
Reproduced on the v1.5.0 release (npm
@colbymchenry/codegraph@1.5.0) and on a local build of currentmain(81e1f4a). macOS arm64, Node v24.7.0.