Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions graphify/extractors/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3441,6 +3441,59 @@ def _emit_java_parent_type(type_node, rel: str, at_line: int) -> None:
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
Expand Down
72 changes: 72 additions & 0 deletions tests/test_csharp_member_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"