Summary
A self.method() call in Rust has its receiver stripped to a bare method name, which is then resolved among all same-named methods by file-path proximity. When another type happens to have a same-named method closer to the call site, the result is a calls edge pointing at an unrelated type — carrying no provenance and a plain confidence, so it is indistinguishable from a correctly resolved edge.
The type of self is purely syntactic — it is written on the enclosing impl line — and codegraph already reads it: the caller on the very same edge is correctly recorded as Target::run.
Reproduction
One file. Verified on the official codegraph 1.6.0 build, rustc 1.96.0, cargo check passes.
# Cargo.toml
[package]
name = "samefile"
version = "0.1.0"
edition = "2021"
// src/lib.rs
pub struct Target { pub n: i32 }
impl Target {
pub fn reset(&mut self) { self.n = -1; }
}
pub struct Decoy { pub n: i32 }
impl Decoy {
pub fn reset(&mut self) { self.n = 0; } // nearer the call site below
}
impl Target {
pub fn run(&mut self) { self.reset(); } // rustc resolves this to Target::reset
}
codegraph init && codegraph index .
caller resolved to refName resolvedBy confidence
Target::run Decoy::reset reset exact-match 0.4
The caller is recorded as Target::run, and the same row sends self.reset() to Decoy.
The same thing happens across directories, which shows proximity is what decides it. Identical code, only the directory changes:
impl Target { fn f(&mut self) { self.reset(); } }
placed in src/alpha/ (next to Decoy) -> Decoy::reset wrong
placed in src/beta/ (next to Target) -> Target::reset right
Both of those edges carry confidence: 0.7, so confidence does not separate the wrong answer from the right one.
Where it comes from
In src/extraction/tree-sitter.ts, at the site that emits the calls reference:
const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']);
if (receiver && (receiver.type === 'identifier' || ...)) {
const receiverName = getNodeText(receiver, this.source);
if (!SKIP_RECEIVERS.has(receiverName)) {
calleeName = `${receiverName}.${methodName}`;
} else {
calleeName = methodName; // self.reset() -> bare `reset`
}
}
The bare name reaches matchByExactName, and among same-named candidates the winner is decided by the path-proximity term in findBestMatch.
The enclosing type is available: getReceiverType() in src/extraction/languages/rust.ts already walks up to the impl_item and reads its type field — that is why the caller is recorded as Target::run.
The branch immediately after the snippet above already handles Rust's self.<field>.method() specifically (#1585), with a comment noting that the bare name "exact-matched whichever same-named method was nearest". self.method() looks like the other half of that gap.
Scale on real projects
Counting edges where the source line is genuinely self.<name>( and the caller and callee belong to different types (codegraph 1.6.0):
self.<bare>() calls resolved onto another type
ripgrep 600 15 (2.5%)
tokio 1039 277 (27%)
For tokio, about 80% of those 277 concentrate on three names:
165 self.project() generated by pin_project!, not in the graph
52 self.as_mut() std's Pin::as_mut
16 self.get_mut() std
The correct answer for those is not in the graph at all (macro-generated or external), but instead of leaving the ref unresolved, an edge onto an unrelated project type is emitted. The remaining ~60 are scattered project-internal methods (state, schedule, next, header, …).
Examples from ripgrep:
self.clone() inside impl Ignore -> Error::clone
self.len() inside impl Tokens -> GlobSet::len
self.glob() inside impl Glob -> GlobMatcher::glob
self.config() inside impl PreludeWriter -> StandardImpl::config
Why this shape shows up in Rust
I tried the equivalent shape in Python, Java and Go and all three resolved correctly: in those languages a type's methods live in one contiguous class body, so the enclosing type's own method is naturally the nearest candidate and proximity picks it. Rust allows a type's methods to be spread over several impl blocks and files, so another type's same-named method can sit between the call and its real target.
Suggested direction
When the receiver is self, restrict candidates to the target type of the enclosing impl — already derived by getReceiverType(). If no same-named method exists on that type, leave the ref in unresolved_refs rather than emitting a confident wrong edge. That would also cover the self.project() / self.as_mut() cases above, where the correct target is not in the graph to begin with.
Summary
A
self.method()call in Rust has its receiver stripped to a bare method name, which is then resolved among all same-named methods by file-path proximity. When another type happens to have a same-named method closer to the call site, the result is acallsedge pointing at an unrelated type — carrying noprovenanceand a plainconfidence, so it is indistinguishable from a correctly resolved edge.The type of
selfis purely syntactic — it is written on the enclosingimplline — and codegraph already reads it: the caller on the very same edge is correctly recorded asTarget::run.Reproduction
One file. Verified on the official codegraph 1.6.0 build, rustc 1.96.0,
cargo checkpasses.The caller is recorded as
Target::run, and the same row sendsself.reset()toDecoy.The same thing happens across directories, which shows proximity is what decides it. Identical code, only the directory changes:
Both of those edges carry
confidence: 0.7, so confidence does not separate the wrong answer from the right one.Where it comes from
In
src/extraction/tree-sitter.ts, at the site that emits thecallsreference:The bare name reaches
matchByExactName, and among same-named candidates the winner is decided by the path-proximity term infindBestMatch.The enclosing type is available:
getReceiverType()insrc/extraction/languages/rust.tsalready walks up to theimpl_itemand reads itstypefield — that is why the caller is recorded asTarget::run.The branch immediately after the snippet above already handles Rust's
self.<field>.method()specifically (#1585), with a comment noting that the bare name "exact-matched whichever same-named method was nearest".self.method()looks like the other half of that gap.Scale on real projects
Counting edges where the source line is genuinely
self.<name>(and the caller and callee belong to different types (codegraph 1.6.0):For tokio, about 80% of those 277 concentrate on three names:
The correct answer for those is not in the graph at all (macro-generated or external), but instead of leaving the ref unresolved, an edge onto an unrelated project type is emitted. The remaining ~60 are scattered project-internal methods (
state,schedule,next,header, …).Examples from ripgrep:
Why this shape shows up in Rust
I tried the equivalent shape in Python, Java and Go and all three resolved correctly: in those languages a type's methods live in one contiguous class body, so the enclosing type's own method is naturally the nearest candidate and proximity picks it. Rust allows a type's methods to be spread over several
implblocks and files, so another type's same-named method can sit between the call and its real target.Suggested direction
When the receiver is
self, restrict candidates to the target type of the enclosingimpl— already derived bygetReceiverType(). If no same-named method exists on that type, leave the ref inunresolved_refsrather than emitting a confident wrong edge. That would also cover theself.project()/self.as_mut()cases above, where the correct target is not in the graph to begin with.