mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-27 00:36:39 +00:00
fix(csharp): walk primary-constructor parameters for references and calls (#2829)
A C# 12 primary constructor puts its parameters on the class/record declaration itself, which the extractor never walked: class Holder(IDep dep) emitted no references edge Holder->IDep, and because dep was never registered in the class receiver table, a dep.Method() call inside the class was dropped too. Scan the declaration's parameter_list, register each param name->type, and emit the param type reference — mirroring the field/property handlers. Built-in and type-parameter types are not fabricated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
7c10a961b0
commit
3e8ec33994
@@ -3500,6 +3500,59 @@ def _extract_generic(
|
||||
add_edge(class_nid, target_nid, "references",
|
||||
cp_line, context=ctx)
|
||||
|
||||
# C#: a primary constructor (`class Foo(IBar bar)`, C# 12+) declares
|
||||
# its dependencies on the type declaration itself rather than in a
|
||||
# field or property, so neither the field_declaration nor the
|
||||
# property_declaration handler ever sees them — the parameter type
|
||||
# got no references edge, and because the name was never registered
|
||||
# in csharp_field_types, _csharp_method_receiver_types could not type
|
||||
# the receiver either, so calls through it (`bar.Baz()`) lost their
|
||||
# calls edge as well. The Scala class_parameters branch directly
|
||||
# above is the analogue; Kotlin's is #2063. Grammar note: the list is
|
||||
# an UNNAMED child of the declaration, so child_by_field_name(
|
||||
# "parameters") returns None and the children must be scanned.
|
||||
if config.ts_module == "tree_sitter_c_sharp" and t in (
|
||||
"class_declaration",
|
||||
"record_declaration",
|
||||
"struct_declaration",
|
||||
):
|
||||
csharp_type_params = _csharp_type_parameters_in_scope(node, source)
|
||||
for c in node.children:
|
||||
if c.type != "parameter_list":
|
||||
continue
|
||||
for param in c.children:
|
||||
if param.type != "parameter":
|
||||
continue
|
||||
ptype = param.child_by_field_name("type")
|
||||
if ptype is None:
|
||||
continue
|
||||
pname = param.child_by_field_name("name")
|
||||
p_line = param.start_point[0] + 1
|
||||
# Receiver binding mirrors the field_declaration rule:
|
||||
# Pascal-case only (a primitive owns no resolvable
|
||||
# method) and never a bare type parameter (`T item`).
|
||||
recv = _csharp_receiver_type_name(ptype, source)
|
||||
if (pname is not None and recv and recv[:1].isupper()
|
||||
and recv not in csharp_type_params):
|
||||
csharp_field_types.setdefault(class_nid, {})[
|
||||
_read_text(pname, source)
|
||||
] = recv
|
||||
refs = []
|
||||
_csharp_collect_type_refs(
|
||||
ptype, source, False, refs, csharp_type_params
|
||||
)
|
||||
for ref_name, role, qualified, qualifier in refs:
|
||||
ctx = "generic_arg" if role == "generic_arg" else "field"
|
||||
target_nid = ensure_named_node(ref_name, p_line)
|
||||
if target_nid != class_nid:
|
||||
metadata = {"ref_token": ref_name}
|
||||
if qualified:
|
||||
metadata["qualified"] = True
|
||||
if qualifier:
|
||||
metadata["ref_qualifier"] = qualifier
|
||||
add_edge(class_nid, target_nid, "references",
|
||||
p_line, context=ctx, metadata=metadata)
|
||||
|
||||
# C++-specific: inheritance via base_class_clause (class and struct).
|
||||
# tree-sitter-cpp shape:
|
||||
# class_specifier / struct_specifier
|
||||
|
||||
@@ -668,3 +668,75 @@ def test_sibling_pattern_rebind_conflict_poisons(tmp_path):
|
||||
twig_go = _find(r, ".Go()", "twig")
|
||||
assert (r_a, sect_go) not in calls, "conflicting pattern bindings must poison the name"
|
||||
assert (r_a, twig_go) not in calls, "conflicting pattern bindings must poison the name"
|
||||
|
||||
|
||||
def _refs(r):
|
||||
return {(e["source"], e["target"]) for e in r["edges"]
|
||||
if e["relation"] == "references"}
|
||||
|
||||
|
||||
_PRIMARY_CTOR = {
|
||||
"S.cs": (
|
||||
"public interface IDep { bool Plain(); }\n"
|
||||
"public class Holder(IDep dep) {\n"
|
||||
" public bool Run() { return dep.Plain(); }\n"
|
||||
"}\n"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def test_primary_constructor_parameter_emits_references_edge(tmp_path):
|
||||
"""`class Holder(IDep dep)` declares its dependency on the type declaration
|
||||
rather than in a field or property, so neither the field_declaration nor the
|
||||
property_declaration handler sees it — the type still needs a references edge."""
|
||||
_, r = _calls(tmp_path, _PRIMARY_CTOR)
|
||||
holder = _find(r, "Holder", "holder")
|
||||
idep = _find(r, "IDep", "idep")
|
||||
assert (holder, idep) in _refs(r), \
|
||||
"a primary-constructor parameter type must produce a references edge"
|
||||
|
||||
|
||||
def test_primary_constructor_parameter_resolves_member_calls(tmp_path):
|
||||
"""A call through a primary-constructor parameter must resolve to the
|
||||
parameter's declared type, exactly as a field receiver does."""
|
||||
calls, r = _calls(tmp_path, _PRIMARY_CTOR)
|
||||
run = _find(r, ".Run()", "holder")
|
||||
plain = _find(r, ".Plain()", "idep")
|
||||
assert (run, plain) in calls, \
|
||||
"dep.Plain() must resolve through the primary-constructor parameter"
|
||||
|
||||
|
||||
def test_record_positional_parameter_emits_references_edge(tmp_path):
|
||||
"""Positional record parameters use the same parameter_list shape."""
|
||||
_, r = _calls(tmp_path, {
|
||||
"S.cs": (
|
||||
"public interface IDep { bool Plain(); }\n"
|
||||
"public record Holder(IDep Dep) {\n"
|
||||
" public bool Run() { return Dep.Plain(); }\n"
|
||||
"}\n"
|
||||
)
|
||||
})
|
||||
holder = _find(r, "Holder", "holder")
|
||||
idep = _find(r, "IDep", "idep")
|
||||
assert (holder, idep) in _refs(r), \
|
||||
"a positional record parameter type must produce a references edge"
|
||||
|
||||
|
||||
def test_primary_constructor_type_parameter_is_not_referenced(tmp_path):
|
||||
"""A bare type parameter (`T item`) names no real type — it must not be
|
||||
emitted as a phantom referenced node."""
|
||||
_, r = _calls(tmp_path, {
|
||||
"S.cs": (
|
||||
"public interface IDep { bool Plain(); }\n"
|
||||
"public class Holder<T>(IDep dep, T item) {\n"
|
||||
" public bool Run() { return dep.Plain(); }\n"
|
||||
"}\n"
|
||||
)
|
||||
})
|
||||
holder = _find(r, "Holder", "holder")
|
||||
idep = _find(r, "IDep", "idep")
|
||||
refs = _refs(r)
|
||||
assert (holder, idep) in refs, "the real dependency must still be referenced"
|
||||
labels = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
assert not [t for s, t in refs if s == holder and labels.get(t) == "T"], \
|
||||
"a type parameter must not be emitted as a referenced type"
|
||||
|
||||
Reference in New Issue
Block a user