fix(csharp): never bind a nameless member-call receiver to a same-named method in the file

A C# member call whose receiver has no capturable name — a member chain
(`run.Sequence.Apply()`, `s.Items.Count()`), an invocation (`Make().Apply()`)
or a `new X(...)` — reached the per-file bare-name map, because the C# defer
rule required `member_receiver`. It then bound to the caller's own same-named
method as an EXTRACTED edge: a false self-loop (`a.look.LateUpdate()` inside
`LateUpdate()`), LINQ `Count()` bound to a local `Count()`, or a typed chain
landing on the wrong class instead of reaching `receiver_chain` resolution.

Every C# member call now defers. A typed chain still resolves through
`receiver_chain`; `new X(...).M()` (#1770) is typed from the construction so
it keeps its edge; anything untypable gets no edge (fail-closed).

Fixes #4418.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d5544b125f)
This commit is contained in:
nixvideojuego-ui authored and safishamsi committed 2026-10-13 22:21:48 +01:00
1 parent ab8d63bf4c
commit f41ec2ec26
2 files changed
+58 -4

No files matched your search

+16 -4
View File
@@ -8309,6 +8309,15 @@ def _extract_generic(
)
if isinstance(elem_type, str):
csharp_receiver_chain = [elem_type]
elif recv is not None and recv.type == "object_creation_expression":
# new X(...).M(): the receiver is the constructed X
# (#1770), typed here now that a nameless receiver no
# longer falls back to the file's bare-name map.
type_info = _read_csharp_type_name(
recv.child_by_field_name("type"), source
)
if type_info and type_info[0]:
csharp_receiver_chain = [type_info[0]]
elif recv is not None and recv.type == "parenthesized_expression":
# ((IStore)x).M() / (x as IStore).M(): the cast names
# the receiver's type in source, so resolve it as
@@ -8670,9 +8679,13 @@ def _extract_generic(
# receiver's declared type and mis-binds to an unrelated same-named
# method (#1609). The receiver may be lowercase (`_server.Save()`),
# so this is broader than the capitalized/this-field Python rule.
# A receiver with no capturable name (`run.Sequence.Apply()`,
# `Make().Apply()`, `s.Items.Count()`) defers too: the bare-name
# map would bind it to the caller's own same-named method. A typed
# chain still resolves through `receiver_chain`; anything else
# gets no edge.
_csharp_defer = (
config.ts_module == "tree_sitter_c_sharp"
and is_member_call and member_receiver
config.ts_module == "tree_sitter_c_sharp" and is_member_call
)
_python_defer = (
config.ts_module == "tree_sitter_python"
@@ -8691,13 +8704,12 @@ def _extract_generic(
and is_member_call
and not lua_self_qualified
)
if _python_defer or _java_defer or (_builtin_member_call and not php_builtin_self_call) or _lua_member_defer or is_scoped_call or (
if _python_defer or _java_defer or _csharp_defer or (_builtin_member_call and not php_builtin_self_call) or _lua_member_defer or is_scoped_call or (
is_member_call
and member_receiver
and (
member_receiver[:1].isupper()
or is_this_field_call
or _csharp_defer
)
):
tgt_nid = None
+42
View File
@@ -238,3 +238,45 @@ def test_element_of_nested_list_stays_unresolved(tmp_path):
),
})
assert not [c for c in calls if "grid" in c[0]]
# A member call whose receiver is not a simple identifier, `this` or `base`
# (`run.Sequence.Apply()`, `Make().Apply()`, `s.Items.Count()`) captured no
# receiver name. With no receiver to defer on, the call fell back to the
# file's bare-name map and bound to the caller's own same-named method — a
# false self-loop or a wrong sibling edge, written as EXTRACTED.
def test_member_chain_never_binds_to_the_callers_same_named_method(tmp_path):
calls, r = _calls(tmp_path, {
"Model.cs": _MODEL,
"Controller.cs": _controller("run.Sequence.Apply();", "public void Apply() { }"),
})
tick = _find(r, ".Tick()", "controller")
assert (tick, _find(r, ".Apply()", "controller")) not in calls
assert (tick, _find(r, ".Apply()", "sequence")) in calls
def test_chained_receiver_in_one_file_gives_no_self_loop(tmp_path):
calls, r = _calls(tmp_path, {"Crowd.cs": (
"namespace Demo\n{\n"
" public class Crowd\n {\n"
" public void LateUpdate() { Pick().look.LateUpdate(); }\n"
" object Pick() { return null; }\n"
" }\n}\n"
)})
late = _find(r, ".LateUpdate()", "crowd")
assert (late, late) not in calls
def test_untyped_chain_never_binds_to_a_same_file_method(tmp_path):
calls, r = _calls(tmp_path, {"Inventory.cs": (
"using System.Collections.Generic;\nnamespace Demo\n{\n"
" public class State { public List<int> Items; }\n"
" public class Inventory\n {\n"
" public int Count() { return 0; }\n"
" public int Owned(State s) { return s.Items.Count(); }\n"
" }\n}\n"
)})
owned = _find(r, ".Owned()", "inventory")
assert (owned, _find(r, ".Count()", "inventory")) not in calls