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>
(cherry picked from commit 2d26dad2ab)
This commit is contained in:
Mahaboob pasha authored and safishamsi committed 2026-10-06 20:54:15 +01:00
1 parent f50b79ffc5
commit ac467b0c00
3 files changed
+67 -6

No files matched your search

+7 -6
View File
@@ -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
+13
View File
@@ -7120,6 +7120,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":
+47
View File
@@ -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",