mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
fix(csharp): resolve calls through casts and interface-typed properties (#3797)
((IStore)_obj).Save() and (_obj as IStore).Save() produced no calls edge: the receiver capture didn't handle a parenthesized cast, so the call never reached the receiver-typed resolver. Use the cast's type as the receiver so it resolves like IStore.Save(). IStore Store => ...; Store.Save() bound to the concrete Store.Save() because a capitalised receiver was looked up as a type name first. A field, property or local of that name shadows the type, so try the recorded receiver_type first and fall back to the type name. Co-Authored-By: Grok Bot <noreply@x.ai>
This commit is contained in:
1 parent
5c7b84792f
commit
2d26dad2ab
3 files changed
+67
-6
No files matched your search
+7
-6
@@ -4679,13 +4679,14 @@ def _resolve_csharp_member_calls(
|
||||
type_nid = bases[0]
|
||||
type_qualified = True
|
||||
elif receiver[:1].isupper():
|
||||
# Type.M() — the type is named explicitly (also covers a Pascal-cased
|
||||
# local whose name equals its type, resolved via the table below if the
|
||||
# explicit-type lookup misses).
|
||||
type_nid = _resolve_type_name_nid(receiver, caller_node, src_file)
|
||||
# Type.M() — the type is named explicitly. A field/property/local of
|
||||
# that name in scope shadows the type (`IStore Store => ...;
|
||||
# Store.Save()`, #3797), so the receiver's declared type from the
|
||||
# table wins when known; otherwise fall back to the type name.
|
||||
type_name = rc.get("receiver_type")
|
||||
type_nid = _resolve_type_name_nid(type_name, caller_node, src_file)
|
||||
if not type_nid:
|
||||
type_name = rc.get("receiver_type")
|
||||
type_nid = _resolve_type_name_nid(type_name, caller_node, src_file)
|
||||
type_nid = _resolve_type_name_nid(receiver, caller_node, src_file)
|
||||
if not type_nid:
|
||||
_park_if_absent(type_name or receiver, caller_node, rc)
|
||||
continue
|
||||
|
||||
@@ -7101,6 +7101,19 @@ def _extract_generic(
|
||||
and fname.type == "identifier"
|
||||
):
|
||||
member_receiver = _read_text(fname, source)
|
||||
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
|
||||
# IStore.M() (#3797).
|
||||
inner = recv.named_children[0] if recv.named_children else None
|
||||
cast_type = None
|
||||
if inner is not None and inner.type == "cast_expression":
|
||||
cast_type = inner.child_by_field_name("type")
|
||||
elif inner is not None and inner.type == "as_expression":
|
||||
cast_type = inner.child_by_field_name("right")
|
||||
type_info = _read_csharp_type_name(cast_type, source)
|
||||
if type_info and type_info[0]:
|
||||
member_receiver = type_info[0]
|
||||
elif fn_node is not None and fn_node.type == "identifier":
|
||||
callee_name = _read_text(fn_node, source)
|
||||
elif fn_node is not None and fn_node.type == "generic_name":
|
||||
|
||||
@@ -353,6 +353,53 @@ def test_null_conditional_cross_file_receiver_resolves(tmp_path):
|
||||
assert not any("commit" in s and "cache_save" in t for s, t in calls)
|
||||
|
||||
|
||||
_STORE = (
|
||||
"public interface IStore { void Save(); }\n"
|
||||
"public class Store : IStore { public void Save() { } }\n"
|
||||
)
|
||||
|
||||
|
||||
def _store_call(tmp_path, editor_body: str):
|
||||
"""Call `IStore.Save()` from another file; return (calls, caller, IStore.Save, Store.Save)."""
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Store.cs": _STORE,
|
||||
"Editor.cs": (
|
||||
"public class Editor {\n"
|
||||
" object _obj;\n"
|
||||
" IStore Store => (IStore)_obj;\n"
|
||||
f" public void Run() {{ {editor_body} }}\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
run = _find(r, ".Run()", "editor")
|
||||
istore_save = _find(r, ".Save()", "istore")
|
||||
store_save = next(n["id"] for n in r["nodes"]
|
||||
if n["label"] == ".Save()" and n["id"] != istore_save)
|
||||
return calls, run, istore_save, store_save
|
||||
|
||||
|
||||
def test_cast_receiver_resolves_to_cast_type(tmp_path):
|
||||
"""`((IStore)_obj).Save()`: the cast names the receiver type (#3797)."""
|
||||
calls, run, istore_save, store_save = _store_call(tmp_path, "((IStore)_obj).Save();")
|
||||
assert (run, istore_save) in calls
|
||||
assert (run, store_save) not in calls
|
||||
|
||||
|
||||
def test_as_cast_receiver_resolves_to_cast_type(tmp_path):
|
||||
"""`(_obj as IStore).Save()` types the receiver the same way (#3797)."""
|
||||
calls, run, istore_save, store_save = _store_call(tmp_path, "(_obj as IStore).Save();")
|
||||
assert (run, istore_save) in calls
|
||||
assert (run, store_save) not in calls
|
||||
|
||||
|
||||
def test_property_receiver_shadows_same_named_type(tmp_path):
|
||||
"""`IStore Store => ...; Store.Save()`: inside Editor `Store` is the
|
||||
property, so the call is IStore.Save, not the class Store's (#3797)."""
|
||||
calls, run, istore_save, store_save = _store_call(tmp_path, "Store.Save();")
|
||||
assert (run, istore_save) in calls
|
||||
assert (run, store_save) not in calls
|
||||
|
||||
|
||||
def test_base_receiver_resolves_to_base_class_method(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Base.cs": "public class BaseSvc { public bool Ping() => true; }\n",
|
||||
|
||||
Reference in new issue
Block a user