mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-18 01:49:22 +00:00
fix(csharp): type a var from the called method's return type (#4266)
Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 816dcf5927)
This commit is contained in:
1 parent
d3f8687c00
commit
07f3d0fc3c
3 files changed
+292
-9
No files matched your search
@@ -5050,6 +5050,41 @@ def _resolve_csharp_member_calls(
|
||||
field_type, owner, (owner or {}).get("source_file", "")
|
||||
)
|
||||
|
||||
# Method return types per class, for `var v = R.M<A>()` (#4266).
|
||||
method_returns = _bind_member_field_tables(per_file, all_nodes, lang="csharp_returns")
|
||||
|
||||
def _call_return_type_nid(call: list, caller: str, caller_node: dict | None,
|
||||
src_file: str) -> str | None:
|
||||
"""The type a ``var`` gets from ``R.M<A>()``: M's declared return type.
|
||||
|
||||
M must resolve to exactly one declaration (after matching the number of
|
||||
explicit type arguments); a return type that is M's own type parameter
|
||||
takes the call-site type argument. Anything unknown yields None.
|
||||
"""
|
||||
recv, method, targs = call
|
||||
if recv == "this" or not recv:
|
||||
owner = enclosing_type.get(caller) or (caller if caller in type_nids else None)
|
||||
else:
|
||||
owner = _resolve_type_name_nid(recv, caller_node, src_file)
|
||||
method_nid = _method_on_type_or_bases(owner, _key(method)) if owner else None
|
||||
decl_type = enclosing_type.get(method_nid) if method_nid else None
|
||||
decls = method_returns.get(decl_type, {}).get(method) if decl_type else None
|
||||
if not isinstance(decls, list):
|
||||
return None
|
||||
if targs:
|
||||
decls = [d for d in decls if d[1] == len(targs)]
|
||||
if len(decls) != 1:
|
||||
return None # missing or an overload we can't choose
|
||||
ret = decls[0][0]
|
||||
if isinstance(ret, str) and ret.startswith("#"):
|
||||
idx = int(ret[1:])
|
||||
ret = targs[idx] if idx < len(targs) else None # inferred: no guess
|
||||
return _resolve_type_name_nid(ret, caller_node, src_file)
|
||||
decl_node = node_by_id.get(decl_type or "")
|
||||
return _resolve_type_name_nid(
|
||||
ret, decl_node, (decl_node or {}).get("source_file", "")
|
||||
)
|
||||
|
||||
all_raw_calls: list[dict] = []
|
||||
for result in per_file:
|
||||
all_raw_calls.extend(result.get("raw_calls", []))
|
||||
@@ -5103,6 +5138,13 @@ def _resolve_csharp_member_calls(
|
||||
_park_if_absent(type_name or receiver, caller_node, rc)
|
||||
continue
|
||||
type_qualified = True
|
||||
elif rc.get("receiver_call") and not rc.get("receiver_type"):
|
||||
type_nid = _call_return_type_nid(
|
||||
rc["receiver_call"], caller, caller_node, src_file
|
||||
)
|
||||
if not type_nid:
|
||||
continue
|
||||
type_qualified = False
|
||||
else:
|
||||
type_name = rc.get("receiver_type")
|
||||
if not type_name:
|
||||
|
||||
@@ -2327,11 +2327,67 @@ def _csharp_element_type_name(type_node, source: bytes) -> str | None:
|
||||
return name
|
||||
|
||||
|
||||
def _csharp_return_info(method_node, source: bytes) -> list:
|
||||
"""``[return type, type-parameter count]`` of a C# method declaration (#4266).
|
||||
|
||||
The return type is the class name a receiver of that type would carry
|
||||
(``List<T>`` -> ``List``), ``"#i"`` when it is the method's own i-th type
|
||||
parameter (bound by the call-site type arguments), or None when no class
|
||||
can be named (``void``, a primitive, a class-level type parameter).
|
||||
"""
|
||||
tparams: list[str] = []
|
||||
tpl = method_node.child_by_field_name("type_parameters")
|
||||
for param in tpl.named_children if tpl is not None else []:
|
||||
name_node = param.child_by_field_name("name")
|
||||
tparams.append(_read_text(name_node, source) if name_node is not None else "")
|
||||
rtype = method_node.child_by_field_name("returns")
|
||||
ret = _csharp_receiver_type_name(rtype, source)
|
||||
if ret is not None and rtype is not None and rtype.type == "identifier" and ret in tparams:
|
||||
ret = "#%d" % tparams.index(ret)
|
||||
elif ret in tparams or ret in _csharp_type_parameters_in_scope(method_node, source):
|
||||
ret = None
|
||||
return [ret, len(tparams)]
|
||||
|
||||
|
||||
def _csharp_deferred_call(invocation, source: bytes) -> list | None:
|
||||
"""``[receiver, method, type args]`` for ``var x = R.M<A>(...)`` (#4266).
|
||||
|
||||
The variable's type is the return type of the called method, which may be
|
||||
declared in another file, so the cross-file pass resolves it. ``receiver``
|
||||
is an identifier, ``"this"`` or ``""`` (unqualified call); each type
|
||||
argument is its class name or None. Any other call shape returns None.
|
||||
"""
|
||||
fn = invocation.child_by_field_name("function")
|
||||
recv = ""
|
||||
if fn is not None and fn.type == "member_access_expression":
|
||||
inner = fn.child_by_field_name("expression")
|
||||
fn = fn.child_by_field_name("name")
|
||||
if inner is not None and inner.type in ("this", "this_expression"):
|
||||
recv = "this"
|
||||
elif inner is not None and inner.type == "identifier":
|
||||
recv = _read_text(inner, source)
|
||||
else:
|
||||
return None
|
||||
if fn is None or fn.type not in ("identifier", "generic_name"):
|
||||
return None
|
||||
args: list[str | None] = []
|
||||
if fn.type == "generic_name":
|
||||
tal = next((c for c in fn.children if c.type == "type_argument_list"), None)
|
||||
for arg in tal.named_children if tal is not None else []:
|
||||
args.append(_csharp_receiver_type_name(arg, source))
|
||||
name_node = next((c for c in fn.children if c.type == "identifier"), None)
|
||||
else:
|
||||
name_node = fn
|
||||
if name_node is None:
|
||||
return None
|
||||
return ["call", recv, _read_text(name_node, source), args]
|
||||
|
||||
|
||||
def _csharp_method_receiver_types(
|
||||
method_node,
|
||||
source: bytes,
|
||||
field_types: dict[str, str],
|
||||
) -> tuple[dict[str, list[tuple[int, int, str | None]]], dict[str, str]]:
|
||||
) -> tuple[dict[str, list[tuple[int, int, str | list | None]]], dict[str, str]]:
|
||||
"""Build the SCOPED receiver bindings visible to one C# method (#2299, #2472).
|
||||
|
||||
The C# twin of ``_java_method_receiver_types``, but positional: instead of
|
||||
@@ -2357,11 +2413,12 @@ def _csharp_method_receiver_types(
|
||||
``out var x`` itself stays untyped — resolving it from the callee's
|
||||
``out`` parameter signature is a separate, pre-existing gap.
|
||||
"""
|
||||
bindings: dict[str, list[tuple[int, int, str | None]]] = {}
|
||||
bindings: dict[str, list[tuple[int, int, str | list | None]]] = {}
|
||||
field_poisoned: set[str] = set()
|
||||
|
||||
def bind(
|
||||
name: str | None, type_name: str | None, scope_node, elem_type: str | None = None
|
||||
name: str | None, type_name: str | list | None, scope_node,
|
||||
elem_type: str | None = None,
|
||||
) -> None:
|
||||
if not name or scope_node is None:
|
||||
return
|
||||
@@ -2461,6 +2518,11 @@ def _csharp_method_receiver_types(
|
||||
g.child_by_field_name("type"), source
|
||||
)
|
||||
break
|
||||
if g.type == "invocation_expression":
|
||||
# `var v = R.M<A>()` — typed cross-file from
|
||||
# M's declared return type (#4266).
|
||||
type_name = _csharp_deferred_call(g, source)
|
||||
break
|
||||
bind(_read_text(name_node, source), type_name, scope, declared_elem)
|
||||
elif node.type in ("declaration_expression", "declaration_pattern"):
|
||||
# #2346: inline-declared receivers. `out Sect s` is a
|
||||
@@ -2500,10 +2562,10 @@ def _csharp_method_receiver_types(
|
||||
|
||||
|
||||
def _csharp_scoped_receiver_type(
|
||||
table: tuple[dict[str, list[tuple[int, int, str | None]]], dict[str, str]] | None,
|
||||
table: tuple[dict[str, list[tuple[int, int, str | list | None]]], dict[str, str]] | None,
|
||||
name: str | None,
|
||||
call_byte: int,
|
||||
) -> str | None:
|
||||
) -> str | list | None:
|
||||
"""Resolve a C# receiver name to its type at a specific call offset (#2472).
|
||||
|
||||
``table`` is the (scoped bindings, field base) pair built by
|
||||
@@ -4783,6 +4845,9 @@ def _extract_generic(
|
||||
# same-named, explicitly typed receiver in a different method.
|
||||
csharp_field_types: dict[str, dict[str, str]] = {}
|
||||
csharp_method_scopes: dict[int, tuple[object, str]] = {}
|
||||
# class nid -> method name -> one [return type, type-param count] per
|
||||
# overload, for typing `var v = R.M<A>()` cross-file (#4266).
|
||||
csharp_method_returns: dict[str, dict[str, list]] = {}
|
||||
|
||||
csharp_interface_names: set[str] = set()
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
@@ -6418,6 +6483,10 @@ def _extract_generic(
|
||||
metadata=ruby_method_metadata,
|
||||
)
|
||||
add_edge(parent_class_nid, func_nid, "method", line)
|
||||
if config.ts_module == "tree_sitter_c_sharp" and t == "method_declaration":
|
||||
csharp_method_returns.setdefault(parent_class_nid, {}).setdefault(
|
||||
func_name, []
|
||||
).append(_csharp_return_info(node, source))
|
||||
else:
|
||||
func_nid = _make_id(stem, sanitized_name)
|
||||
if config.ts_module == "tree_sitter_python":
|
||||
@@ -7711,7 +7780,7 @@ def _extract_generic(
|
||||
csharp_table, _read_text(inner, source),
|
||||
node.start_byte,
|
||||
)
|
||||
if root_type:
|
||||
if isinstance(root_type, str):
|
||||
csharp_receiver_chain = [
|
||||
root_type, _read_text(fname, source)
|
||||
]
|
||||
@@ -7724,7 +7793,7 @@ def _extract_generic(
|
||||
csharp_table, _read_text(inner, source) + "[]",
|
||||
node.start_byte,
|
||||
)
|
||||
if elem_type:
|
||||
if isinstance(elem_type, str):
|
||||
csharp_receiver_chain = [elem_type]
|
||||
elif recv is not None and recv.type == "parenthesized_expression":
|
||||
# ((IStore)x).M() / (x as IStore).M(): the cast names
|
||||
@@ -8241,7 +8310,22 @@ def _extract_generic(
|
||||
receiver_type = _csharp_scoped_receiver_type(
|
||||
receiver_types, member_receiver, node.start_byte
|
||||
)
|
||||
if receiver_type:
|
||||
if isinstance(receiver_type, list):
|
||||
# `var v = R.M<A>(); v.X()` (#4266): R is typed
|
||||
# here, M's return type in the cross-file pass.
|
||||
_, recv, method, targs = receiver_type
|
||||
if recv and recv != "this":
|
||||
recv_t = _csharp_scoped_receiver_type(
|
||||
receiver_types if isinstance(receiver_types, tuple)
|
||||
else None, recv, node.start_byte,
|
||||
)
|
||||
if isinstance(recv_t, str):
|
||||
recv = recv_t
|
||||
elif recv_t is not None or not recv[:1].isupper():
|
||||
recv = None # untyped local: no guess
|
||||
if recv is not None:
|
||||
rc_entry["receiver_call"] = [recv, method, targs]
|
||||
elif receiver_type:
|
||||
rc_entry["receiver_type"] = receiver_type
|
||||
if config.ts_module == "tree_sitter_java":
|
||||
rc_entry["lang"] = "java"
|
||||
@@ -8692,7 +8776,11 @@ def _extract_generic(
|
||||
_field_table_export = [
|
||||
{"lang": _lang, "class_label": _n.get("label"),
|
||||
"source_file": _n.get("source_file"), "fields": dict(_tbl)}
|
||||
for _lang, _tables in (("java", java_field_types), ("csharp", csharp_field_types))
|
||||
for _lang, _tables in (
|
||||
("java", java_field_types),
|
||||
("csharp", csharp_field_types),
|
||||
("csharp_returns", csharp_method_returns),
|
||||
)
|
||||
for _cls, _tbl in _tables.items()
|
||||
if _tbl
|
||||
for _n in (next((x for x in nodes if x["id"] == _cls), None),)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
"""C# `var x = R.M<A>(...)` is typed from M's declared return type (#4266).
|
||||
|
||||
`var report = ServiceFactory.Get<IReport>(scope); report.Build()` had no calls
|
||||
edge because a `var` was only typed from `new T()`. The variable now takes the
|
||||
called method's return type, with call-site type arguments substituted, when
|
||||
the method resolves to exactly one declaration. Anything else stays untyped.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _calls(tmp_path, files: dict[str, str]):
|
||||
for name, body in files.items():
|
||||
(tmp_path / name).write_text(body)
|
||||
old = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
r = extract([Path(n) for n in files], cache_root=tmp_path / ".cache")
|
||||
finally:
|
||||
os.chdir(old)
|
||||
calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
|
||||
return calls, r
|
||||
|
||||
|
||||
def _find(r, label, id_contains):
|
||||
return next(n["id"] for n in r["nodes"]
|
||||
if n["label"] == label and id_contains in n["id"])
|
||||
|
||||
|
||||
_LIB = (
|
||||
"namespace Demo\n{\n"
|
||||
" public interface IReport { string Build(string name); }\n"
|
||||
" public class Report : IReport { public string Build(string name) => name; }\n"
|
||||
" public class Widget { public void Spin() { } }\n"
|
||||
" public class Item { }\n"
|
||||
" public class List<T> { public void Add(T item) { } }\n"
|
||||
" public class Box<T> { public T Value() => default; }\n"
|
||||
" public static class ServiceFactory\n {\n"
|
||||
" public static T Get<T>(object scope) where T : new() => new T();\n"
|
||||
" public static Widget Make() => new Widget();\n"
|
||||
" public static List<T> CreateList<T>() => new List<T>();\n"
|
||||
" public static Widget Pick(int a) => null;\n"
|
||||
" public static IReport Pick(string b) => null;\n"
|
||||
" public static T Infer<T>(T value) => value;\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
|
||||
def _consumer(body: str) -> str:
|
||||
return (
|
||||
"namespace Demo\n{\n"
|
||||
f" public class Consumer {{ public void Run(object scope) {{ {body} }} }}\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
|
||||
def _run(r):
|
||||
return _find(r, ".Run()", "consumer")
|
||||
|
||||
|
||||
def test_generic_factory_same_file(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"All.cs": _LIB + _consumer(
|
||||
"var report = ServiceFactory.Get<IReport>(scope); report.Build(\"daily\");"
|
||||
),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Build()", "ireport")) in calls
|
||||
assert (_run(r), _find(r, ".Build()", "_report_")) not in calls
|
||||
|
||||
|
||||
def test_generic_factory_cross_file(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer(
|
||||
"var report = ServiceFactory.Get<IReport>(scope); report.Build(\"daily\");"
|
||||
),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Build()", "ireport")) in calls
|
||||
|
||||
|
||||
def test_non_generic_return_type(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer("var w = ServiceFactory.Make(); w.Spin();"),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Spin()", "widget")) in calls
|
||||
|
||||
|
||||
def test_generic_list_return_type(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer(
|
||||
"var items = ServiceFactory.CreateList<Item>(); items.Add(null);"
|
||||
),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Add()", "list")) in calls
|
||||
|
||||
|
||||
def test_ambiguous_overload_no_edge(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer("var w = ServiceFactory.Pick(1); w.Spin();"),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Spin()", "widget")) not in calls
|
||||
|
||||
|
||||
def test_missing_declaration_no_edge(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer(
|
||||
"var w = ServiceFactory.Unknown<Widget>(); w.Spin();"
|
||||
" var v = Elsewhere.Get<Widget>(scope); v.Spin();"
|
||||
),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Spin()", "widget")) not in calls
|
||||
|
||||
|
||||
def test_inferred_generic_no_edge(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer(
|
||||
"var w = ServiceFactory.Infer(new Widget()); w.Spin();"
|
||||
),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Spin()", "widget")) not in calls
|
||||
|
||||
|
||||
def test_class_type_parameter_return_no_edge(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer(
|
||||
"var box = new Box<Widget>(); var w = box.Value(); w.Spin();"
|
||||
),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Value()", "box")) in calls
|
||||
assert (_run(r), _find(r, ".Spin()", "widget")) not in calls
|
||||
|
||||
|
||||
def test_explicit_type_and_new_still_resolve(tmp_path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Lib.cs": _LIB,
|
||||
"Consumer.cs": _consumer(
|
||||
"IReport a = ServiceFactory.Get<IReport>(scope); a.Build(\"x\");"
|
||||
" var w = new Widget(); w.Spin();"
|
||||
),
|
||||
})
|
||||
assert (_run(r), _find(r, ".Build()", "ireport")) in calls
|
||||
assert (_run(r), _find(r, ".Spin()", "widget")) in calls
|
||||
Reference in new issue
Block a user