Skip to content

Rust: std-typed receivers hijack same-named project methods, fabricating CALLS edges (37.6% of edges stdlib-shaped on 0.10.8) — minimal repro #2053

Description

@Nicolas0315

Version

0.10.8 (also reproduces on 0.9.0)

Platform

macOS (arm64), Darwin 25.6.0

Summary

On Rust, a method call whose receiver is a std type loses the receiver at extraction and exact-matches a same-named project method, fabricating a CALLS edge. PathBuf::join, Option::from, Vec::len etc. are attributed to project symbols that the calling file never mentions.

This is the Rust instance of the class already filed for Go (#1906, #1909, #1927) and cross-language (#1572). I did not find a Rust-specific issue open for it — #1694 (cfg-duplicated names) is closed and a different mechanism.

Minimal reproduction

Cargo.toml + a single src/lib.rs:

use std::path::{Path, PathBuf};

/// A project method named `join`. Only `real_caller` below calls it.
pub struct EvidenceTier(pub u8);
impl EvidenceTier {
    pub fn join(&self, other: &EvidenceTier) -> EvidenceTier {
        EvidenceTier(self.0.max(other.0))
    }
}

/// The ONLY legitimate caller of EvidenceTier::join.
pub fn real_caller() -> EvidenceTier {
    EvidenceTier(1).join(&EvidenceTier(2))
}

/// Calls std PathBuf::join. Must NOT produce an edge to EvidenceTier::join.
pub fn stdlib_receiver(root: &Path) -> PathBuf {
    root.join("subdir")
}

/// Call-expression receiver. Must NOT produce an edge to EvidenceTier::join.
pub fn call_expr_receiver(root: &Path) -> PathBuf {
    root.to_path_buf().join("nested")
}
codebase-memory-mcp cli index_repository --repo_path <repro>
codebase-memory-mcp cli query_graph --project <p> \
  --query "MATCH (a)-[:CALLS]->(b) RETURN a.name AS caller, b.name AS callee, b.qualified_name AS callee_qn"

Actual

  join               EvidenceTier  ...src.lib.EvidenceTier
  real_caller        join          ...src.lib.EvidenceTier.join     <- correct
  real_caller        EvidenceTier  ...src.lib.EvidenceTier
  stdlib_receiver    join          ...src.lib.EvidenceTier.join     <- FABRICATED
  call_expr_receiver join          ...src.lib.EvidenceTier.join     <- FABRICATED

Expected

EvidenceTier::join has exactly one caller (real_caller). stdlib_receiver and call_expr_receiver call std::path::Path::join / PathBuf::join, which is not a project symbol and should produce no CALLS edge to a project node.

Scale on a real repo

A 376k-line Rust workspace (860 .rs files, indexed as 53,989 nodes):

0.9.0 0.10.8
total CALLS edges 26,635 34,746
edges targeting stdlib-shaped names1 5,728 (21.5%) 13,065 (37.6%)

1 target name in new, len, join, iter, from, get, contains, push, take, ok, all, min, max, clone, into, next, insert, unwrap, to_string, … (70 names). Not every one of these is fabricated — some project methods legitimately carry those names — but spot-checks came back 100% fabricated:

  • scripts/reasoning-helper/src/path_safety.rs::accepts_child_path → EvidenceAxisTier::join.
    Real code is root.join("3.8.0"). grep -c EvidenceAxisTier in that file = 0.
  • plugins/grok-bench-v2/src/checkpoint.rs::absolutize → EvidenceAxisTier::join.
    Real code is fn absolutize(root: &Path, path: PathBuf) { root.join(path) }. Same file, EvidenceAxisTier count = 0.

Impact beyond the edges

get_architecture hotspots are ranked by this fabricated fan-in, so the "most important functions" list is wrong, not just noisy — on the repo above the top entries were join(426), matches(343), iter(257), len(234), i.e. std methods. That matches what #1572 reports for Louvain clusters.

Because the answer is confident, well-formed, and wrong, an agent that trusts trace_path for "who calls X" has no signal that it should fall back to grep. Suggestion: when the receiver type cannot be resolved, emit no edge (or a distinctly-typed low-confidence edge that trace_path filters by default) rather than name-matching into the project namespace — and exclude unresolved-receiver edges from hotspot/cluster ranking.

Activity

  1. added
    parsing/qualityGraph extraction bugs, false positives, missing edges
    ux/behaviorDisplay bugs, docs, adoption UX
    on Sep 4, 2026
  2. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    ux/behaviorDisplay bugs, docs, adoption UX
    on Sep 5, 2026
  3. DeusData commented on Sep 25, 2026

    @DeusData
    Owner

    Thank you so much, @Nicolas0315, for such a careful report. The minimal repro, the scale numbers and the spot checks against real files made this straightforward to pin down.

    You were right about the mechanism. The Rust type resolver did work out that root is a std::path::Path and correctly resolved root.join(..) to Path::join, but that symbol isn't a node in your graph, and the pipeline treated "resolved, but not in the graph" the same as "couldn't resolve", so it fell back to plain name matching and bound the call to your EvidenceTier::join. #2339 makes the pipeline trust that answer: when the Rust resolver places a call on a std or known-crate symbol, the name-matching fallback no longer runs. The std model now also knows that to_path_buf() / join() return PathBuf and that PathBuf derefs to Path, so root.to_path_buf().join(..) is covered too. On ripgrep this removes 357 fabricated CALLS edges and adds none; every type-resolved edge is kept, and receivers whose type can't be inferred still use the fallback. Thanks again!

  4. Nicolas0315 commented on Sep 26, 2026

    @Nicolas0315
    Author

    Thank you for the quick turnaround and the clear write-up of the mechanism — "resolved but not in the graph" being treated as "unresolved" explains everything I was seeing.

    I built #2339 (793e0b61) and its merge base (5958f546) side by side on macOS arm64 and re-ran the repro with separate CBM_CACHE_DIRs.

    Original repro — fixed. EvidenceTier::join now has exactly one caller:

    caller → EvidenceTier::join base #2339
    real_caller edge edge ✅
    stdlib_receiver (root.join(..)) fabricated none ✅
    call_expr_receiver (root.to_path_buf().join(..)) fabricated none ✅

    Total edges went 24 → 22; the only two removed are the fabricated ones.

    A few more receiver shapes. Legitimate project calls are all kept, and several more std receivers are fixed. Two typed receivers still fall through to name matching:

    receiver base #2339
    let t = EvidenceTier(1); t.join(..) edge edge ✅ (legit)
    t: &EvidenceTier → t.join(t) edge edge ✅ (legit)
    p: PathBuf → p.join(..) fabricated none ✅
    let p = PathBuf::from(..); p.join(..) fabricated none ✅
    v: &[&str] → v.join("/") fabricated none ✅
    struct field self.root.join(..) with root: PathBuf fabricated fabricated
    v: Vec<String> → v.join(",") fabricated fabricated
    untyped closure param |p| p.join(..) fabricated fabricated (expected per your note)

    Minimal repro for the two remaining cases (same EvidenceTier as in the original report):

    use std::path::PathBuf;
    
    pub struct EvidenceTier(pub u8);
    impl EvidenceTier {
        pub fn join(&self, other: &EvidenceTier) -> EvidenceTier { EvidenceTier(self.0.max(other.0)) }
    }
    
    pub struct Cfg { pub root: PathBuf }
    impl Cfg {
        /// Field receiver: type is declared on the struct. Must NOT hit EvidenceTier::join.
        pub fn field_receiver(&self) -> PathBuf { self.root.join("x") }
    }
    
    /// Vec<String> derefs to [String]; join is [T]::join. Must NOT hit EvidenceTier::join.
    pub fn vec_join(v: Vec<String>) -> String { v.join(",") }
    MATCH (a)-[:CALLS]->(b) WHERE b.name = "join" RETURN a.name, b.qualified_name
    

    On #2339 this still returns field_receiver and vec_join → ...EvidenceTier.join. My guess is that field types on self.<field> aren't propagated into receiver resolution, and the std model is missing the Vec<T> → [T] deref for join, but I haven't dug into the resolver to confirm.

    The self.field.join(..) shape is very common in the real workspace from the report (config/context structs holding a root: PathBuf), so I'd expect a noticeable share of the remaining join fan-in to come from it. Happy to open a separate issue for these two if you'd rather keep #2053 scoped to the original repro.

    Thanks again!

  5. added a commit that references this issue on Sep 30, 2026
  6. DeusData commented on Sep 30, 2026

    @DeusData
    Owner

    Thank you again for reporting this, @Nicolas0315! The fix is now on main in 78795ad (merged via #2339), and it will ship in the next release.

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

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions