Skip to content

Rust: a method call on a typed field resolves to a same-named method on the wrong type #1585

Description

@Dshuishui

In Rust, a method call whose receiver is a typed field can resolve to a method of the same name on an unrelated type — including the calling method itself, producing a self-recursive edge that does not exist in the source. It also happens when the receiver's type is not a local one at all (a std container, an external crate, a generic parameter): the call is attached to a same-named local method.

The same structure in C++ resolves correctly (Outer::run -> Inner::run), so this looks specific to Rust.

Minimal reproduction

# Cargo.toml
[package]
name = "repro"
version = "0.1.0"
edition = "2021"
// src/lib.rs
pub mod inner;
pub mod outer;
// src/inner.rs
pub struct Inner {
    pub n: usize,
}

impl Inner {
    pub fn run(&mut self) {
        self.n += 1;
    }
}
// src/outer.rs
use crate::inner::Inner;

pub struct Outer {
    pub inner: Inner,
}

impl Outer {
    pub fn run(&mut self) {
        self.inner.run();
    }
}
codegraph init && codegraph index .
SELECT s.qualified_name || ' -> ' || t.qualified_name, e.line
FROM edges e JOIN nodes s ON e.source = s.id JOIN nodes t ON e.target = t.id
WHERE e.kind = 'calls';

Actual:

Outer::run  ->  Outer::run     line 9

Outer::run does not call itself. self.inner has its type declared on the struct, so Outer::run -> Inner::run is what the source says. Resolving it correctly would be ideal; leaving the ref in unresolved_refs would at least be safe — as it stands unresolved_refs holds no entry for this call, so a consumer has no way to tell the edge was a guess.

Impact

The shape is common in idiomatic Rust: a wrapper or delegating struct holds a field of another type and forwards a method of the same name, or the field is a std container / external crate type while a local method happens to share its name.

Three real examples from ripgrep — in all three the receiver is not self, yet each is recorded as a self-recursive edge:

// crates/ignore/src/walk.rs:824      ig_builder: IgnoreBuilder
self.ig_builder.add_custom_ignore_filename(file_name);

// crates/ignore/src/walk.rs:1195     its: std::vec::IntoIter<...>
match self.its.next() {

// crates/globset/src/lib.rs:983      matcher: Regex
self.matcher.is_match(candidate.path.as_bytes())

There are a few hundred such non-existent self-edges in ripgrep and more in tokio (SELECT count(*) FROM edges WHERE kind = 'calls' AND source = target, then checking the receiver at each call site). Self-edges are only the subset that is easy to falsify — a wrong pick that lands on some other method cannot be spotted this way, so the affected set is larger.

A few self-edges are genuine recursion (e.g. ripgrep's Error::is_partial unwrapping a nested error), and are not counted above.

Environment

Reproduced both 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