mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
fix(csharp): new X() never binds to a method that shares X's name
A C# object creation was resolved against every definition named X, methods
included. Inside a helper like `static Bounds Bounds(..) => new Bounds(..)`
(Unity's Bounds, outside the corpus) it became an EXTRACTED self-loop; across
files the bare-name pass bound every `new List<T>()` in the corpus to an
unrelated `List()` method, minting an INFERRED hub with one false caller per
construction site.
The same-file path now defers a construction whose bare-name target is a
method, and the cross-file pass drops method candidates for `csharp_new`
calls, next to the enum-member/property exclusion (#3815, #4245). A real
type with the same name still wins.
Dropping those candidates removes ties that used to hide a separate guess: a
qualified construction (`new QuickSlotsBar.Slot()`) with several bare-name
`Slot` classes went to the path-proximity tie-break, which ignores the
qualifier and picks the Slot next to the caller. A qualified construction now
takes the one candidate nested in a type named by the qualifier's last
segment, or gets no edge; namespace qualifiers stay with
`_resolve_csharp_qualified_calls`.
Fixes #4419.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 68202f9766)
This commit is contained in:
1 parent
f41ec2ec26
commit
50436e7860
3 files changed
+136
-7
No files matched your search
+36
-7
@@ -10174,6 +10174,7 @@ def extract(
|
||||
)
|
||||
_csharp_stub_resolver: CsharpNameResolver | None = None # built on first use
|
||||
_csharp_method_owner: dict[str, str] | None = None # method nid -> type nid, built on first use
|
||||
_csharp_outer_type: dict[str, str] | None = None # nested type nid -> outer type label, built on first use
|
||||
for rc in all_raw_calls:
|
||||
if rc.get("_ambiguous_python_import"):
|
||||
continue
|
||||
@@ -10230,6 +10231,16 @@ def extract(
|
||||
# #4245) bound to the case instead of getting no edge/deferring to
|
||||
# raw_calls resolution that could reject it on its own grounds.
|
||||
candidates = [c for c in candidates if c not in _member_nids]
|
||||
if rc.get("csharp_new"):
|
||||
# Nor does `new X()` construct a method that shares X's name:
|
||||
# without this every `new List<T>()` in the corpus binds to a
|
||||
# `List()` method as an INFERRED hub.
|
||||
if _csharp_method_owner is None:
|
||||
_csharp_method_owner = {
|
||||
e["target"]: e["source"] for e in all_edges
|
||||
if e.get("relation") == "method"
|
||||
}
|
||||
candidates = [c for c in candidates if c not in _csharp_method_owner]
|
||||
if not candidates:
|
||||
continue
|
||||
# A call rescued from an annotation stub (#3888): the caller's own file
|
||||
@@ -10377,13 +10388,31 @@ def extract(
|
||||
# tie-breakers (non-test preference, then path proximity).
|
||||
# Resolve only if exactly one candidate survives; otherwise
|
||||
# the #543/#1219 guard still holds and we skip.
|
||||
tgt = disambiguate_ambiguous_candidates(
|
||||
candidates,
|
||||
{c: nid_to_source_file.get(c, "") for c in candidates},
|
||||
rc.get("source_file", ""),
|
||||
)
|
||||
if tgt is None:
|
||||
continue
|
||||
# A qualified C# construction (`new Outer.Slot()`) names
|
||||
# which Slot it means, and a proximity guess over the bare
|
||||
# name ignores that: it takes the one Slot nested in a type
|
||||
# called Outer, or no edge. A namespace qualifier matches no
|
||||
# outer type and is left to _resolve_csharp_qualified_calls.
|
||||
if rc.get("csharp_new") and rc.get("qualified_prefix"):
|
||||
if _csharp_outer_type is None:
|
||||
labels = {n["id"]: str(n.get("label", "")) for n in all_nodes}
|
||||
_csharp_outer_type = {
|
||||
e["target"]: labels.get(e["source"], "")
|
||||
for e in all_edges if e.get("relation") == "contains"
|
||||
}
|
||||
outer = str(rc["qualified_prefix"]).rsplit(".", 1)[-1]
|
||||
nested = [c for c in candidates if _csharp_outer_type.get(c) == outer]
|
||||
if len(nested) != 1:
|
||||
continue
|
||||
tgt = nested[0]
|
||||
else:
|
||||
tgt = disambiguate_ambiguous_candidates(
|
||||
candidates,
|
||||
{c: nid_to_source_file.get(c, "") for c in candidates},
|
||||
rc.get("source_file", ""),
|
||||
)
|
||||
if tgt is None:
|
||||
continue
|
||||
has_import_evidence = False
|
||||
# A source-less candidate is an unresolved external stub, not a
|
||||
# project-defined callable. It can remain a target of type/reference
|
||||
|
||||
@@ -8758,6 +8758,16 @@ def _extract_generic(
|
||||
and not nid_to_sf.get(tgt_nid)
|
||||
):
|
||||
tgt_nid = None
|
||||
# `new X()` constructs a type, never a method that shares X's
|
||||
# name (`static Bounds Bounds(..) => new Bounds(..)` on a type
|
||||
# outside the corpus). Defer so the cross-file pass can look
|
||||
# for the type itself.
|
||||
if (
|
||||
config.ts_module == "tree_sitter_c_sharp"
|
||||
and node.type == "object_creation_expression"
|
||||
and tgt_nid in method_owner
|
||||
):
|
||||
tgt_nid = None
|
||||
# A name-only reference can leave a source-less stub behind when
|
||||
# the target is outside the scanned corpus (for example,
|
||||
# ``HTTPException`` imported from FastAPI). It is useful for the
|
||||
|
||||
@@ -281,3 +281,93 @@ def test_qualified_construction_of_a_foreign_type_makes_no_stub_edge(tmp_path):
|
||||
)})
|
||||
builders = {n["id"] for n in r["nodes"] if n["label"] == "StringBuilder"}
|
||||
assert not any(target in builders for _, target in calls)
|
||||
|
||||
|
||||
# `new X()` constructs a type. A method that merely shares X's name is never its
|
||||
# target, yet the call was resolved against every same-named definition: inside
|
||||
# the method it became a self-loop (`static Bounds Bounds(..) => new Bounds(..)`
|
||||
# on Unity's Bounds), and across files the bare-name pass bound every
|
||||
# `new List<T>()` in the corpus to a `List()` method as an INFERRED hub.
|
||||
|
||||
|
||||
def test_construction_never_binds_to_a_same_named_method_cross_file(tmp_path):
|
||||
calls, r = _extract(tmp_path, {
|
||||
"Market.cs": (
|
||||
"namespace Demo { public class Market {"
|
||||
" public static int List(int a) { return a; } } }\n"
|
||||
),
|
||||
"Use.cs": (
|
||||
"namespace Demo { public class Use {"
|
||||
" public void Run() { var xs = new List<int>(); } } }\n"
|
||||
),
|
||||
})
|
||||
run = _find(r, ".Run()", "run")
|
||||
assert (run, _find(r, ".List()", "market")) not in calls
|
||||
|
||||
|
||||
def test_construction_inside_a_same_named_method_is_no_self_loop(tmp_path):
|
||||
calls, r = _extract(tmp_path, {"Box.cs": (
|
||||
"namespace Demo { public class Box {"
|
||||
" static Bounds Bounds(int a) { return new Bounds(a); } } }\n"
|
||||
)})
|
||||
bounds = _find(r, ".Bounds()", "box")
|
||||
assert (bounds, bounds) not in calls
|
||||
|
||||
|
||||
def test_construction_still_binds_to_the_type_beside_a_same_named_method(tmp_path):
|
||||
calls, r = _extract(tmp_path, {
|
||||
"Kit.cs": "namespace Demo { public class Piece { } }\n",
|
||||
"Shop.cs": (
|
||||
"namespace Demo { public class Shop {"
|
||||
" public static int Piece() { return 0; } } }\n"
|
||||
),
|
||||
"Setup.cs": (
|
||||
"namespace Demo { public class Setup {"
|
||||
" public void Link() { var p = new Piece(); } } }\n"
|
||||
),
|
||||
})
|
||||
link = _find(r, ".Link()", "link")
|
||||
assert (link, _find(r, "Piece", "kit")) in calls
|
||||
assert (link, _find(r, ".Piece()", "shop")) not in calls
|
||||
|
||||
|
||||
def test_qualified_nested_construction_follows_its_outer_type(tmp_path):
|
||||
# `new QuickSlotsBar.Slot()` names the outer type. Two `Slot` classes make
|
||||
# the bare name ambiguous, and the path-proximity tie-break picked the one
|
||||
# next to the caller — the wrong Slot. With method candidates no longer
|
||||
# muddying the tie, that guess would fire more often; a qualified
|
||||
# construction now takes the Slot nested in QuickSlotsBar, or no edge.
|
||||
calls, r = _extract(tmp_path, {
|
||||
"UI/Bar.cs": (
|
||||
"namespace Game.UI { public class QuickSlotsBar {"
|
||||
" public class Slot { public int kind; } } }\n"
|
||||
),
|
||||
"Editor/Towers.cs": (
|
||||
"namespace Game.Tools { public class Towers {"
|
||||
" class Slot { public string texture; } } }\n"
|
||||
),
|
||||
"Editor/Hud.cs": (
|
||||
"using Game.UI;\nnamespace Game.Tools { public class Hud {"
|
||||
" public void Ensure() { var s = new QuickSlotsBar.Slot { kind = 1 }; } } }\n"
|
||||
),
|
||||
})
|
||||
ensure = _find(r, ".Ensure()", "ensure")
|
||||
def slot_in(stem):
|
||||
return next(n["id"] for n in r["nodes"]
|
||||
if n["label"] == "Slot" and stem in str(n.get("source_file", "")))
|
||||
assert (ensure, slot_in("Towers")) not in calls
|
||||
assert (ensure, slot_in("Bar")) in calls
|
||||
|
||||
|
||||
def test_qualified_construction_with_an_unknown_outer_type_gives_no_edge(tmp_path):
|
||||
calls, r = _extract(tmp_path, {
|
||||
"UI/Bar.cs": "namespace Game.UI { public class Bar { public class Slot { } } }\n",
|
||||
"Editor/Towers.cs": "namespace Game.Tools { public class Towers { class Slot { } } }\n",
|
||||
"Editor/Hud.cs": (
|
||||
"namespace Game.Tools { public class Hud {"
|
||||
" public void Ensure() { var s = new Missing.Slot(); } } }\n"
|
||||
),
|
||||
})
|
||||
ensure = _find(r, ".Ensure()", "ensure")
|
||||
slots = {n["id"] for n in r["nodes"] if n["label"] == "Slot"}
|
||||
assert not any(t in slots for s, t in calls if s == ensure)
|
||||
Reference in new issue
Block a user