Repository navigation
Description
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 isroot.join("3.8.0").grep -c EvidenceAxisTierin that file = 0.plugins/grok-bench-v2/src/checkpoint.rs::absolutize→EvidenceAxisTier::join.
Real code isfn absolutize(root: &Path, path: PathBuf) { root.join(path) }. Same file,EvidenceAxisTiercount = 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
DeusData commentedon Sep 25, 2026
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
rootis astd::path::Pathand correctly resolvedroot.join(..)toPath::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 yourEvidenceTier::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 thatto_path_buf()/join()returnPathBufand thatPathBufderefs toPath, soroot.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!Nicolas0315 commentedon Sep 26, 2026
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 separateCBM_CACHE_DIRs.Original repro — fixed.
EvidenceTier::joinnow has exactly one caller:EvidenceTier::joinreal_callerstdlib_receiver(root.join(..))call_expr_receiver(root.to_path_buf().join(..))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:
let t = EvidenceTier(1); t.join(..)t: &EvidenceTier→t.join(t)p: PathBuf→p.join(..)let p = PathBuf::from(..); p.join(..)v: &[&str]→v.join("/")self.root.join(..)withroot: PathBufv: Vec<String>→v.join(",")|p| p.join(..)Minimal repro for the two remaining cases (same
EvidenceTieras in the original report):On #2339 this still returns
field_receiverandvec_join→...EvidenceTier.join. My guess is that field types onself.<field>aren't propagated into receiver resolution, and the std model is missing theVec<T>→[T]deref forjoin, 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 aroot: PathBuf), so I'd expect a noticeable share of the remainingjoinfan-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!
Merge pull request #2339 from DeusData/fix/issue-2053
fix(rust): never let a std-typed receiver bind a same-named project m…
DeusData commentedon Sep 30, 2026
Thank you again for reporting this, @Nicolas0315! The fix is now on
mainin 78795ad (merged via #2339), and it will ship in the next release.feat(java): no name guess for calls on a proven-external receiver
feat(java): no name guess for calls on a proven-external receiver