From cd04bf1bf3696d80be94e613ec45d209b4172fcb Mon Sep 17 00:00:00 2001 From: rohit-jsfreaky Date: Tue, 11 Aug 2026 20:09:59 +0100 Subject: [PATCH] fix(csharp): extract members inside #if preprocessor blocks (#2634) --- graphify/extractors/engine.py | 9 ++++++- tests/test_dotnet.py | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 08865096..2b3433cf 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -2162,7 +2162,7 @@ def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: nodes: list, edges: list, seen_ids: set, function_bodies: list, parent_class_nid: str | None, add_node_fn, add_edge_fn, walk_fn, namespace_stack: list[str], scope_stack: list[str]) -> bool: - """Handle namespace declarations for C#. Returns True if handled.""" + """Handle C# namespaces and transparent class-member wrappers.""" if node.type == "namespace_declaration": ns_name = _csharp_namespace_name(node, source) pushed = False @@ -2199,6 +2199,13 @@ def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: add_node_fn(ns_nid, ns_label, line, node_type="namespace", metadata={"kind": "csharp_namespace"}) add_edge_fn(file_nid, ns_nid, "contains", line) return True + if parent_class_nid and node.type.startswith("preproc_"): + # tree-sitter wraps members in #if/#else/#elif directives in preproc_* + # nodes. They are conditional containers, not ownership scopes: dropping + # parent_class_nid here makes guarded methods look file-level (#2631). + for child in node.children: + walk_fn(child, parent_class_nid) + return True return False def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, diff --git a/tests/test_dotnet.py b/tests/test_dotnet.py index ec48f8b7..eb11467e 100644 --- a/tests/test_dotnet.py +++ b/tests/test_dotnet.py @@ -519,6 +519,56 @@ def test_xaml_viewmodel_with_non_utf8_codebehind_does_not_crash(tmp_path): assert nodes[edges[0]["target"]]["label"] == "SettingsViewModel" +def test_csharp_members_in_preprocessor_blocks_are_extracted_and_resolved(tmp_path): + """C# preprocessor wrappers must preserve class ownership for members (#2631).""" + helper = tmp_path / "Helper.cs" + helper.write_text( + """namespace Probe.Lib; +public static class Gated +{ + public static void Outside(string a) { } +#if NET8_0_OR_GREATER + public static void InsideIf(string a) { } +#else + public static void InsideElse(string a) { } +#endif +#if DEBUG + public static void InsideDebug(string a) { } +#endif +} +""" + ) + caller = tmp_path / "Caller.cs" + caller.write_text( + """namespace Probe.Lib; +public static class Caller +{ + public static void Drive() + { + Gated.Outside(\"x\"); + Gated.InsideIf(\"x\"); + Gated.InsideDebug(\"x\"); + } +} +""" + ) + + result = extract([helper, caller], cache_root=tmp_path) + by_label = {node["label"]: node["id"] for node in result["nodes"]} + + for label in (".Outside()", ".InsideIf()", ".InsideElse()", ".InsideDebug()"): + assert label in by_label + + calls = { + (edge["source"], edge["target"]) + for edge in result["edges"] + if edge["relation"] == "calls" + } + assert (by_label[".Drive()"], by_label[".Outside()"]) in calls + assert (by_label[".Drive()"], by_label[".InsideIf()"]) in calls + assert (by_label[".Drive()"], by_label[".InsideDebug()"]) in calls + + # ── .razor ─────────────────────────────────────────────────────────────────── def test_razor_using_and_inject():