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:
nixvideojuego-ui authored and safishamsi committed 2026-10-13 22:21:48 +01:00
1 parent f41ec2ec26
commit 50436e7860
3 files changed
+136 -7

No files matched your search

+36 -7
View File
@@ -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
+10
View File
@@ -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
+90
View File
@@ -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)