mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-25 16:22:30 +00:00
Applied from PR #4591 (authorship preserved); stale cache-schema bump dropped for one combined bump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
270 lines
9.7 KiB
Python
270 lines
9.7 KiB
Python
"""C# overloads get a node each, and calls pick theirs by argument count (#4584).
|
|
|
|
Overloads share a name, so they minted one id: ``add_node`` kept the first
|
|
declaration, the others vanished, every caller of any overload pointed at the
|
|
first one, and a later overload's own calls were credited to it. Now the first
|
|
declared overload keeps the plain id (so a type without overloads, and the
|
|
first overload, keep the ids they had) and every other signature gets a salted
|
|
one. A call binds to the overload its argument count fits; when that is not
|
|
unique (``M(int)`` / ``M(string)``) it binds to the first, as before.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from graphify.extract import extract
|
|
|
|
_MESSAGES = """namespace Demo
|
|
{
|
|
public static class Messages
|
|
{
|
|
public static string Banner(int code)
|
|
{
|
|
return Format(code);
|
|
}
|
|
|
|
public static string Banner(int code, string format)
|
|
{
|
|
return Pad(format);
|
|
}
|
|
|
|
private static string Format(int code) { return code.ToString(); }
|
|
private static string Pad(string text) { return text; }
|
|
}
|
|
}
|
|
"""
|
|
|
|
_CALLER = """namespace Demo
|
|
{
|
|
public class Caller
|
|
{
|
|
public string OneArg() { return Messages.Banner(1); }
|
|
public string TwoArgs() { return Messages.Banner(1, "x"); }
|
|
}
|
|
}
|
|
"""
|
|
|
|
|
|
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)
|
|
return extract([Path(n) for n in files], cache_root=Path(tempfile.mkdtemp()))
|
|
finally:
|
|
os.chdir(old)
|
|
|
|
|
|
def _by_line(r, label):
|
|
return {
|
|
n["source_location"]: n["id"]
|
|
for n in r["nodes"] if n["label"] == label and n.get("source_file")
|
|
}
|
|
|
|
|
|
def _edges(r, relation="calls"):
|
|
return {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == relation}
|
|
|
|
|
|
def _id(r, label):
|
|
ids = [n["id"] for n in r["nodes"] if n["label"] == label and n.get("source_file")]
|
|
assert len(ids) == 1, (label, ids)
|
|
return ids[0]
|
|
|
|
|
|
def test_issue_repro_each_overload_keeps_its_node_callers_and_callees(tmp_path):
|
|
r = _extract(tmp_path, {"Messages.cs": _MESSAGES, "Caller.cs": _CALLER})
|
|
banner = _by_line(r, ".Banner()")
|
|
assert set(banner) == {"L5", "L10"}
|
|
one, two = banner["L5"], banner["L10"]
|
|
# The first overload keeps the id the single node had.
|
|
assert one == "messages_demo_messages_banner"
|
|
calls = _edges(r)
|
|
assert (_id(r, ".OneArg()"), one) in calls
|
|
assert (_id(r, ".TwoArgs()"), two) in calls
|
|
assert (_id(r, ".OneArg()"), two) not in calls
|
|
assert (_id(r, ".TwoArgs()"), one) not in calls
|
|
assert (one, _id(r, ".Format()")) in calls
|
|
assert (two, _id(r, ".Pad()")) in calls
|
|
assert (one, _id(r, ".Pad()")) not in calls
|
|
|
|
|
|
def test_method_without_overloads_is_unchanged(tmp_path):
|
|
r = _extract(tmp_path, {"Messages.cs": _MESSAGES})
|
|
fmt = next(n for n in r["nodes"] if n["label"] == ".Format()")
|
|
assert fmt["id"] == "messages_demo_messages_format"
|
|
|
|
|
|
def test_in_file_and_instance_calls_pick_by_argument_count(tmp_path):
|
|
r = _extract(tmp_path, {
|
|
"Svc.cs": (
|
|
"public class Svc {\n"
|
|
" public void Run() { }\n"
|
|
" public void Run(int a) { Run(); }\n"
|
|
" public void Twice() { Run(1); }\n"
|
|
"}\n"
|
|
),
|
|
"Use.cs": (
|
|
"public class Use {\n"
|
|
" public void Go(Svc s) { s.Run(); }\n"
|
|
" public void Go1(Svc s) { s.Run(2); }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
run = _by_line(r, ".Run()")
|
|
calls = _edges(r)
|
|
assert (run["L3"], run["L2"]) in calls
|
|
assert (_id(r, ".Twice()"), run["L3"]) in calls
|
|
assert (_id(r, ".Twice()"), run["L2"]) not in calls
|
|
assert (_id(r, ".Go()"), run["L2"]) in calls
|
|
assert (_id(r, ".Go1()"), run["L3"]) in calls
|
|
|
|
|
|
def test_same_count_overloads_bind_to_the_first(tmp_path):
|
|
r = _extract(tmp_path, {"S.cs": (
|
|
"public static class S {\n"
|
|
" public static void Put(int x) { }\n"
|
|
" public static void Put(string x) { }\n"
|
|
" public static void Call() { Put(1); }\n"
|
|
"}\n"
|
|
)})
|
|
put = _by_line(r, ".Put()")
|
|
assert len(put) == 2
|
|
calls = _edges(r)
|
|
assert (_id(r, ".Call()"), put["L2"]) in calls
|
|
assert (_id(r, ".Call()"), put["L3"]) not in calls
|
|
|
|
|
|
def test_optional_and_params_arguments_count(tmp_path):
|
|
r = _extract(tmp_path, {"S.cs": (
|
|
"public static class S {\n"
|
|
" public static void Opt(string s) { }\n"
|
|
" public static void Opt(int a, int b, int c = 1) { }\n"
|
|
" public static void Many(int a) { }\n"
|
|
" public static void Many(int a, int b, params int[] rest) { }\n"
|
|
" public static void Call() { Opt(1, 2); Many(1, 2, 3, 4); }\n"
|
|
"}\n"
|
|
)})
|
|
calls = _edges(r)
|
|
caller = _id(r, ".Call()")
|
|
assert (caller, _by_line(r, ".Opt()")["L3"]) in calls
|
|
assert (caller, _by_line(r, ".Many()")["L5"]) in calls
|
|
|
|
|
|
def test_same_file_partial_halves_keep_their_overloads(tmp_path):
|
|
r = _extract(tmp_path, {"P.cs": (
|
|
"public partial class P { public void M(int a) { } }\n"
|
|
"public partial class P { public void M(int a, int b) { M(a); } }\n"
|
|
)})
|
|
m = _by_line(r, ".M()")
|
|
assert set(m) == {"L1", "L2"}
|
|
assert (m["L2"], m["L1"]) in _edges(r)
|
|
|
|
|
|
def test_identical_signature_behind_preprocessor_stays_one_node(tmp_path):
|
|
r = _extract(tmp_path, {"S.cs": (
|
|
"public class S {\n"
|
|
"#if A\n"
|
|
" public void M(int a) { }\n"
|
|
"#else\n"
|
|
" public void M(int a) { }\n"
|
|
"#endif\n"
|
|
"}\n"
|
|
)})
|
|
assert len(_by_line(r, ".M()")) == 1
|
|
|
|
|
|
def test_interface_overloads_dispatch_to_their_own_implementation(tmp_path):
|
|
r = _extract(tmp_path, {"S.cs": (
|
|
"public interface IR { void M(int a); void M(int a, string b); }\n"
|
|
"public class A : IR {\n"
|
|
" public void M(int a) { }\n"
|
|
" public void M(int a, string b) { }\n"
|
|
"}\n"
|
|
)})
|
|
iface = _by_line(r, ".M()")
|
|
dispatch = _edges(r, "dispatches_to")
|
|
ir1 = next(n["id"] for n in r["nodes"] if n["id"].startswith("s_ir_m") and n["source_location"] == "L1"
|
|
and n["metadata"].get("arity") == 1)
|
|
ir2 = next(n["id"] for n in r["nodes"] if n["id"].startswith("s_ir_m")
|
|
and n["metadata"].get("arity") == 2)
|
|
assert dispatch == {(ir1, iface["L3"]), (ir2, iface["L4"])}
|
|
|
|
|
|
def test_overload_ids_ignore_whitespace_and_line_endings(tmp_path):
|
|
src = (
|
|
"public class S {\n"
|
|
" public void M(int a) { }\n"
|
|
" public void M(System.Collections.Generic.Dictionary<string, int> d) { }\n"
|
|
"}\n"
|
|
)
|
|
a = _extract(tmp_path / "a", {"S.cs": src})
|
|
b = _extract(tmp_path / "b", {"S.cs": src.replace(", int>", ",\n int>").replace("\n", "\r\n")})
|
|
assert sorted(_by_line(a, ".M()").values()) == sorted(_by_line(b, ".M()").values())
|
|
|
|
|
|
def test_interface_overload_implemented_on_a_base_does_not_dispatch_to_a_sibling(tmp_path):
|
|
# Impl declares only M(int); M(string) comes from Base. IR.M(string) must
|
|
# not be pinned on Impl.M(int) just because both take one argument.
|
|
r = _extract(tmp_path, {"D.cs": (
|
|
"public interface IR { void M(int a); void M(string a); }\n"
|
|
"public class Base { public void M(string a) { } }\n"
|
|
"public class Impl : Base, IR { public void M(int a) { } }\n"
|
|
)})
|
|
by_sig = {
|
|
n["metadata"].get("signature"): n["id"]
|
|
for n in r["nodes"] if n["id"].startswith("d_ir_m")
|
|
}
|
|
impl = next(n["id"] for n in r["nodes"] if n["id"] == "d_impl_m")
|
|
assert _edges(r, "dispatches_to") == {(by_sig["int"], impl)}
|
|
|
|
|
|
def test_collapse_keeps_distinct_names_on_one_type_apart(tmp_path):
|
|
# Review: overloads are folded per owner type, but only among candidates
|
|
# of one name, so `Put` / `_Put` / `put` stay separate targets.
|
|
r = _extract(tmp_path, {
|
|
"A.cs": (
|
|
"public partial class S {\n"
|
|
" public void Put(int a) { }\n"
|
|
" public void _Put(int a, int b) { }\n"
|
|
" public void put(int a, int b, int c) { }\n"
|
|
" public void Put(string a, int b) { }\n"
|
|
"}\n"
|
|
),
|
|
"B.cs": (
|
|
"public partial class S {\n"
|
|
" public void Go() { Put(1); _Put(1, 2); put(1, 2, 3); Put(\"x\", 1); }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
go = _id(r, ".Go()")
|
|
calls = {t for s, t in _edges(r) if s == go}
|
|
put = _by_line(r, ".Put()")
|
|
assert calls == {put["L2"], put["L5"], _id(r, "._Put()"), _id(r, ".put()")}
|
|
|
|
|
|
def test_params_parameter_is_variadic_and_optional(tmp_path):
|
|
r = _extract(tmp_path, {"P.cs": (
|
|
"public static class P {\n"
|
|
" public static void Log(string m) { }\n"
|
|
" public static void Log(string m, params object[] args) { }\n"
|
|
"}\n"
|
|
)})
|
|
meta = {n["source_location"]: n["metadata"] for n in r["nodes"] if n["label"] == ".Log()"}
|
|
assert meta["L3"]["arity"] == 2 and meta["L3"]["min_arity"] == 1 and meta["L3"]["variadic"]
|
|
assert "variadic" not in meta["L2"]
|
|
|
|
|
|
def test_overloads_in_struct_record_and_beside_other_members(tmp_path):
|
|
r = _extract(tmp_path, {"L.cs": (
|
|
"public struct St { public void M(int a) { } public int M2 { get; } public void M(int a, int b) { } }\n"
|
|
"public record R { public void N() { } class Inner { public void N(int x) { } } public void N(int x) { } }\n"
|
|
)})
|
|
assert len([n for n in r["nodes"] if n["id"].startswith("l_st_m") and n["label"] == ".M()"]) == 2
|
|
assert {n["id"] for n in r["nodes"] if n["label"] == ".N()"} == {"l_r_n", "l_r_n_46f8ab", "l_inner_n"}
|