Skip to content

Rust: method call on std/external or struct-literal receiver links to a same-named project method #2614

Closed

Description

@Egrn

Environment

codegraph 1.6.2 from npm, Linux, rustc 1.99.0, fresh codegraph init. The repro crate passes cargo check.

Problem

In Rust, a method call whose receiver is not a project type still gets linked to a project method that just has the same name. I see four shapes of it:

  1. names.contains(&k) where names: &[&str] (slice) goes to Obj::contains.
  2. vis.map.get("a") where map: HashMap<String, i32> (std container in a struct field) goes to Ext::get, and Ext::get is even an associated fn without self.
  3. J::from(i) where use serde_json::Value as J goes to Fail::from, which is just a local impl From<io::Error> for Fail.
  4. Xyz { x: 1.0 }.to_val() (method on a struct literal) goes to Stat::to_val instead of Xyz::to_val. Both implement the same trait.

In our crate (12 files) this gave 7 false call edges like these. I think #1585 was about exactly case 2 ("receiver's type is not a local one at all (a std container ...)"), it is closed but still happens in 1.6.2.

Minimal repro

Cargo.toml:

[package]
name = "repro"
version = "0.1.0"
edition = "2021"

[dependencies]
serde_json = "1"

src/main.rs:

use std::collections::HashMap;
use serde_json::Value as J;

pub enum Val {
    Null,
    Int(i64),
    List(Vec<Val>),
}

pub struct Obj {
    keys: Vec<String>,
}

impl Obj {
    pub fn contains(&self, k: &str) -> bool {
        self.keys.iter().any(|x| x == k)
    }
}

pub struct Ext;

impl Ext {
    pub fn get(name: &str) -> Ext {
        let _ = name;
        Ext
    }
}

pub struct Fail;

impl From<std::io::Error> for Fail {
    fn from(_: std::io::Error) -> Fail {
        Fail
    }
}

pub struct Xyz {
    x: f64,
}

pub struct Stat {
    n: f64,
}

pub trait ToVal {
    fn to_val(&self) -> Val;
}

impl ToVal for Xyz {
    fn to_val(&self) -> Val {
        Val::Int(self.x as i64)
    }
}

impl ToVal for Stat {
    fn to_val(&self) -> Val {
        Val::Int(self.n as i64)
    }
}

// 1. slice::contains, not Obj::contains
fn slice_has(names: &[&str], k: &str) -> bool {
    names.contains(&k)
}

// 2. HashMap::get on a struct field, not Ext::get
pub struct Vis {
    map: HashMap<String, i32>,
}

fn map_get(vis: &Vis) -> Option<&i32> {
    vis.map.get("a")
}

// 3. serde_json::Value::from via alias J, not Fail::from
fn to_json(i: i64) -> J {
    J::from(i)
}

// 4. method on struct literal: Xyz::to_val, not Stat::to_val
fn centroid() -> Val {
    Xyz { x: 1.0 }.to_val()
}

fn main() {
    let o = Obj { keys: vec![] };
    let _ = o.contains("a");
    let _ = Ext::get("x");
    let _ = Stat { n: 0.0 }.to_val();
    let _ = slice_has(&["a"], "a");
    let _ = map_get(&Vis { map: HashMap::new() });
    let _ = to_json(1);
    let _ = centroid();
    let _ = Val::List(vec![Val::Null]);
}

Actual

Call edges in .codegraph/codegraph.db (enum variant edges skipped):

slice_has -> Obj::contains   resolvedBy instance-method, confidence 0.7, refName names.contains   (wrong)
map_get   -> Ext::get        resolvedBy exact-match,     confidence 0.9, refName get              (wrong)
to_json   -> Fail::from      resolvedBy instance-method, confidence 0.7, refName J::from          (wrong)
centroid  -> Stat::to_val    resolvedBy exact-match,     confidence 0.4, refName to_val           (wrong, should be Xyz::to_val)
main      -> Obj::contains   resolvedBy instance-method, confidence 0.7, refName o.contains       (ok)
main      -> Ext::get        resolvedBy qualified-name,  confidence 0.95, refName Ext::get        (ok)

And codegraph callers contains lists slice_has as a caller of Obj::contains.

Expected

  • slice_has, map_get, to_json: no edge to a project symbol. The receiver type is a slice, a HashMap and serde_json::Value, all outside the project. Unresolved is fine.
  • centroid -> Xyz::to_val. The type is written right in the struct literal.

Notes

Case 2 loses the receiver like #1707 (TS chained receiver) and then goes to exact-match by bare name. Case 4 is the same kind of thing as #1861 (pick by name instead of by the known type).

Maybe some small checks before the name fallback would help:

receiver is a struct literal `T { .. }`          -> type T, look up T::method only
receiver is a param/field with type from std/ext -> no project edge
path head is a `use ... as X` alias              -> resolve through the alias, external -> no project edge

Activity

  1. colbymchenry commented on Oct 23, 2026

    @colbymchenry
    Owner

    Thanks for the detailed report and the ready-made repro crate!

    Case 1 (names.contains on a slice) was already fixed on main by the typed-receiver work. #2831, now merged, fixes the other three:

    • vis.map.get("a") on a HashMap field no longer links to a project method.
    • J::from(i) through use serde_json::Value as J no longer links to a project method either.
    • Xyz { x: 1.0 }.to_val() now links to Xyz::to_val.

    On real projects such as tokio, ripgrep and rust-analyzer it removed over a thousand wrong links and retargeted several hundred to the right method. It ships in the next release; please re-index Rust projects after upgrading.

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