Keep every method a duplicate index key maps to, not just the last one

A plain dict assignment silently overwrote an earlier target whenever
two method nodes shared both a source impl node and a stripped
label, which would have kept whichever one was seen last instead of
surfacing the case as the ambiguity it is. The index now keeps a set
per key and folds every match into the candidate set before the
exactly one candidate check runs, so a genuine duplicate still bails
instead of guessing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 2cbbf1a670)
This commit is contained in:
ayushcodes10 authored and safishamsi committed 2026-09-16 14:43:12 +01:00
1 parent 4f13b17aa2
commit 58c497d2da
2 files changed
+46 -8

No files matched your search

+10 -8
View File
@@ -4758,8 +4758,12 @@ def _resolve_rust_self_member_calls(
if str(n.get("source_file") or "").endswith(".rs"):
nids_by_label.setdefault(n.get("label", ""), []).append(n.get("id"))
# (impl/type node id, bare method name) -> method node id, from `method` edges.
method_index: dict[tuple[str, str], str] = {}
# (impl/type node id, bare method name) -> method node id(s), from `method`
# edges. A set, not a single overwritten value: two distinct method nodes
# sharing both a source and a stripped label (however unlikely) must still
# surface as an ambiguity below rather than silently keeping whichever one
# was seen last.
method_index: dict[tuple[str, str], set[str]] = {}
for e in all_edges:
if e.get("relation") != "method":
continue
@@ -4767,7 +4771,7 @@ def _resolve_rust_self_member_calls(
tnode = node_by_id.get(tgt)
if tnode is not None:
name = str(tnode.get("label", "")).strip("()").lstrip(".")
method_index[(src, name)] = tgt
method_index.setdefault((src, name), set()).add(tgt)
# Scoped to `calls`: a caller that already has a DIFFERENT relation to the
# same target (e.g. a `references` edge from also naming the type in a
@@ -4781,11 +4785,9 @@ def _resolve_rust_self_member_calls(
for rc in raw:
caller = rc["caller_nid"]
callee = rc["callee"]
candidates = {
method_index[(nid, callee)]
for nid in nids_by_label.get(rc["rust_self_type"], [])
if (nid, callee) in method_index
}
candidates: set[str] = set()
for nid in nids_by_label.get(rc["rust_self_type"], []):
candidates |= method_index.get((nid, callee), set())
if len(candidates) != 1: # zero or ambiguous -> no edge (god-node guard)
continue
tgt = next(iter(candidates))
+36
View File
@@ -168,3 +168,39 @@ def test_resolver_is_not_suppressed_by_an_unrelated_edge_to_the_same_pair(tmp_pa
if e["relation"] == "calls"
}
assert ("impl_foo_caller", "impl_foo_method") in calls
def test_resolver_treats_a_duplicate_method_index_key_as_ambiguous(tmp_path: Path):
"""Two DIFFERENT method nodes sharing both a source impl node and a
stripped label must surface as an ambiguity (no edge), not silently
keep whichever one a plain dict overwrite happened to see last."""
from graphify.extract import _resolve_rust_self_member_calls
all_nodes = [
{"id": "impl_foo", "label": "Foo", "source_file": "a.rs"},
{"id": "impl_foo_method_a", "label": ".method()", "source_file": "a.rs"},
{"id": "impl_foo_method_b", "label": ".method()", "source_file": "a.rs"},
{"id": "impl_foo_caller", "label": ".caller()", "source_file": "a.rs"},
]
all_edges = [
{"source": "impl_foo", "target": "impl_foo_method_a", "relation": "method"},
{"source": "impl_foo", "target": "impl_foo_method_b", "relation": "method"},
]
per_file = [{
"raw_calls": [{
"caller_nid": "impl_foo_caller",
"callee": "method",
"rust_self_type": "Foo",
"source_file": "a.rs",
"source_location": "L1",
}],
}]
_resolve_rust_self_member_calls(per_file, all_nodes, all_edges)
calls = {
(e["source"], e["target"])
for e in all_edges
if e["relation"] == "calls"
}
assert not calls, "two same-labeled method nodes on one type must not guess"