From 0a351eaa374fcac4ac077e765c36a30c12a5e8eb Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Mon, 19 Oct 2026 00:25:42 +0530 Subject: [PATCH] fix(csharp): extract delegate declarations so references don't mint sourceless stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A C# `delegate` is a named type, but `delegate_declaration` was in neither `class_types` nor `function_types`, so the declaration was dropped from the graph. A field or parameter typed as the delegate (`public BinOp Op;`, `int Apply(BinOp f, ...)`) still resolved the name `BinOp` through `ensure_named_node`, which minted a *sourceless* stub and pointed the `references` edge at it — even though `BinOp` is declared in the same file. That violates the provenance invariant: a real source-backed declaration must not lose to a sourceless god node (and the bare-id stub becomes a cross-file hub shared by every same-named reference in the corpus). `delegate_declaration` has a `name` field and no body, so adding it to the C# `class_types` emits a clean empty type node (no phantom members) that the in-file references now bind to. Namespaced delegates are scoped and contained exactly like the sibling class/enum/struct declarations. Delegate parameter/return type references and delegate-instance invocation resolution are unchanged (out of scope). --- graphify/extract.py | 7 +++ tests/test_csharp_delegate_declarations.py | 70 ++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tests/test_csharp_delegate_declarations.py diff --git a/graphify/extract.py b/graphify/extract.py index 0fe689033..76ae2428d 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1520,6 +1520,13 @@ _CSHARP_CONFIG = LanguageConfig( "enum_declaration", "struct_declaration", "record_declaration", + # A `delegate` is a named type. Without it the declaration was dropped, + # yet in-file references to the delegate type (a `BinOp` field/parameter) + # still ran through `ensure_named_node` and minted a *sourceless* stub — + # a provenance violation (a real in-file type losing to a sourceless god + # node). It has a `name` field and no body, so it emits a clean empty + # type node that those references bind to. + "delegate_declaration", }), function_types=frozenset({"method_declaration"}), import_types=frozenset({"using_directive"}), diff --git a/tests/test_csharp_delegate_declarations.py b/tests/test_csharp_delegate_declarations.py new file mode 100644 index 000000000..21a7353c4 --- /dev/null +++ b/tests/test_csharp_delegate_declarations.py @@ -0,0 +1,70 @@ +"""A C# `delegate` is a named type and must be extracted as a sourced node. + +`public delegate int BinOp(int a, int b);` was in neither `class_types` nor +`function_types`, so the declaration was dropped. But a field or parameter typed +as the delegate (`BinOp Op;`) still ran the name `BinOp` through +`ensure_named_node`, which minted a *sourceless* stub node and pointed the +`references` edge at it — even though `BinOp` is declared in the very same file. +That is a provenance violation (a real source-backed declaration losing to a +sourceless god node). Adding `delegate_declaration` to the C# `class_types` emits +a clean empty type node (it has a `name` field and no body) that the in-file +references bind to instead. +""" +from __future__ import annotations + +from pathlib import Path + +from graphify.extract import extract + + +def _extract(tmp_path, body: str): + (tmp_path / "d.cs").write_text(body) + return extract([tmp_path / "d.cs"], cache_root=tmp_path, parallel=False) + + +def _node(r, label): + return next((n for n in r["nodes"] if n.get("label") == label), None) + + +def test_delegate_is_a_sourced_node_not_a_sourceless_stub(tmp_path): + r = _extract( + tmp_path, + "public delegate int BinOp(int a, int b);\n" + "public class Calc {\n" + " public BinOp Op;\n" + " public int Apply(BinOp f, int x) { return f(x, x); }\n" + "}\n", + ) + binop = _node(r, "BinOp") + assert binop is not None, "delegate declaration was dropped" + assert binop.get("source_file"), ( + f"delegate BinOp is a sourceless god node: {binop}" + ) + # No sourceless node is left over for a type that is declared in-file. + assert not any( + (not n.get("source_file")) and n.get("label") == "BinOp" + for n in r["nodes"] + ) + # The field/parameter references now bind to the real sourced delegate node. + refs_to_binop = { + e["source"] + for e in r["edges"] + if e["relation"] == "references" and e["target"] == binop["id"] + } + assert refs_to_binop, "references to the delegate did not bind to it" + + +def test_namespaced_delegate_is_scoped_and_sourced(tmp_path): + r = _extract( + tmp_path, + "namespace App {\n" + " public delegate void Handler(string msg);\n" + " public class Bus { public Handler OnMsg; }\n" + "}\n", + ) + handler = _node(r, "Handler") + assert handler is not None and handler.get("source_file") + assert any( + e["relation"] == "references" and e["target"] == handler["id"] + for e in r["edges"] + )