fix Rust cross-crate spurious INFERRED edges: skip scoped_identifier and trait-method blocklist from raw_calls (#908)

This commit is contained in:
Safi
2026-05-17 13:12:49 +01:00
parent 96e17adf4d
commit f7160c81c5
6 changed files with 71 additions and 1 deletions
+17 -1
View File
@@ -3610,6 +3610,17 @@ def extract_go(path: Path) -> dict:
# ── Rust extractor (custom walk) ──────────────────────────────────────────────
# Common Rust trait/stdlib method names that appear in virtually every codebase.
# Resolving these cross-file produces spurious INFERRED edges across crate
# boundaries (issue #908) — skip them from the unresolved-call queue entirely.
_RUST_TRAIT_METHOD_BLOCKLIST: frozenset[str] = frozenset({
"new", "default", "parse", "from_str", "now", "clone", "into", "from",
"to_string", "to_owned", "len", "is_empty", "iter", "next", "build",
"start", "run", "init", "app", "get", "set", "push", "pop", "insert",
"remove", "contains", "collect", "map", "filter", "unwrap", "expect",
"ok", "err", "some", "none", "send", "recv", "lock", "read", "write",
})
def extract_rust(path: Path) -> dict:
"""Extract functions, structs, enums, traits, impl methods, and use declarations from a .rs file."""
try:
@@ -3740,6 +3751,7 @@ def extract_rust(path: Path) -> dict:
func_node = node.child_by_field_name("function")
callee_name: str | None = None
is_member_call: bool = False
is_scoped_call: bool = False
if func_node:
if func_node.type == "identifier":
callee_name = _read_text(func_node, source)
@@ -3749,6 +3761,10 @@ def extract_rust(path: Path) -> dict:
if field:
callee_name = _read_text(field, source)
elif func_node.type == "scoped_identifier":
# Type::method() — still allow in-file EXTRACTED match, but
# skip cross-file resolution: bare last-segment lookup ignores
# crate boundaries and produces spurious INFERRED edges (#908).
is_scoped_call = True
name = func_node.child_by_field_name("name")
if name:
callee_name = _read_text(name, source)
@@ -3769,7 +3785,7 @@ def extract_rust(path: Path) -> dict:
"source_location": f"L{line}",
"weight": 1.0,
})
else:
elif not is_scoped_call and callee_name.lower() not in _RUST_TRAIT_METHOD_BLOCKLIST:
raw_calls.append({
"caller_nid": caller_nid,
"callee": callee_name,
+4
View File
@@ -0,0 +1,4 @@
[package]
name = "crate_a"
version = "0.1.0"
edition = "2021"
+7
View File
@@ -0,0 +1,7 @@
pub fn start() -> bool {
true
}
pub fn parse(s: &str) -> u32 {
s.len() as u32
}
+4
View File
@@ -0,0 +1,4 @@
[package]
name = "crate_b"
version = "0.1.0"
edition = "2021"
+18
View File
@@ -0,0 +1,18 @@
// crate_b has no dependency on crate_a.
// These calls use Type::method() (scoped_identifier) and common names that
// previously produced spurious INFERRED edges into crate_a (#908).
pub struct Server;
impl Server {
pub fn run(&self) {
// Server::start() — scoped call, should not wire to crate_a::start
let _ = Server::start();
// Url::parse() — scoped call, should not wire to crate_a::parse
let _ = Url::parse("http://example.com");
}
fn start() -> bool {
false
}
}
+21
View File
@@ -177,6 +177,27 @@ def test_rust_no_dangling_edges():
assert e["source"] in node_ids
def test_rust_no_cross_crate_spurious_edges():
"""Scoped calls (Type::method) and blocklisted names must not produce
INFERRED cross-crate calls edges (#908)."""
from graphify.extract import extract
crate_a = FIXTURES / "crate_a" / "src" / "lib.rs"
crate_b = FIXTURES / "crate_b" / "src" / "lib.rs"
r = extract([crate_a, crate_b])
node_ids_a = {n["id"] for n in r["nodes"] if "crate_a" in (n.get("source_file") or "")}
node_ids_b = {n["id"] for n in r["nodes"] if "crate_b" in (n.get("source_file") or "")}
# No calls edge should cross from crate_b into crate_a
cross_crate_calls = [
e for e in r["edges"]
if e["relation"] == "calls"
and e["source"] in node_ids_b
and e["target"] in node_ids_a
]
assert cross_crate_calls == [], (
f"Spurious cross-crate edges: {cross_crate_calls}"
)
# ── extract() dispatch ────────────────────────────────────────────────────────
def test_extract_dispatches_all_languages():