Skip to content

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

Description

@Dshuishui

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 FileSourceFileSource::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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions