mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
_dispatch_confidence was computed from the raw implementers set before
the per-implementer language gate ran, so a sourceless/external-reference
implementer, or a cross-language name collision, that never produces an
edge could still dilute the confidence score for the implementer(s) that
do. A single real implementer alongside one gate-failing entry scored
0.75 (the two-implementer tier) instead of 0.85.
Gate first, then count only the implementers that pass it, so the score
reflects how many implementers actually share the dispatch edge.
Addresses review feedback on PR #4396 (Graphify review bot, finding at
interface_dispatch.py:154).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit b01a6fcf10)
320 lines
14 KiB
Python
320 lines
14 KiB
Python
"""C# member-level interface dispatch (#3003).
|
|
|
|
`_report.Build()` on an injected `IReport` resolves to `IReport.Build()`, which
|
|
is what the call site names. The implementing `Report.Build()` is a separate
|
|
node with nothing joining the two, so a directed walk stops at the interface
|
|
and every chain through an injected dependency is cut there.
|
|
|
|
`resolve_csharp_interface_dispatch` links the interface's method to every
|
|
implementing method, as long as that implementer owns exactly one method of
|
|
the same name. An implementer's own ambiguity leaves only that implementer's
|
|
edge out, the same single-owner guard the Pascal and Ruby resolvers use.
|
|
Confidence falls as the implementer count grows (0.85 / 0.75 / 0.65).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from graphify.extract import extract
|
|
|
|
|
|
def _extract(tmp_path, files: dict[str, str]):
|
|
for name, body in files.items():
|
|
p = tmp_path / name
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(body)
|
|
old = os.getcwd()
|
|
try:
|
|
os.chdir(tmp_path)
|
|
r = extract([Path(n) for n in files], cache_root=Path(tempfile.mkdtemp()))
|
|
finally:
|
|
os.chdir(old)
|
|
dispatch = {(e["source"], e["target"]) for e in r["edges"]
|
|
if e["relation"] == "dispatches_to"}
|
|
return dispatch, r
|
|
|
|
|
|
def _find(r, label, id_contains):
|
|
return next(n["id"] for n in r["nodes"]
|
|
if n["label"] == label and id_contains in n["id"])
|
|
|
|
|
|
def _reachable(r, start: str) -> set[str]:
|
|
adjacency: dict[str, list[str]] = {}
|
|
for e in r["edges"]:
|
|
adjacency.setdefault(e["source"], []).append(e["target"])
|
|
seen = {start}
|
|
queue = [start]
|
|
while queue:
|
|
for nxt in adjacency.get(queue.pop(0), []):
|
|
if nxt not in seen:
|
|
seen.add(nxt)
|
|
queue.append(nxt)
|
|
return seen
|
|
|
|
|
|
_INJECTED = {
|
|
"IReport.cs": "public interface IReport { void Build(); }\n",
|
|
"Report.cs": (
|
|
"public class Report : IReport {\n"
|
|
" public void Build() { Format(); }\n"
|
|
" public void Format() { }\n"
|
|
"}\n"
|
|
),
|
|
"Runner.cs": (
|
|
"public class Runner {\n"
|
|
" private readonly IReport _report;\n"
|
|
" public Runner(IReport report) { _report = report; }\n"
|
|
" public void Go() { _report.Build(); }\n"
|
|
"}\n"
|
|
),
|
|
}
|
|
|
|
|
|
def test_single_implementer_links_the_interface_method(tmp_path):
|
|
dispatch, r = _extract(tmp_path, _INJECTED)
|
|
assert (_find(r, ".Build()", "ireport"), _find(r, ".Build()", "report_report")) in dispatch
|
|
for edge in r["edges"]:
|
|
if edge["relation"] == "dispatches_to":
|
|
assert edge["confidence"] == "INFERRED"
|
|
assert edge["confidence_score"] == 0.85
|
|
|
|
|
|
def test_chain_through_an_injected_dependency_becomes_reachable(tmp_path):
|
|
dispatch, r = _extract(tmp_path, _INJECTED)
|
|
assert dispatch
|
|
# Go -> IReport.Build -> Report.Build -> Format
|
|
assert _find(r, ".Format()", "report") in _reachable(r, _find(r, ".Go()", "runner"))
|
|
|
|
|
|
def test_the_call_to_the_interface_method_is_kept(tmp_path):
|
|
# Additive: the call site really does name the interface.
|
|
_, r = _extract(tmp_path, _INJECTED)
|
|
calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
|
|
assert (_find(r, ".Go()", "runner"), _find(r, ".Build()", "ireport")) in calls
|
|
|
|
|
|
def test_two_implementers_both_get_an_edge_at_lower_confidence(tmp_path):
|
|
dispatch, r = _extract(tmp_path, {"S.cs": (
|
|
"public interface IR { void M(); }\n"
|
|
"public class A : IR { public void M() { } }\n"
|
|
"public class B : IR { public void M() { } }\n"
|
|
)})
|
|
iface_m = _find(r, ".M()", "ir")
|
|
assert dispatch == {
|
|
(iface_m, _find(r, ".M()", "s_a")),
|
|
(iface_m, _find(r, ".M()", "s_b")),
|
|
}
|
|
for edge in r["edges"]:
|
|
if edge["relation"] == "dispatches_to":
|
|
assert edge["confidence"] == "INFERRED"
|
|
assert edge["confidence_score"] == 0.75
|
|
|
|
|
|
def test_three_implementers_floor_at_the_weak_inference_tier(tmp_path):
|
|
dispatch, r = _extract(tmp_path, {"S.cs": (
|
|
"public interface IR { void M(); }\n"
|
|
"public class A : IR { public void M() { } }\n"
|
|
"public class B : IR { public void M() { } }\n"
|
|
"public class C : IR { public void M() { } }\n"
|
|
)})
|
|
assert len(dispatch) == 3
|
|
for edge in r["edges"]:
|
|
if edge["relation"] == "dispatches_to":
|
|
assert edge["confidence_score"] == 0.65
|
|
|
|
|
|
def test_one_ambiguous_implementer_does_not_suppress_its_siblings(tmp_path):
|
|
# B owns two distinct nodes named `.M()` (synthetic: the real extractor
|
|
# collapses same-named overloads into one node, so this models a case
|
|
# where it would not, e.g. partial classes merged incorrectly). B's
|
|
# ambiguity must stay scoped to B and not suppress A's clean match.
|
|
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": "b", "label": "B", "source_file": "a.cs", "_callable_class": True},
|
|
{"id": "iface_m", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
{"id": "a_m", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
{"id": "b_m1", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
{"id": "b_m2", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
]
|
|
edges = [
|
|
{"source": "a", "target": "iface", "relation": "implements"},
|
|
{"source": "b", "target": "iface", "relation": "implements"},
|
|
{"source": "iface", "target": "iface_m", "relation": "method"},
|
|
{"source": "a", "target": "a_m", "relation": "method"},
|
|
{"source": "b", "target": "b_m1", "relation": "method"},
|
|
{"source": "b", "target": "b_m2", "relation": "method"},
|
|
]
|
|
resolve_csharp_interface_dispatch([], nodes, edges)
|
|
dispatch = {(e["source"], e["target"]) for e in edges if e["relation"] == "dispatches_to"}
|
|
assert dispatch == {("iface_m", "a_m")}
|
|
|
|
|
|
def test_no_matching_member_produces_no_edge(tmp_path):
|
|
dispatch, _ = _extract(tmp_path, {"S.cs": (
|
|
"public interface IR { void Build(); }\n"
|
|
"public class A : IR { public void Other() { } }\n"
|
|
)})
|
|
assert not dispatch
|
|
|
|
|
|
def test_overloads_collapsed_into_one_node_still_link(tmp_path):
|
|
# The extractor mints one node per method name, so two overloads share it.
|
|
# The guard counts distinct nodes, not `method` edge multiplicity, so this
|
|
# is one candidate rather than a tie. Same reasoning as the dedup comment in
|
|
# resolve_pascal_inherited_calls.
|
|
dispatch, r = _extract(tmp_path, {"S.cs": (
|
|
"public interface IR { void M(); }\n"
|
|
"public class A : IR { public void M() { } public void M(int x) { } }\n"
|
|
)})
|
|
assert (_find(r, ".M()", "ir"), _find(r, ".M()", "s_a")) in dispatch
|
|
|
|
|
|
def test_abstract_base_class_is_out_of_scope(tmp_path):
|
|
# `class Impl : Base` is an `inherits` edge, and a base method may well be
|
|
# the real target, so walking inheritance is a different bet from dispatch.
|
|
dispatch, _ = _extract(tmp_path, {"S.cs": (
|
|
"public abstract class Base { public abstract void M(); }\n"
|
|
"public class Impl : Base { public override void M() { } }\n"
|
|
)})
|
|
assert not dispatch
|
|
|
|
|
|
def test_interface_hierarchy_is_not_walked_transitively(tmp_path):
|
|
# Known limit: IBase's only implementer is IChild, which declares nothing,
|
|
# so IBase.M never reaches Impl.M. Walking the chain is follow-up work.
|
|
dispatch, _ = _extract(tmp_path, {"S.cs": (
|
|
"public interface IBase { void M(); }\n"
|
|
"public interface IChild : IBase { }\n"
|
|
"public class Impl : IChild { public void M() { } }\n"
|
|
)})
|
|
assert not dispatch
|
|
|
|
|
|
def test_java_interface_and_implementation_are_untouched(tmp_path):
|
|
# The resolver is registered for .cs; a Java corpus must not gain the edge.
|
|
dispatch, _ = _extract(tmp_path, {
|
|
"IReport.java": "public interface IReport { void build(); }\n",
|
|
"Report.java": "public class Report implements IReport { public void build() { } }\n",
|
|
})
|
|
assert not dispatch
|
|
|
|
def test_case_only_member_difference_is_not_a_match(tmp_path):
|
|
# C# is case sensitive: an implementation must spell the member exactly, so
|
|
# `build` does not implement `Build` and must not be linked to it.
|
|
dispatch, _ = _extract(tmp_path, {"S.cs": (
|
|
"public interface IR { void Build(); }\n"
|
|
"public class A : IR { public void build() { } }\n"
|
|
)})
|
|
assert not dispatch
|
|
|
|
|
|
def test_an_unrelated_edge_between_the_members_does_not_suppress_the_link(tmp_path):
|
|
# The dedup is scoped to dispatches_to. Another relation between the two
|
|
# member nodes says nothing about whether the dispatch link is present.
|
|
from graphify.csharp_dispatch import resolve_csharp_interface_dispatch
|
|
|
|
nodes = [
|
|
{"id": "iface", "label": "IR", "source_file": "a.cs", "_callable_class": True},
|
|
{"id": "impl", "label": "A", "source_file": "a.cs", "_callable_class": True},
|
|
{"id": "iface_m", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
{"id": "impl_m", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
]
|
|
edges = [
|
|
{"source": "impl", "target": "iface", "relation": "implements"},
|
|
{"source": "iface", "target": "iface_m", "relation": "method"},
|
|
{"source": "impl", "target": "impl_m", "relation": "method"},
|
|
{"source": "iface_m", "target": "impl_m", "relation": "references"},
|
|
]
|
|
resolve_csharp_interface_dispatch([], nodes, edges)
|
|
assert ("iface_m", "impl_m", "dispatches_to") in {
|
|
(e["source"], e["target"], e["relation"]) for e in edges
|
|
}
|
|
|
|
|
|
def test_an_existing_dispatch_edge_is_not_duplicated(tmp_path):
|
|
from graphify.csharp_dispatch import resolve_csharp_interface_dispatch
|
|
|
|
nodes = [
|
|
{"id": "iface", "label": "IR", "source_file": "a.cs", "_callable_class": True},
|
|
{"id": "impl", "label": "A", "source_file": "a.cs", "_callable_class": True},
|
|
{"id": "iface_m", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
{"id": "impl_m", "label": ".M()", "source_file": "a.cs", "_callable": True},
|
|
]
|
|
edges = [
|
|
{"source": "impl", "target": "iface", "relation": "implements"},
|
|
{"source": "iface", "target": "iface_m", "relation": "method"},
|
|
{"source": "impl", "target": "impl_m", "relation": "method"},
|
|
{"source": "iface_m", "target": "impl_m", "relation": "dispatches_to"},
|
|
]
|
|
resolve_csharp_interface_dispatch([], nodes, edges)
|
|
assert sum(1 for e in edges if e["relation"] == "dispatches_to") == 1
|
|
|
|
def test_a_non_csharp_implementer_is_not_dispatched_to(tmp_path):
|
|
# `implements` resolves by name, so in a mixed corpus a Java class declaring
|
|
# `implements IReport` binds to the C# IReport when that is the only node
|
|
# with the name. Linking a C# member to a Java method would be a wrong edge.
|
|
dispatch, _ = _extract(tmp_path, {
|
|
"IReport.cs": "public interface IReport { void Build(); }\n",
|
|
"Report.java": "public class Report implements IReport { public void Build() { } }\n",
|
|
})
|
|
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",
|
|
"Report.cs": "public class Report : IReport { public void Build() { } }\n",
|
|
"IJob.java": "public interface IJob { void run(); }\n",
|
|
"Job.java": "public class Job implements IJob { public void run() { } }\n",
|
|
})
|
|
by_id = {n["id"]: n for n in r["nodes"]}
|
|
linked = {(by_id[s].get("source_file"), by_id[t].get("source_file")) for s, t in dispatch}
|
|
assert linked == {("IReport.cs", "Report.cs")}
|
|
|
|
def test_member_name_is_read_up_to_the_first_parenthesis(tmp_path):
|
|
# The pair match is keyed on this string. C# labels are `.Name()` today, so
|
|
# this pins the reduction itself rather than a shape the extractor emits.
|
|
from graphify.csharp_dispatch import _method_label
|
|
|
|
assert _method_label({"label": ".Build()"}) == "Build"
|
|
assert _method_label({"label": "Build"}) == "Build"
|
|
assert _method_label({"label": ".Build(int, string)"}) == "Build"
|
|
assert _method_label({"label": ".build()"}) == "build"
|