From bb5e5192df14a522cf0c8ec72ca1bd5452af29da Mon Sep 17 00:00:00 2001 From: Synvoya <16019863+Synvoya@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:24:22 +1000 Subject: [PATCH] fix(csharp): emit type references for properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C# class-body walker only handled field_declaration, so a property's type produced no references(field) edge. In idiomatic C#, auto-properties (`public Widget Main { get; set; }`) — not bare fields — are the standard way to declare state, so this silently dropped most of a class's type relationships. Add a property_declaration branch alongside the field_declaration handler, guarded the same way (ts_module == tree_sitter_c_sharp, parent_class_nid set). A property exposes its type on the node directly (no variable_declaration wrapper), so read it via child_by_field_name("type") and collect refs with _csharp_collect_type_refs, mirroring the Java/PHP/Kotlin siblings so List yields both the List field ref and the Widget generic_arg ref. Only emit when target != parent_class_nid. --- graphify/extract.py | 29 +++++++++++++++++++++++++++++ tests/fixtures/sample.cs | 4 ++++ tests/test_languages.py | 11 +++++++++++ 3 files changed, 44 insertions(+) diff --git a/graphify/extract.py b/graphify/extract.py index e6c611d1..5a3ec2ab 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -3786,6 +3786,35 @@ def _extract_generic( "references", line, context="field", metadata=metadata) return + if (config.ts_module == "tree_sitter_c_sharp" + and t == "property_declaration" + and parent_class_nid): + # C# auto-properties (`public Widget Main { get; set; }`) are the + # idiomatic way to declare state, yet only field_declaration was + # handled — so property types produced no references edge. Unlike a + # field, a property exposes its type on the node directly (no + # variable_declaration wrapper), so read it straight off the `type` + # field. Use _csharp_collect_type_refs (like the Java/PHP/Kotlin + # siblings) so `List` yields both the List field ref and the + # Widget generic_arg ref. + type_node = node.child_by_field_name("type") + if type_node is not None: + line = node.start_point[0] + 1 + refs: list[tuple[str, str, bool, str]] = [] + _csharp_collect_type_refs(type_node, source, False, refs) + for ref_name, role, qualified, qualifier in refs: + ctx = "generic_arg" if role == "generic_arg" else "field" + target_nid = ensure_named_node(ref_name, line) + if target_nid != parent_class_nid: + metadata = {"ref_token": ref_name} + if qualified: + metadata["qualified"] = True + if qualifier: + metadata["ref_qualifier"] = qualifier + add_edge(parent_class_nid, target_nid, "references", + line, context=ctx, metadata=metadata) + return + if (config.ts_module == "tree_sitter_java" and t == "field_declaration" and parent_class_nid): diff --git a/tests/fixtures/sample.cs b/tests/fixtures/sample.cs index 4ee1b06e..729651c6 100644 --- a/tests/fixtures/sample.cs +++ b/tests/fixtures/sample.cs @@ -21,6 +21,10 @@ namespace GraphifyDemo { private readonly HttpClient _client; + public Processor Owner { get; set; } + + public List Workers { get; set; } + public DataProcessor() { _client = new HttpClient(); diff --git a/tests/test_languages.py b/tests/test_languages.py index 95dfd9a1..31832052 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -542,6 +542,17 @@ def test_csharp_field_type_references_have_field_context(): ), "DataProcessor field declarations should reference HttpClient with field context" +def test_csharp_property_type_references_have_field_context(): + r = extract_csharp(FIXTURES / "sample.cs") + field_refs = _edge_labels(r, "references", "field") + # `public Processor Owner { get; set; }` — property type -> field ref. + assert ("DataProcessor", "Processor") in field_refs + # `public List Workers { get; set; }` — the List container -> field. + assert ("DataProcessor", "List") in field_refs + # ...and the generic argument -> generic_arg. + assert ("DataProcessor", "Processor") in _edge_labels(r, "references", "generic_arg") + + def test_csharp_call_edges_have_call_context(): r = extract_csharp(FIXTURES / "sample.cs") node_by_id = {n["id"]: n["label"] for n in r["nodes"]}