Skip to content

Rust: self.method() resolves to a same-named method on an unrelated type #1861

Description

@Dshuishui

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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