From 58c497d2da23a58edd865eb470083682bae25beb Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Wed, 16 Sep 2026 15:45:18 +0530 Subject: [PATCH] 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 (cherry picked from commit 2cbbf1a67082cb0bab3011ddac6b9e1694bc1790) --- graphify/extract.py | 18 +++++++------- tests/test_rust_self_member_calls.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index db8a87b48..02b4e4c15 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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)) diff --git a/tests/test_rust_self_member_calls.py b/tests/test_rust_self_member_calls.py index 6582baf39..b66c7f127 100644 --- a/tests/test_rust_self_member_calls.py +++ b/tests/test_rust_self_member_calls.py @@ -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"