diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/csharp-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/csharp-extractor.test.ts index e1534b8d1..2b3a4bb1a 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/csharp-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/csharp-extractor.test.ts @@ -302,6 +302,72 @@ namespace App { tree.delete(); parser.delete(); }); + + it("extracts aliased using with a simple-identifier target", () => { + const { tree, parser, root } = parse(`using Alias = System; +namespace App { public class C {} } +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toEqual([ + { source: "System", specifiers: ["System"], lineNumber: 1 }, + ]); + + tree.delete(); + parser.delete(); + }); + + it("extracts a global using directive", () => { + const { tree, parser, root } = parse(`global using System.Collections.Generic; +namespace App { public class C {} } +`); + const result = extractor.extractStructure(root); + + // tree-sitter-c-sharp parses `global using` as a `using_directive` + // carrying a `global` modifier child, so the existing handler applies. + expect(result.imports).toEqual([ + { + source: "System.Collections.Generic", + specifiers: ["Generic"], + lineNumber: 1, + }, + ]); + + tree.delete(); + parser.delete(); + }); + + it("extracts a global using with a simple-identifier alias target", () => { + const { tree, parser, root } = parse(`global using Alias = System; +namespace App { public class C {} } +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toEqual([ + { source: "System", specifiers: ["System"], lineNumber: 1 }, + ]); + + tree.delete(); + parser.delete(); + }); + + it("extracts a global using with a qualified alias target", () => { + const { tree, parser, root } = parse(`global using Alias = System.Collections.Generic; +namespace App { public class C {} } +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toEqual([ + { + source: "System.Collections.Generic", + specifiers: ["Generic"], + lineNumber: 1, + }, + ]); + + tree.delete(); + parser.delete(); + }); }); // ---- Exports ---- @@ -452,6 +518,52 @@ namespace App { parser.delete(); }); + it("extracts object creation of a qualified type", () => { + const { tree, parser, root } = parse(`namespace App { public class C { public void M() { var x = new System.Text.StringBuilder(); } } }`); + const result = extractor.extractCallGraph(root); + + expect(result).toContainEqual({ + caller: "M", + callee: "new System.Text.StringBuilder", + lineNumber: 1, + }); + + tree.delete(); + parser.delete(); + }); + + it("extracts object creation of a generic type", () => { + const { tree, parser, root } = parse(`namespace App { public class C { public void M() { var x = new List(); } } }`); + const result = extractor.extractCallGraph(root); + + // The type node is a `generic_name`; its text includes the type arguments. + expect(result).toContainEqual({ + caller: "M", + callee: "new List", + lineNumber: 1, + }); + + tree.delete(); + parser.delete(); + }); + + it("extracts object creation of a qualified generic type", () => { + const { tree, parser, root } = parse(`namespace App { public class C { public void M() { var x = new System.Collections.Generic.List(); } } }`); + const result = extractor.extractCallGraph(root); + + // tree-sitter-c-sharp shapes this as a single `qualified_name` whose + // trailing segment is a `generic_name`, so the node text carries the + // full dotted path including the type arguments. + expect(result).toContainEqual({ + caller: "M", + callee: "new System.Collections.Generic.List", + lineNumber: 1, + }); + + tree.delete(); + parser.delete(); + }); + it("tracks correct caller for constructors", () => { const { tree, parser, root } = parse(`namespace App { public class Foo { diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/csharp-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/csharp-extractor.ts index 19b77b5b9..85e6d45c0 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/csharp-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/csharp-extractor.ts @@ -59,24 +59,47 @@ function hasModifier(node: TreeSitterNode, modifier: string): boolean { * * Handles both simple identifiers (`using System;`) and qualified names * (`using System.Collections.Generic;`). For aliased usings like - * `using Alias = Some.Namespace;`, extracts the target namespace. + * `using Alias = Some.Namespace;`, extracts the target namespace (the part + * after the `=`, not the alias). `global using` forms parse as a + * `using_directive` with a leading `global` child, so they flow through the + * same single pass below. */ function extractUsingSource(node: TreeSitterNode): string | null { - // Check for alias form: `using Alias = Some.Namespace;` - const hasEquals = findChild(node, "=") !== null; + // A single pass over the children covers every using shape: + // `using System;` -> first identifier + // `using System.Collections.Generic;` -> qualified_name + // `using Alias = System;` -> identifier AFTER the `=` + // `using Alias = System.Collections.X;` -> qualified_name AFTER the `=` + // The alias name (before `=`) is intentionally skipped; `source` is always + // the target namespace. + let target: string | null = null; + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child) continue; + + if (child.type === "=") { + // Discard anything seen before the `=` (that was the alias name) and + // resume scanning for the real target after it. + target = null; + continue; + } - if (hasEquals) { - // The target namespace is the qualified_name after the `=` - const qualifiedName = findChild(node, "qualified_name"); - return qualifiedName ? qualifiedName.text : null; - } + if (child.type === "qualified_name") { + // qualified_name is unambiguous: take it immediately whether or not we + // are in an alias directive. + return child.text; + } - // Simple or qualified using - const qualifiedName = findChild(node, "qualified_name"); - if (qualifiedName) return qualifiedName.text; + if (child.type === "identifier" && target === null) { + // First identifier (the post-`=` one in the alias case). For a simple + // target this is the answer; we keep scanning in case a qualified_name + // follows (it would win above). + target = child.text; + } + } - const identifier = findChild(node, "identifier"); - return identifier ? identifier.text : null; + return target; } /** @@ -173,8 +196,12 @@ export class CSharpExtractor implements LanguageExtractor { // Extract object creation: e.g. new Foo() if (node.type === "object_creation_expression") { if (functionStack.length > 0) { - // The type is the child after `new` — can be identifier or generic_name - const typeNode = findChild(node, "identifier") ?? findChild(node, "generic_name"); + // The type is the child after `new` — can be identifier, generic_name, + // or a qualified_name (e.g. `new System.Text.StringBuilder()`). + const typeNode = + findChild(node, "identifier") ?? + findChild(node, "generic_name") ?? + findChild(node, "qualified_name"); if (typeNode) { entries.push({ caller: functionStack[functionStack.length - 1], @@ -219,6 +246,10 @@ export class CSharpExtractor implements LanguageExtractor { switch (child.type) { case "using_directive": + // Covers `using X;`, `using X.Y;`, `using A = X;`, and the C# 10 + // `global using ...` forms — tree-sitter-c-sharp parses the latter + // as a `using_directive` with a leading `global` modifier child + // rather than a distinct node type. this.extractUsing(child, imports); break;