diff --git a/graphify/interface_dispatch.py b/graphify/interface_dispatch.py index f77253745..172f5d28e 100644 --- a/graphify/interface_dispatch.py +++ b/graphify/interface_dispatch.py @@ -150,17 +150,22 @@ def resolve_interface_dispatch( interface_methods = methods_of.get(interface_nid, {}) if not interface_methods: continue - score = _dispatch_confidence(len(impls)) + # Gate first, then count: an `implements` edge from a sourceless stub + # (a dangling/external reference) or a cross-language name collision + # never produces an edge below, so it must not count toward "how many + # implementers is this split between" either — that would understate + # confidence for the implementers that actually get a dispatch edge. + gated_impls: list[tuple[str, dict]] = [] for impl_nid in impls: impl_node = node_by_id.get(impl_nid) - # Both ends must be declarations in this language's files. A - # sourceless stub minted for a dangling reference carries no - # members worth dispatching to, and a cross-language pair is a - # name collision rather than an implementation. - if impl_node is None or not declared_in(impl_node, suffixes): - continue + if impl_node is not None and declared_in(impl_node, suffixes): + gated_impls.append((impl_nid, impl_node)) + if not gated_impls: + continue + score = _dispatch_confidence(len(gated_impls)) + for impl_nid, impl_node in gated_impls: impl_methods = methods_of.get(impl_nid, {}) for name, declared in interface_methods.items(): if len(declared) != 1: diff --git a/tests/test_csharp_interface_dispatch.py b/tests/test_csharp_interface_dispatch.py index 4bae67b02..d5f42636f 100644 --- a/tests/test_csharp_interface_dispatch.py +++ b/tests/test_csharp_interface_dispatch.py @@ -266,6 +266,37 @@ def test_a_non_csharp_implementer_is_not_dispatched_to(tmp_path): assert not dispatch +def test_a_gate_failing_implementer_does_not_dilute_confidence(tmp_path): + # `implementers[iface]` holds every `implements` source regardless of + # language or whether the node even resolved (a dangling/external + # reference stub). Only `A` passes the gate and gets an edge; the stub + # must not count toward "how many implementers" for confidence purposes, + # or a single real implementer would score as if it had a sibling. + from graphify.csharp_dispatch import resolve_csharp_interface_dispatch + + nodes = [ + {"id": "iface", "label": "IR", "source_file": "a.cs", "_callable_class": True}, + {"id": "a", "label": "A", "source_file": "a.cs", "_callable_class": True}, + {"id": "stub", "label": "Stub", "_callable_class": True}, # sourceless: fails declared_in + {"id": "iface_m", "label": ".M()", "source_file": "a.cs", "_callable": True}, + {"id": "a_m", "label": ".M()", "source_file": "a.cs", "_callable": True}, + {"id": "stub_m", "label": ".M()", "_callable": True}, + ] + edges = [ + {"source": "a", "target": "iface", "relation": "implements"}, + {"source": "stub", "target": "iface", "relation": "implements"}, + {"source": "iface", "target": "iface_m", "relation": "method"}, + {"source": "a", "target": "a_m", "relation": "method"}, + {"source": "stub", "target": "stub_m", "relation": "method"}, + ] + resolve_csharp_interface_dispatch([], nodes, edges) + dispatch_edges = [e for e in edges if e["relation"] == "dispatches_to"] + assert len(dispatch_edges) == 1 + assert dispatch_edges[0]["source"] == "iface_m" + assert dispatch_edges[0]["target"] == "a_m" + assert dispatch_edges[0]["confidence_score"] == 0.85 + + def test_mixed_corpus_links_only_the_csharp_pair(tmp_path): dispatch, r = _extract(tmp_path, { "IReport.cs": "public interface IReport { void Build(); }\n",