From 3c407dcb8c501acdda20c26dcb7afcf1fdfd4bcd Mon Sep 17 00:00:00 2001 From: KaiyiQuan Date: Sun, 27 Sep 2026 14:38:17 +0800 Subject: [PATCH] fix(csharp): don't collect tuple element names as type references (#3796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A named tuple type `(int mode, string label)` is a `tuple_type` whose `tuple_element` children carry both a `type` and a `name` field. _csharp_collect_type_refs walked every named child, so the element NAMES were collected as type references, minted as sourceless placeholder nodes and emitted `references` edges — one junk type node per element, repeated in every file declaring such a signature. Only the `type` field of each tuple element should feed type-reference collection; the `name` is an identifier, not a type reference. Fix the collector to descend into the element's type field only. Regression: tests/test_csharp_tuple_type_refs.py asserts no node/edge is minted for element names (all existing C# tests pass). (cherry picked from commit bfcdda97b08e48d5f6e61cff038de11bf39f796c) --- graphify/extractors/engine.py | 11 ++++++ tests/test_csharp_tuple_type_refs.py | 52 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/test_csharp_tuple_type_refs.py diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index c06a46bfa..c147c376f 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -235,6 +235,17 @@ def _csharp_collect_type_refs( if c.is_named: _csharp_collect_type_refs(c, source, generic, out, skip) return + if t == "tuple_type": + # A named tuple element carries both a `type` and a `name` field + # (`(int mode, string label)`). Only the `type` field feeds + # type-reference collection; the `name` is an identifier, not a type + # reference, and minting it produces junk "type" nodes (#3796). + for el in node.children: + if el.type == "tuple_element": + type_child = el.child_by_field_name("type") + if type_child is not None: + _csharp_collect_type_refs(type_child, source, generic, out, skip) + return if node.is_named: for c in node.children: if c.is_named: diff --git a/tests/test_csharp_tuple_type_refs.py b/tests/test_csharp_tuple_type_refs.py new file mode 100644 index 000000000..d5a8cad1e --- /dev/null +++ b/tests/test_csharp_tuple_type_refs.py @@ -0,0 +1,52 @@ +"""C# named-tuple element names must not be collected as type references (#3796). + +In tree-sitter-c-sharp a named tuple type `(int mode, string label)` is a +`tuple_type` whose children are `tuple_element` nodes carrying a `type` field +and a `name` field. The type-reference collector walks every named child, so +the element NAMES (`mode`, `label`) were minted as sourceless "type" nodes and +emitted `references` edges from the declaring member — one junk type node per +element, repeated in every file that declares such a signature. Only the +`type` field of each element should feed type-reference collection. +""" + +from graphify.extract import extract + +READER_CS = ( + "namespace Demo\n{\n public class Reader\n {\n" + " public static (int mode, string label) Read() => (1, \"a\");\n" + " }\n}\n" +) + + +def _extract_source(source: str): + import os + import tempfile + from pathlib import Path + + d = Path(tempfile.mkdtemp()) + p = d / "Reader.cs" + p.write_text(source) + old = os.getcwd() + try: + os.chdir(d) + return extract([Path("Reader.cs")], cache_root=Path(tempfile.mkdtemp())) + finally: + os.chdir(old) + + +def test_tuple_element_names_are_not_type_references(): + r = _extract_source(READER_CS) + + # Element NAMES must not be minted as type nodes. + labels = {n["label"] for n in r["nodes"]} + assert "mode" not in labels + assert "label" not in labels + + # And no references edge may target a placeholder minted from a name. + for e in r["edges"]: + if e["relation"] != "references": + continue + target = next(n for n in r["nodes"] if n["id"] == e["target"]) + assert target["label"] not in ("mode", "label"), ( + f"references edge to tuple element name {target['label']}" + )