mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
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:
1 parent
ab8d63bf4c
commit
f41ec2ec26
2 files changed
+58
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user