fix(csharp): per-method receiver typing so cross-method name reuse stops dropping true calls edges (#2299)
The C# receiver-type table was per-FILE and poisoned a name on any conflicting/untypable rebind anywhere in the file, so `var item = items[i]` in one method silently deleted the true calls edge in another method where the same name was a typed parameter (~2.3% of calls lost, per the reporter). Ported C# to a per-method table mirroring the Java resolver (per-class field scope + method params/locals, method-local poisoning only) and retired the file-wide table. The namespace resolver, ambiguity bail, and inherits-chain guards are unchanged, so the no-wrong-edge bar holds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2ca565ac1a
commit
f99f8d70c8
+13
-16
@@ -138,7 +138,7 @@ from graphify.extractors.resolution import ( # noqa: E402,F401
|
||||
|
||||
from graphify.symbol_resolution import resolve_bash_source_edges # noqa: E402
|
||||
|
||||
from graphify.extractors.engine import REFERENCE_CONTEXTS, _CSHARP_TYPE_PARAMETER_SCOPE_DECLARATIONS, _C_PRIMITIVE_TYPE_NODES, _JAVA_BUILTIN_TYPES, _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS, _JS_FUNCTION_VALUE_TYPES, _JS_SCOPE_BOUNDARY, _PYTHON_ANNOTATION_NOISE, _PYTHON_TYPE_CONTAINERS, _RUBY_CLASS_FACTORIES, _c_collect_type_refs, _cpp_collect_type_refs, _cpp_declarator_name, _cpp_local_var_types, _csharp_attribute_names, _csharp_classify_base, _csharp_collect_type_refs, _csharp_extra_walk, _csharp_member_type_table, _csharp_namespace_id, _csharp_namespace_name, _csharp_pre_scan_interfaces, _csharp_type_parameters_in_scope, _dynamic_import_js, _extract_generic, _find_body, _find_require_call, _get_cpp_func_name, _java_annotation_names, _java_collect_type_refs, _java_extra_walk, _java_type_parameters_in_scope, _js_collect_pattern_idents, _js_dispatch_value_idents, _js_extra_walk, _js_local_bound_names, _js_member_assignment_target, _js_module_bound_names, _kotlin_collect_type_refs, _kotlin_function_return_type_node, _kotlin_property_type_node, _kotlin_user_type_name, _php_collect_type_refs, _php_method_return_type_node, _php_name_text, _python_collect_assignment_targets, _python_collect_param_refs, _python_collect_type_refs, _python_local_bound_names, _python_module_bound_names, _python_param_names, _read_csharp_type_name, _require_imports_js, _ruby_const_last_name, _ruby_extra_walk, _ruby_local_class_bindings, _ruby_new_class_name, _scala_collect_type_refs, _semantic_reference_edge, _source_location, _swift_classify_base, _swift_collect_type_refs, _swift_constructor_type, _swift_declaration_keyword, _swift_extra_walk, _swift_local_var_types, _swift_pre_scan, _swift_property_name, _swift_property_type_node, _swift_receiver_name, _swift_user_type_name, _ts_decorator_name, _ts_descendant_decorators, _ts_emit_decorator_edges, _ts_extra_walk, _ts_method_name, _ts_receiver_type_table # noqa: E402,F401
|
||||
from graphify.extractors.engine import REFERENCE_CONTEXTS, _CSHARP_TYPE_PARAMETER_SCOPE_DECLARATIONS, _C_PRIMITIVE_TYPE_NODES, _JAVA_BUILTIN_TYPES, _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS, _JS_FUNCTION_VALUE_TYPES, _JS_SCOPE_BOUNDARY, _PYTHON_ANNOTATION_NOISE, _PYTHON_TYPE_CONTAINERS, _RUBY_CLASS_FACTORIES, _c_collect_type_refs, _cpp_collect_type_refs, _cpp_declarator_name, _cpp_local_var_types, _csharp_attribute_names, _csharp_classify_base, _csharp_collect_type_refs, _csharp_extra_walk, _csharp_namespace_id, _csharp_namespace_name, _csharp_pre_scan_interfaces, _csharp_type_parameters_in_scope, _dynamic_import_js, _extract_generic, _find_body, _find_require_call, _get_cpp_func_name, _java_annotation_names, _java_collect_type_refs, _java_extra_walk, _java_type_parameters_in_scope, _js_collect_pattern_idents, _js_dispatch_value_idents, _js_extra_walk, _js_local_bound_names, _js_member_assignment_target, _js_module_bound_names, _kotlin_collect_type_refs, _kotlin_function_return_type_node, _kotlin_property_type_node, _kotlin_user_type_name, _php_collect_type_refs, _php_method_return_type_node, _php_name_text, _python_collect_assignment_targets, _python_collect_param_refs, _python_collect_type_refs, _python_local_bound_names, _python_module_bound_names, _python_param_names, _read_csharp_type_name, _require_imports_js, _ruby_const_last_name, _ruby_extra_walk, _ruby_local_class_bindings, _ruby_new_class_name, _scala_collect_type_refs, _semantic_reference_edge, _source_location, _swift_classify_base, _swift_collect_type_refs, _swift_constructor_type, _swift_declaration_keyword, _swift_extra_walk, _swift_local_var_types, _swift_pre_scan, _swift_property_name, _swift_property_type_node, _swift_receiver_name, _swift_user_type_name, _ts_decorator_name, _ts_descendant_decorators, _ts_emit_decorator_edges, _ts_extra_walk, _ts_method_name, _ts_receiver_type_table # noqa: E402,F401
|
||||
|
||||
from graphify.extractors.pascal import _PAS_BEGIN_END_TOKEN_RE, _PAS_CALL_RE, _PAS_END_SEMI_RE, _PAS_IMPL_HEADER_RE, _PAS_KEYWORDS, _PAS_METHOD_DECL_RE, _PAS_MODULE_RE, _PAS_TOKEN_RE, _PAS_TYPE_HEADER_RE, _PAS_USES_RE, _extract_pascal_regex, _pascal_find_body, _pascal_split_bases, _pascal_split_sections, _pascal_split_uses, _pascal_strip_comments, extract_pascal # noqa: E402,F401
|
||||
|
||||
@@ -2571,11 +2571,13 @@ def _resolve_csharp_member_calls(
|
||||
The shared cross-file pass drops every ``is_member_call`` because a bare method
|
||||
name collides across the corpus — and for C# an in-file bare match silently
|
||||
mis-bound ``_server.Save()`` to an unrelated ``Cache.Save()``. The C# extractor
|
||||
now records each member call's receiver plus a per-file ``name -> Type`` table
|
||||
(``csharp_type_table``) of fields/properties/params/locals (with conflicting
|
||||
rebindings POISONED out, so a shadowing local of a different type produces no
|
||||
edge rather than a wrong one). This pass types the receiver, then resolves the
|
||||
declared type name with the same namespace/using/alias scoping machinery the
|
||||
records each member call's receiver and stamps ``receiver_type`` on the raw
|
||||
call from a METHOD-scoped ``name -> Type`` table of class fields/properties
|
||||
plus the declaring method's params/locals (#2299 — per-method like Java, so a
|
||||
name rebound in a different method never poisons this one; same-method
|
||||
conflicts and untypable rebindings are still POISONED, so a shadowing local of
|
||||
a different type produces no edge rather than a wrong one). This pass resolves
|
||||
the stamped type name with the same namespace/using/alias scoping machinery the
|
||||
type-reference pass uses (``CsharpNameResolver``), so a class name duplicated
|
||||
across namespaces still binds to the one in scope; only when scoping knows
|
||||
nothing about the name does it fall back to the corpus-wide unique bare-name
|
||||
@@ -2586,8 +2588,9 @@ def _resolve_csharp_member_calls(
|
||||
* ``this.M()`` — receiver is the caller's own enclosing class -> EXTRACTED.
|
||||
* ``base.M()`` — the caller's single resolvable base class -> EXTRACTED.
|
||||
* ``Type.M()`` (capitalized) — the type is named explicitly in source -> EXTRACTED.
|
||||
* ``recv.M()`` / ``this.recv.M()`` — ``recv`` typed via the file's
|
||||
field/param/local table -> INFERRED.
|
||||
* ``recv.M()`` / ``this.recv.M()`` — ``recv`` typed via the extractor's
|
||||
method-scoped field/property/param/local table (``receiver_type`` on the
|
||||
raw call) -> INFERRED.
|
||||
|
||||
A method not declared on the receiver's type is looked up through its
|
||||
``inherits`` chain; a chain containing an unresolvable (out-of-corpus) base
|
||||
@@ -2595,12 +2598,6 @@ def _resolve_csharp_member_calls(
|
||||
|
||||
Must run after id-disambiguation so node ids and caller_nids are final.
|
||||
"""
|
||||
type_table_by_file: dict[str, dict[str, str]] = {}
|
||||
for result in per_file:
|
||||
tt = result.get("csharp_type_table")
|
||||
if tt and tt.get("path"):
|
||||
type_table_by_file[tt["path"]] = tt.get("table", {})
|
||||
|
||||
def _key(label: str) -> str:
|
||||
return re.sub(r"[^a-zA-Z0-9]+", "", str(label)).lower()
|
||||
|
||||
@@ -2739,13 +2736,13 @@ def _resolve_csharp_member_calls(
|
||||
# explicit-type lookup misses).
|
||||
type_nid = _resolve_type_name_nid(receiver, caller_node, src_file)
|
||||
if not type_nid:
|
||||
type_name = type_table_by_file.get(src_file, {}).get(receiver)
|
||||
type_name = rc.get("receiver_type")
|
||||
type_nid = _resolve_type_name_nid(type_name, caller_node, src_file)
|
||||
if not type_nid:
|
||||
continue
|
||||
type_qualified = True
|
||||
else:
|
||||
type_name = type_table_by_file.get(src_file, {}).get(receiver)
|
||||
type_name = rc.get("receiver_type")
|
||||
if not type_name:
|
||||
continue
|
||||
type_nid = _resolve_type_name_nid(type_name, caller_node, src_file)
|
||||
|
||||
+186
-84
@@ -1420,80 +1420,138 @@ def _swift_local_var_types(body_node, source: bytes, table: dict[str, str]) -> N
|
||||
for c in n.children:
|
||||
stack.append(c)
|
||||
|
||||
def _csharp_member_type_table(root, source: bytes) -> dict[str, str]:
|
||||
"""Collect ``name -> TypeName`` for C# receiver typing (#1609): class fields,
|
||||
properties, method parameters, and local variable declarations.
|
||||
def _csharp_receiver_type_name(type_node, source: bytes) -> str | None:
|
||||
"""Resolve a C# declared type to a receiver-typable class name, or None.
|
||||
|
||||
File-scoped with conflict POISONING (#1620): a name bound to two different
|
||||
resolvable types anywhere in the file — or bound once to a resolvable type and
|
||||
redeclared with an unresolvable one (``var x = Compute();``, a primitive, a
|
||||
``dynamic``) — is dropped from the table entirely, so a local shadowing a field
|
||||
of a DIFFERENT type can never produce a wrong edge (the resolver simply emits
|
||||
none). Consistent rebindings (the same resolved type) keep the single entry.
|
||||
Only a resolvable, non-`var` type name is recorded; `var` without a `new T()`
|
||||
initializer, and predefined/lower-cased primitives, are unresolvable (precision
|
||||
over recall — an untypable receiver is left for the resolver to drop rather
|
||||
than guess). `var v = new T()` is typed from the object-creation.
|
||||
A genuine C# class name is Pascal-cased; predefined primitives
|
||||
(int/bool/string) and ``dynamic`` never own a resolvable method definition
|
||||
here, and ``var`` (``implicit_type``) carries no name at all.
|
||||
"""
|
||||
table: dict[str, str] = {}
|
||||
poisoned: set[str] = set()
|
||||
|
||||
def _bind(name: str | None, resolved: str | None) -> None:
|
||||
if not name:
|
||||
return
|
||||
if resolved is None or table.get(name, resolved) != resolved:
|
||||
# An unresolvable redeclaration, or a second binding with a different
|
||||
# type: the name is scope-ambiguous at file granularity — poison it.
|
||||
poisoned.add(name)
|
||||
else:
|
||||
table[name] = resolved
|
||||
|
||||
def _typed(type_node) -> str | None:
|
||||
info = _read_csharp_type_name(type_node, source)
|
||||
if not info:
|
||||
return None
|
||||
name = info[0]
|
||||
# A genuine C# class name is Pascal-cased; skip predefined primitives
|
||||
# (int/bool/string) which never own a resolvable method definition here.
|
||||
return name if name and name[:1].isupper() else None
|
||||
|
||||
def _decl_names(var_decl):
|
||||
for c in var_decl.children:
|
||||
if c.type == "variable_declarator":
|
||||
nm = c.child_by_field_name("name") or next(
|
||||
(g for g in c.children if g.type == "identifier"), None)
|
||||
if nm is not None:
|
||||
yield _read_text(nm, source), c
|
||||
|
||||
def _new_type(declarator) -> str | None:
|
||||
# `var v = new Server()` — recover the type from the object_creation_expression.
|
||||
for g in declarator.children:
|
||||
if g.type == "object_creation_expression":
|
||||
return _typed(g.child_by_field_name("type"))
|
||||
info = _read_csharp_type_name(type_node, source)
|
||||
if not info:
|
||||
return None
|
||||
name = info[0]
|
||||
return name if name and name[:1].isupper() else None
|
||||
|
||||
stack = [root]
|
||||
|
||||
def _csharp_method_receiver_types(
|
||||
method_node,
|
||||
source: bytes,
|
||||
field_types: dict[str, str],
|
||||
) -> dict[str, str]:
|
||||
"""Build the receiver type table visible to one C# method (#2299).
|
||||
|
||||
The C# twin of ``_java_method_receiver_types``: current-class fields and
|
||||
properties are the base scope, and parameters plus local declarations bind
|
||||
on top of them for the full method. C# scoping is per-method, so a name
|
||||
rebound in a DIFFERENT method never poisons this one — the #2299 regression
|
||||
under the old file-wide table, where ``var item = items[i]`` in one method
|
||||
(untypable) silently deleted the true edge for a same-named, explicitly
|
||||
typed parameter elsewhere in the file.
|
||||
|
||||
Poisoning stays method-local and conservative, because raw call facts do
|
||||
not retain lexical position inside the method: a name is dropped entirely on
|
||||
an unresolvable binding (``var x = Compute();``, a primitive, ``dynamic``,
|
||||
an untyped lambda parameter), a same-method conflict, or a conflict with
|
||||
the class field's type for that name. ``var v = new T()`` is typed from the
|
||||
object-creation (precision over recall — an untypable receiver is left for
|
||||
the resolver to drop rather than guess).
|
||||
"""
|
||||
method_types: dict[str, str] = {}
|
||||
ambiguous: set[str] = set()
|
||||
|
||||
def bind(name: str | None, type_name: str | None) -> None:
|
||||
if not name or name in ambiguous:
|
||||
return
|
||||
if (
|
||||
type_name is None
|
||||
or method_types.get(name, type_name) != type_name
|
||||
or field_types.get(name) not in (None, type_name)
|
||||
):
|
||||
method_types.pop(name, None)
|
||||
ambiguous.add(name)
|
||||
else:
|
||||
method_types[name] = type_name
|
||||
|
||||
def bind_parameter(param) -> None:
|
||||
name_node = param.child_by_field_name("name")
|
||||
if name_node is not None:
|
||||
bind(
|
||||
_read_text(name_node, source),
|
||||
_csharp_receiver_type_name(param.child_by_field_name("type"), source),
|
||||
)
|
||||
|
||||
params = method_node.child_by_field_name("parameters")
|
||||
if params is not None:
|
||||
for param in params.children:
|
||||
if param.type == "parameter":
|
||||
bind_parameter(param)
|
||||
|
||||
body = method_node.child_by_field_name("body")
|
||||
stack = list(body.children) if body is not None else []
|
||||
while stack:
|
||||
n = stack.pop()
|
||||
t = n.type
|
||||
if t in ("field_declaration", "local_declaration_statement"):
|
||||
vd = next((c for c in n.children if c.type == "variable_declaration"), None)
|
||||
node = stack.pop()
|
||||
if node.type in (
|
||||
"class_declaration",
|
||||
"struct_declaration",
|
||||
"interface_declaration",
|
||||
"record_declaration",
|
||||
"enum_declaration",
|
||||
):
|
||||
continue
|
||||
if node.type == "lambda_expression":
|
||||
# Raw calls are method-scoped, so a lambda-local binding cannot be
|
||||
# distinguished from an enclosing binding with the same name: a
|
||||
# typed lambda parameter binds, an untyped one (`x => ...`,
|
||||
# `(z) => ...`) binds None and poisons the name method-locally.
|
||||
lam_params = node.child_by_field_name("parameters")
|
||||
if lam_params is not None:
|
||||
if lam_params.type == "implicit_parameter":
|
||||
bind(_read_text(lam_params, source), None)
|
||||
else:
|
||||
for param in lam_params.children:
|
||||
if param.type == "parameter":
|
||||
bind_parameter(param)
|
||||
elif param.type == "implicit_parameter":
|
||||
bind(_read_text(param, source), None)
|
||||
elif node.type == "local_function_statement":
|
||||
lf_params = node.child_by_field_name("parameters")
|
||||
if lf_params is not None:
|
||||
for param in lf_params.children:
|
||||
if param.type == "parameter":
|
||||
bind_parameter(param)
|
||||
elif node.type == "local_declaration_statement":
|
||||
vd = next(
|
||||
(c for c in node.children if c.type == "variable_declaration"), None
|
||||
)
|
||||
if vd is not None:
|
||||
type_node = vd.child_by_field_name("type")
|
||||
declared = _typed(type_node)
|
||||
for name, decl in _decl_names(vd):
|
||||
_bind(name, declared or _new_type(decl))
|
||||
elif t == "property_declaration":
|
||||
nm = n.child_by_field_name("name")
|
||||
if nm is not None:
|
||||
_bind(_read_text(nm, source), _typed(n.child_by_field_name("type")))
|
||||
elif t == "parameter":
|
||||
nm = n.child_by_field_name("name")
|
||||
if nm is not None:
|
||||
_bind(_read_text(nm, source), _typed(n.child_by_field_name("type")))
|
||||
for c in n.children:
|
||||
stack.append(c)
|
||||
for name in poisoned:
|
||||
declared = _csharp_receiver_type_name(
|
||||
vd.child_by_field_name("type"), source
|
||||
)
|
||||
for declarator in vd.children:
|
||||
if declarator.type != "variable_declarator":
|
||||
continue
|
||||
name_node = declarator.child_by_field_name("name") or next(
|
||||
(g for g in declarator.children if g.type == "identifier"),
|
||||
None,
|
||||
)
|
||||
if name_node is None:
|
||||
continue
|
||||
type_name = declared
|
||||
if type_name is None:
|
||||
# `var v = new T()` — recover T from the object-creation.
|
||||
for g in declarator.children:
|
||||
if g.type == "object_creation_expression":
|
||||
type_name = _csharp_receiver_type_name(
|
||||
g.child_by_field_name("type"), source
|
||||
)
|
||||
break
|
||||
bind(_read_text(name_node, source), type_name)
|
||||
stack.extend(node.children)
|
||||
|
||||
table = dict(field_types)
|
||||
table.update(method_types)
|
||||
for name in ambiguous:
|
||||
table.pop(name, None)
|
||||
return table
|
||||
|
||||
@@ -2274,6 +2332,12 @@ def _extract_generic(
|
||||
# while parameters and locals belong only to their declaring method.
|
||||
java_field_types: dict[str, dict[str, str]] = {}
|
||||
java_method_scopes: dict[int, tuple[object, str]] = {}
|
||||
# C# receiver typing is method-scoped too (#2299): class fields/properties
|
||||
# are shared, parameters and locals belong only to their declaring method —
|
||||
# the old file-wide table let one method's untypable rebinding poison a
|
||||
# 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]] = {}
|
||||
|
||||
csharp_interface_names: set[str] = set()
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
@@ -2977,6 +3041,24 @@ def _extract_generic(
|
||||
)
|
||||
if not type_name or type_name in csharp_type_params:
|
||||
return
|
||||
# Record the field's declared type for the method-scoped
|
||||
# receiver tables (#2299) — the C# twin of java_field_types.
|
||||
# Pascal-case only: primitives never own a resolvable method.
|
||||
if type_name[:1].isupper():
|
||||
fields = csharp_field_types.setdefault(parent_class_nid, {})
|
||||
for child in node.children:
|
||||
if child.type != "variable_declaration":
|
||||
continue
|
||||
for declarator in child.children:
|
||||
if declarator.type != "variable_declarator":
|
||||
continue
|
||||
name_node = declarator.child_by_field_name("name") or next(
|
||||
(g for g in declarator.children
|
||||
if g.type == "identifier"),
|
||||
None,
|
||||
)
|
||||
if name_node is not None:
|
||||
fields[_read_text(name_node, source)] = type_name
|
||||
line = node.start_point[0] + 1
|
||||
metadata = {"ref_token": type_name}
|
||||
if qualified:
|
||||
@@ -3000,6 +3082,15 @@ def _extract_generic(
|
||||
# Widget generic_arg ref.
|
||||
type_node = node.child_by_field_name("type")
|
||||
if type_node is not None:
|
||||
# Record the property's declared type for the method-scoped
|
||||
# receiver tables (#2299), like a field: `Main.Render()` on a
|
||||
# `public Widget Main { get; set; }` types Main as Widget.
|
||||
prop_name_node = node.child_by_field_name("name")
|
||||
prop_type = _csharp_receiver_type_name(type_node, source)
|
||||
if prop_name_node is not None and prop_type:
|
||||
csharp_field_types.setdefault(parent_class_nid, {})[
|
||||
_read_text(prop_name_node, source)
|
||||
] = prop_type
|
||||
line = node.start_point[0] + 1
|
||||
refs: list[tuple[str, str, bool, str]] = []
|
||||
_csharp_collect_type_refs(type_node, source, False, refs)
|
||||
@@ -3594,6 +3685,8 @@ def _extract_generic(
|
||||
if body:
|
||||
if config.ts_module == "tree_sitter_java" and parent_class_nid:
|
||||
java_method_scopes[id(body)] = (node, parent_class_nid)
|
||||
if config.ts_module == "tree_sitter_c_sharp" and parent_class_nid:
|
||||
csharp_method_scopes[id(body)] = (node, parent_class_nid)
|
||||
function_bodies.append((func_nid, body))
|
||||
return
|
||||
|
||||
@@ -3738,6 +3831,14 @@ def _extract_generic(
|
||||
)
|
||||
for body_id, (method_node, class_nid) in java_method_scopes.items()
|
||||
}
|
||||
csharp_receiver_types = {
|
||||
body_id: _csharp_method_receiver_types(
|
||||
method_node,
|
||||
source,
|
||||
csharp_field_types.get(class_nid, {}),
|
||||
)
|
||||
for body_id, (method_node, class_nid) in csharp_method_scopes.items()
|
||||
}
|
||||
|
||||
def _emit_indirect_by_name(ident_name: str, loc_node, scope_nid: str,
|
||||
context: str) -> None:
|
||||
@@ -3887,7 +3988,7 @@ def _extract_generic(
|
||||
def walk_calls(
|
||||
node,
|
||||
caller_nid: str,
|
||||
java_types: dict[str, str] | None = None,
|
||||
receiver_types: dict[str, str] | None = None,
|
||||
extra_locals: frozenset[str] = frozenset(),
|
||||
) -> None:
|
||||
if node.type in config.function_boundary_types:
|
||||
@@ -3912,7 +4013,7 @@ def _extract_generic(
|
||||
# closures compound the same way on their own recursion.
|
||||
closure_locals = extra_locals | _js_local_bound_names(node, source)
|
||||
for child in node.children:
|
||||
walk_calls(child, caller_nid, java_types, closure_locals)
|
||||
walk_calls(child, caller_nid, receiver_types, closure_locals)
|
||||
return
|
||||
|
||||
if node.type in config.call_types:
|
||||
@@ -3922,7 +4023,7 @@ def _extract_generic(
|
||||
edges, seen_dyn_import_pairs):
|
||||
# Still recurse into children (import().then(...) may have calls)
|
||||
for child in node.children:
|
||||
walk_calls(child, caller_nid, java_types, extra_locals)
|
||||
walk_calls(child, caller_nid, receiver_types, extra_locals)
|
||||
return
|
||||
|
||||
callee_name: str | None = None
|
||||
@@ -4234,14 +4335,19 @@ def _extract_generic(
|
||||
# suffix sets, so a source_file suffix alone can't separate them.
|
||||
if config.ts_module == "tree_sitter_cpp":
|
||||
rc_entry["lang"] = "cpp"
|
||||
# C#: tag the raw_call so _resolve_csharp_member_calls claims it
|
||||
# and types the receiver against the file's field/param/local
|
||||
# type table (#1609).
|
||||
# C#: tag the raw_call so _resolve_csharp_member_calls claims
|
||||
# it, and stamp the receiver's type from the METHOD-scoped
|
||||
# table (#1609, per-method since #2299). `this.field.M()` is
|
||||
# covered too: member_receiver is the bare field name, and
|
||||
# class fields/properties are in the table.
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
rc_entry["lang"] = "csharp"
|
||||
receiver_type = (receiver_types or {}).get(member_receiver or "")
|
||||
if receiver_type:
|
||||
rc_entry["receiver_type"] = receiver_type
|
||||
if config.ts_module == "tree_sitter_java":
|
||||
rc_entry["lang"] = "java"
|
||||
receiver_type = (java_types or {}).get(member_receiver or "")
|
||||
receiver_type = (receiver_types or {}).get(member_receiver or "")
|
||||
if receiver_type:
|
||||
rc_entry["receiver_type"] = receiver_type
|
||||
raw_calls.append(rc_entry)
|
||||
@@ -4450,7 +4556,7 @@ def _extract_generic(
|
||||
_emit_indirect_ref(ident, caller_nid, enclosing_locals, "return")
|
||||
|
||||
for child in node.children:
|
||||
walk_calls(child, caller_nid, java_types, extra_locals)
|
||||
walk_calls(child, caller_nid, receiver_types, extra_locals)
|
||||
|
||||
if config.ts_module == "tree_sitter_ruby":
|
||||
for caller_nid, body_node in function_bodies:
|
||||
@@ -4479,11 +4585,14 @@ def _extract_generic(
|
||||
# (#1630 Pattern B). Guarding on the tracked set prevents double-walking.
|
||||
_tracked_body_ids.update(id(b) for _, b in function_bodies)
|
||||
|
||||
# Body ids are unique (one language per file), so the Java and C# per-method
|
||||
# receiver tables merge without collision.
|
||||
receiver_types_by_body = {**java_receiver_types, **csharp_receiver_types}
|
||||
for caller_nid, body_node in function_bodies:
|
||||
walk_calls(
|
||||
body_node,
|
||||
caller_nid,
|
||||
java_receiver_types.get(id(body_node)),
|
||||
receiver_types_by_body.get(id(body_node)),
|
||||
)
|
||||
|
||||
# #1356: walk property/field initializers (collected above). walk_calls
|
||||
@@ -4611,13 +4720,6 @@ def _extract_generic(
|
||||
result["ts_type_table"] = {"path": str_path, "table": type_table}
|
||||
elif config.ts_module == "tree_sitter_cpp":
|
||||
result["cpp_type_table"] = {"path": str_path, "table": type_table}
|
||||
# C#: a file-wide receiver type table (field/property/param/local -> Type) for
|
||||
# _resolve_csharp_member_calls (#1609). Built from the whole tree, not just
|
||||
# function bodies, so class-level fields/properties are in scope for every method.
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
cs_table = _csharp_member_type_table(root, source)
|
||||
if cs_table:
|
||||
result["csharp_type_table"] = {"path": str_path, "table": cs_table}
|
||||
return result
|
||||
|
||||
def _python_decorator_name(deco_node, source: bytes) -> str | None:
|
||||
|
||||
@@ -327,6 +327,109 @@ def test_unresolved_base_poisons_inherited_member_lookup(tmp_path):
|
||||
"unresolved base chain must bail, not mis-bind to Server.Save"
|
||||
|
||||
|
||||
# ── Method-scoped receiver typing (#2299) ────────────────────────────────────
|
||||
# C# scoping is per-method: a name rebound (even untypably) in ONE method must
|
||||
# not poison a same-named, explicitly typed receiver in a DIFFERENT method. The
|
||||
# old file-wide table did exactly that, silently deleting true calls edges.
|
||||
|
||||
|
||||
def test_cross_method_name_reuse_does_not_poison(tmp_path):
|
||||
"""#2299 corpus: `var item = items[i]` (untypable) in RunIndexed must not
|
||||
poison the explicitly typed `Item item` parameter in RunOne."""
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Item.cs": (
|
||||
"namespace Demo {\n"
|
||||
" public class Item { public void Handle() {} }\n"
|
||||
"}\n"
|
||||
),
|
||||
"Runner.cs": (
|
||||
"using System.Collections.Generic;\n"
|
||||
"namespace Demo {\n"
|
||||
" public class Runner {\n"
|
||||
" public void RunOne(Item item) { item.Handle(); }\n"
|
||||
" public void RunIndexed(List<Item> items, int i) {\n"
|
||||
" var item = items[i];\n"
|
||||
" item.Handle();\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
run_one = _find(r, ".RunOne()", "runner")
|
||||
run_indexed = _find(r, ".RunIndexed()", "runner")
|
||||
handle = _find(r, ".Handle()", "item")
|
||||
assert (run_one, handle) in calls, \
|
||||
"typed param receiver must resolve despite a same-named untypable local elsewhere"
|
||||
edge = next(e for e in r["edges"] if e["relation"] == "calls"
|
||||
and e["source"] == run_one and e["target"] == handle)
|
||||
assert edge["confidence"] == "INFERRED"
|
||||
assert (run_indexed, handle) not in calls, \
|
||||
"the untypable local (`var item = items[i]`) stays unresolved — no guessed edge"
|
||||
|
||||
|
||||
def test_per_method_locals_resolve_independently(tmp_path):
|
||||
"""Same local name bound to DIFFERENT types in different methods: each
|
||||
method resolves to its own binding (the file-wide table poisoned both)."""
|
||||
calls, r = _calls(tmp_path, {
|
||||
"S.cs": (
|
||||
"public class HtmlWriter { public void Render() {} }\n"
|
||||
"public class TextWriter { public void Render() {} }\n"
|
||||
"public class Doc {\n"
|
||||
" public void AsHtml() { var w = new HtmlWriter(); w.Render(); }\n"
|
||||
" public void AsText() { var w = new TextWriter(); w.Render(); }\n"
|
||||
"}\n"
|
||||
)
|
||||
})
|
||||
as_html = _find(r, ".AsHtml()", "doc")
|
||||
as_text = _find(r, ".AsText()", "doc")
|
||||
html_render = _find(r, ".Render()", "htmlwriter")
|
||||
text_render = _find(r, ".Render()", "textwriter")
|
||||
assert (as_html, html_render) in calls
|
||||
assert (as_text, text_render) in calls
|
||||
assert (as_html, text_render) not in calls, "no cross-method binding leak"
|
||||
assert (as_text, html_render) not in calls, "no cross-method binding leak"
|
||||
|
||||
|
||||
def test_same_method_shadow_still_poisons(tmp_path):
|
||||
"""Keep-the-bar: a SAME-method conflict (param `Server x` + local `Other x`)
|
||||
still poisons the name — raw calls carry no lexical position, so neither
|
||||
candidate may win."""
|
||||
calls, r = _calls(tmp_path, {
|
||||
"S.cs": (
|
||||
"public class Server { public bool Run() => true; }\n"
|
||||
"public class Other { public bool Run() => false; }\n"
|
||||
"public class Holder {\n"
|
||||
" public bool A(Server x) { Other x = new Other(); return x.Run(); }\n"
|
||||
"}\n"
|
||||
)
|
||||
})
|
||||
holder_a = _find(r, ".A()", "holder")
|
||||
server_run = _find(r, ".Run()", "server")
|
||||
other_run = _find(r, ".Run()", "other")
|
||||
assert (holder_a, server_run) not in calls
|
||||
assert (holder_a, other_run) not in calls
|
||||
|
||||
|
||||
def test_file_scoped_namespace_receiver_resolves(tmp_path):
|
||||
"""The C# 10 file-scoped namespace form (`namespace Demo;`) types receivers
|
||||
the same as the braced form."""
|
||||
calls, r = _calls(tmp_path, {
|
||||
"Item.cs": (
|
||||
"namespace Demo;\n"
|
||||
"public class Item { public void Handle() {} }\n"
|
||||
),
|
||||
"Runner.cs": (
|
||||
"namespace Demo;\n"
|
||||
"public class Runner {\n"
|
||||
" public void RunOne(Item item) { item.Handle(); }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
run_one = _find(r, ".RunOne()", "runner")
|
||||
handle = _find(r, ".Handle()", "item")
|
||||
assert (run_one, handle) in calls
|
||||
|
||||
|
||||
def test_method_chained_off_new_expression_resolves(tmp_path):
|
||||
"""#1770: a method invoked directly on a `new X(...)` object-creation
|
||||
expression (no intermediate variable) must still emit a calls edge to the
|
||||
|
||||
Reference in New Issue
Block a user