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 new file mode 100644 index 0000000..e1534b8 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/csharp-extractor.test.ts @@ -0,0 +1,665 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { createRequire } from "node:module"; +import { CSharpExtractor } from "../csharp-extractor.js"; + +const require = createRequire(import.meta.url); + +// Load tree-sitter + C# grammar once +let Parser: any; +let Language: any; +let csharpLang: any; + +beforeAll(async () => { + const mod = await import("web-tree-sitter"); + Parser = mod.Parser; + Language = mod.Language; + await Parser.init(); + const wasmPath = require.resolve( + "tree-sitter-c-sharp/tree-sitter-c_sharp.wasm", + ); + csharpLang = await Language.load(wasmPath); +}); + +function parse(code: string) { + const parser = new Parser(); + parser.setLanguage(csharpLang); + const tree = parser.parse(code); + const root = tree.rootNode; + return { tree, parser, root }; +} + +describe("CSharpExtractor", () => { + const extractor = new CSharpExtractor(); + + it("has correct languageIds", () => { + expect(extractor.languageIds).toEqual(["csharp"]); + }); + + // ---- Methods/Constructors (mapped to functions) ---- + + describe("extractStructure - functions (methods & constructors)", () => { + it("extracts methods with params and return types", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public string GetName(int id) { + return ""; + } + private void Process(string data, int count) { + } + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(2); + + expect(result.functions[0].name).toBe("GetName"); + expect(result.functions[0].params).toEqual(["id"]); + expect(result.functions[0].returnType).toBe("string"); + + expect(result.functions[1].name).toBe("Process"); + expect(result.functions[1].params).toEqual(["data", "count"]); + expect(result.functions[1].returnType).toBe("void"); + + tree.delete(); + parser.delete(); + }); + + it("extracts constructors", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public Foo(string name, int value) { + this.name = name; + } + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("Foo"); + expect(result.functions[0].params).toEqual(["name", "value"]); + expect(result.functions[0].returnType).toBeUndefined(); + + tree.delete(); + parser.delete(); + }); + + it("extracts methods with no params", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public void Run() {} + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("Run"); + expect(result.functions[0].params).toEqual([]); + expect(result.functions[0].returnType).toBe("void"); + + tree.delete(); + parser.delete(); + }); + + it("extracts methods with generic return types", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public List GetItems() { + return null; + } + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("GetItems"); + expect(result.functions[0].returnType).toBe("List"); + + tree.delete(); + parser.delete(); + }); + + it("reports correct line ranges for multi-line methods", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public int Calculate( + int a, + int b + ) { + int result = a + b; + return result; + } + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].lineRange[0]).toBe(3); + expect(result.functions[0].lineRange[1]).toBe(9); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Classes ---- + + describe("extractStructure - classes", () => { + it("extracts class with methods, properties, and fields", () => { + const { tree, parser, root } = parse(`namespace App { + public class Server { + private string _host; + private int _port; + public string Address { get; set; } + public void Start() {} + public void Stop() {} + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Server"); + expect(result.classes[0].properties).toEqual(["_host", "_port", "Address"]); + expect(result.classes[0].methods).toEqual(["Start", "Stop"]); + expect(result.classes[0].lineRange[0]).toBe(2); + + tree.delete(); + parser.delete(); + }); + + it("extracts empty class", () => { + const { tree, parser, root } = parse(`namespace App { + public class Empty { + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Empty"); + expect(result.classes[0].properties).toEqual([]); + expect(result.classes[0].methods).toEqual([]); + + tree.delete(); + parser.delete(); + }); + + it("includes constructors in methods list", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public Foo() {} + public void Run() {} + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toEqual(["Foo", "Run"]); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Interfaces ---- + + describe("extractStructure - interfaces", () => { + it("extracts interface with method signatures", () => { + const { tree, parser, root } = parse(`namespace App { + interface IRepository { + List FindAll(); + User FindById(int id); + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("IRepository"); + expect(result.classes[0].methods).toEqual(["FindAll", "FindById"]); + expect(result.classes[0].properties).toEqual([]); + + tree.delete(); + parser.delete(); + }); + + it("extracts empty interface", () => { + const { tree, parser, root } = parse(`namespace App { + interface IMarker { + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("IMarker"); + expect(result.classes[0].methods).toEqual([]); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Imports (using directives) ---- + + describe("extractStructure - imports", () => { + it("extracts simple using directives", () => { + const { tree, parser, root } = parse(`using System; +namespace App { + public class Foo {} +} +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(1); + expect(result.imports[0].source).toBe("System"); + expect(result.imports[0].specifiers).toEqual(["System"]); + expect(result.imports[0].lineNumber).toBe(1); + + tree.delete(); + parser.delete(); + }); + + it("extracts qualified using directives", () => { + const { tree, parser, root } = parse(`using System; +using System.Collections.Generic; +namespace App { + public class Foo {} +} +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("System"); + expect(result.imports[0].specifiers).toEqual(["System"]); + expect(result.imports[0].lineNumber).toBe(1); + expect(result.imports[1].source).toBe("System.Collections.Generic"); + expect(result.imports[1].specifiers).toEqual(["Generic"]); + expect(result.imports[1].lineNumber).toBe(2); + + tree.delete(); + parser.delete(); + }); + + it("reports correct import line numbers with gaps", () => { + const { tree, parser, root } = parse(`using System; + +using System.Linq; +namespace App { + public class Foo {} +} +`); + const result = extractor.extractStructure(root); + + expect(result.imports[0].lineNumber).toBe(1); + expect(result.imports[1].lineNumber).toBe(3); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Exports ---- + + describe("extractStructure - exports", () => { + it("exports public class, methods, constructor, and properties", () => { + const { tree, parser, root } = parse(`namespace App { + public class UserService { + private string _name; + public int MaxRetries { get; set; } + public UserService(string name) { + _name = name; + } + public void Start() {} + private void Helper() {} + } +} +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("UserService"); // class + // Constructor is also named UserService + const userServiceExports = result.exports.filter( + (e) => e.name === "UserService", + ); + expect(userServiceExports.length).toBe(2); // class + constructor + expect(exportNames).toContain("MaxRetries"); // public property + expect(exportNames).toContain("Start"); + expect(exportNames).not.toContain("Helper"); + expect(exportNames).not.toContain("_name"); // private field + + tree.delete(); + parser.delete(); + }); + + it("does not export non-public classes", () => { + const { tree, parser, root } = parse(`namespace App { + class Internal { + void Run() {} + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.exports).toHaveLength(0); + + tree.delete(); + parser.delete(); + }); + + it("exports public fields", () => { + const { tree, parser, root } = parse(`namespace App { + public class Config { + public string ApiKey; + private int _retries; + } +} +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("Config"); + expect(exportNames).toContain("ApiKey"); + expect(exportNames).not.toContain("_retries"); + + tree.delete(); + parser.delete(); + }); + + it("exports public interface", () => { + const { tree, parser, root } = parse(`namespace App { + public interface IRepository { + void Save(); + } +} +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("IRepository"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Call Graph ---- + + describe("extractCallGraph", () => { + it("extracts simple method calls", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public void Process(int data) { + Transform(data); + Format(data); + } + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(2); + expect(result[0].caller).toBe("Process"); + expect(result[0].callee).toBe("Transform"); + expect(result[1].caller).toBe("Process"); + expect(result[1].callee).toBe("Format"); + + tree.delete(); + parser.delete(); + }); + + it("extracts qualified method calls (e.g. Console.WriteLine)", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + private void Log(string message) { + Console.WriteLine(message); + } + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(1); + expect(result[0].caller).toBe("Log"); + expect(result[0].callee).toBe("Console.WriteLine"); + + tree.delete(); + parser.delete(); + }); + + it("extracts object creation expressions", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public void Create() { + var b = new Bar(); + } + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(1); + expect(result[0].caller).toBe("Create"); + expect(result[0].callee).toBe("new Bar"); + + tree.delete(); + parser.delete(); + }); + + it("tracks correct caller for constructors", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public Foo() { + Init(); + } + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(1); + expect(result[0].caller).toBe("Foo"); + expect(result[0].callee).toBe("Init"); + + tree.delete(); + parser.delete(); + }); + + it("reports correct line numbers for calls", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + public void Run() { + Foo(); + Bar(); + } + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(2); + expect(result[0].lineNumber).toBe(4); + expect(result[1].lineNumber).toBe(5); + + tree.delete(); + parser.delete(); + }); + + it("ignores calls outside methods (no caller)", () => { + const { tree, parser, root } = parse(`namespace App { + public class Foo { + private string _value = String.Empty; + } +} +`); + const result = extractor.extractCallGraph(root); + + // No enclosing method, so these are skipped + expect(result).toHaveLength(0); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Namespace handling ---- + + describe("namespace handling", () => { + it("extracts declarations from block-scoped namespace", () => { + const { tree, parser, root } = parse(`namespace App.Services { + public class Svc { + public void Run() {} + } +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Svc"); + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("Run"); + + tree.delete(); + parser.delete(); + }); + + it("extracts declarations alongside file-scoped namespace", () => { + const { tree, parser, root } = parse(`namespace App.Services; + +public class Svc { + public void Run() {} +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Svc"); + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("Run"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Comprehensive ---- + + describe("comprehensive C# file", () => { + it("handles a realistic C# module", () => { + const { tree, parser, root } = parse(`using System; +using System.Collections.Generic; + +namespace App.Services +{ + public class UserService + { + private string _name; + public int MaxRetries { get; set; } + + public UserService(string name) + { + _name = name; + } + + public List GetUsers(int limit) + { + return FetchFromDb(limit); + } + + private void Log(string message) + { + Console.WriteLine(message); + } + } + + public interface IRepository + { + List FindAll(); + User FindById(int id); + } +} +`); + const result = extractor.extractStructure(root); + + // Functions: UserService (constructor), GetUsers, Log + expect(result.functions).toHaveLength(3); + expect(result.functions.map((f) => f.name).sort()).toEqual( + ["GetUsers", "Log", "UserService"].sort(), + ); + + // Constructor has params but no return type + const ctor = result.functions.find((f) => f.name === "UserService"); + expect(ctor?.params).toEqual(["name"]); + expect(ctor?.returnType).toBeUndefined(); + + // GetUsers has params and generic return type + const getUsers = result.functions.find((f) => f.name === "GetUsers"); + expect(getUsers?.params).toEqual(["limit"]); + expect(getUsers?.returnType).toBe("List"); + + // Log has params and void return type + const log = result.functions.find((f) => f.name === "Log"); + expect(log?.params).toEqual(["message"]); + expect(log?.returnType).toBe("void"); + + // Classes: UserService, IRepository + expect(result.classes).toHaveLength(2); + + const userService = result.classes.find( + (c) => c.name === "UserService", + ); + expect(userService).toBeDefined(); + expect(userService!.methods.sort()).toEqual( + ["GetUsers", "Log", "UserService"].sort(), + ); + expect(userService!.properties.sort()).toEqual( + ["MaxRetries", "_name"].sort(), + ); + + const repository = result.classes.find( + (c) => c.name === "IRepository", + ); + expect(repository).toBeDefined(); + expect(repository!.methods).toEqual(["FindAll", "FindById"]); + expect(repository!.properties).toEqual([]); + + // Imports: 2 (System, System.Collections.Generic) + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("System"); + expect(result.imports[0].specifiers).toEqual(["System"]); + expect(result.imports[1].source).toBe("System.Collections.Generic"); + expect(result.imports[1].specifiers).toEqual(["Generic"]); + + // Exports: UserService (class + constructor), MaxRetries, GetUsers, IRepository + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("UserService"); + expect(exportNames).toContain("GetUsers"); + expect(exportNames).toContain("MaxRetries"); + expect(exportNames).toContain("IRepository"); + expect(exportNames).not.toContain("Log"); // private + expect(exportNames).not.toContain("_name"); // private field + + // Call graph + const calls = extractor.extractCallGraph(root); + + const getUsersCalls = calls.filter((e) => e.caller === "GetUsers"); + expect(getUsersCalls.some((e) => e.callee === "FetchFromDb")).toBe( + true, + ); + + const logCalls = calls.filter((e) => e.caller === "Log"); + expect( + logCalls.some((e) => e.callee === "Console.WriteLine"), + ).toBe(true); + + tree.delete(); + parser.delete(); + }); + }); +}); 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 new file mode 100644 index 0000000..19b77b5 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/csharp-extractor.ts @@ -0,0 +1,524 @@ +import type { StructuralAnalysis, CallGraphEntry } from "../../types.js"; +import type { LanguageExtractor, TreeSitterNode } from "./types.js"; +import { findChild, findChildren } from "./base-extractor.js"; + +/** + * Extract parameter names from a C# `parameter_list` node. + * + * Each `parameter` child has a `name` field (identifier) and a `type` field. + */ +function extractParams(paramsNode: TreeSitterNode | null): string[] { + if (!paramsNode) return []; + const params: string[] = []; + + const paramNodes = findChildren(paramsNode, "parameter"); + for (const param of paramNodes) { + const nameNode = param.childForFieldName("name"); + if (nameNode) { + params.push(nameNode.text); + } + } + + return params; +} + +/** + * Extract the return type text from a method_declaration node. + * + * In tree-sitter-c-sharp, the return type is the `returns` named field. + * It can be a predefined_type (void, int, string), generic_name (List), + * identifier, nullable_type, etc. + */ +function extractReturnType(node: TreeSitterNode): string | undefined { + const typeNode = node.childForFieldName("returns"); + if (!typeNode) return undefined; + return typeNode.text; +} + +/** + * Check if a C# declaration node has a specific modifier. + * + * Unlike Java (which has a single `modifiers` container), C# tree-sitter + * emits multiple separate `modifier` nodes as direct children of the + * declaration. Each modifier node contains a single keyword child + * (e.g., `public`, `private`, `static`). + */ +function hasModifier(node: TreeSitterNode, modifier: string): boolean { + const modifierNodes = findChildren(node, "modifier"); + for (const mod of modifierNodes) { + for (let i = 0; i < mod.childCount; i++) { + const child = mod.child(i); + if (child && child.text === modifier) return true; + } + } + return false; +} + +/** + * Extract the namespace source text from a using_directive. + * + * 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. + */ +function extractUsingSource(node: TreeSitterNode): string | null { + // Check for alias form: `using Alias = Some.Namespace;` + const hasEquals = findChild(node, "=") !== null; + + if (hasEquals) { + // The target namespace is the qualified_name after the `=` + const qualifiedName = findChild(node, "qualified_name"); + return qualifiedName ? qualifiedName.text : null; + } + + // Simple or qualified using + const qualifiedName = findChild(node, "qualified_name"); + if (qualifiedName) return qualifiedName.text; + + const identifier = findChild(node, "identifier"); + return identifier ? identifier.text : null; +} + +/** + * Get the last component of a dotted namespace path. + * e.g. "System.Collections.Generic" -> "Generic" + */ +function lastComponent(path: string): string { + const parts = path.split("."); + return parts[parts.length - 1]; +} + +/** + * Extract the callee name from an invocation_expression node. + * + * Handles: + * - Plain method call: `FetchFromDb(limit)` -> "FetchFromDb" + * (function field is an identifier) + * - Qualified call: `Console.WriteLine(msg)` -> "Console.WriteLine" + * (function field is a member_access_expression) + */ +function extractInvocationName(node: TreeSitterNode): string | null { + const funcNode = node.childForFieldName("function"); + if (!funcNode) return null; + return funcNode.text; +} + +/** + * C# extractor for tree-sitter structural analysis and call graph extraction. + * + * Handles classes, interfaces, methods, constructors, properties, fields, + * using directives, visibility-based exports, and call graphs for C# source code. + * + * C#-specific mapping decisions: + * - Classes and interfaces are mapped to the `classes` array. + * - Constructors are mapped to the `functions` array (named after the class). + * - Methods (including interface method signatures) are listed in the + * containing class/interface's `methods` array and also in the `functions` array. + * - Properties (e.g., `public string Name { get; set; }`) are extracted into + * the containing class's `properties` array alongside fields. + * - Exports are determined by the `public` modifier on classes, interfaces, + * methods, constructors, properties, and fields. + * - Namespaces: both block-scoped (`namespace Foo { ... }`) and file-scoped + * (`namespace Foo;`) are traversed to find declarations. + * - Using directives are mapped to imports, with the last dotted component + * as the specifier. + */ +export class CSharpExtractor implements LanguageExtractor { + readonly languageIds = ["csharp"]; + + extractStructure(rootNode: TreeSitterNode): StructuralAnalysis { + const functions: StructuralAnalysis["functions"] = []; + const classes: StructuralAnalysis["classes"] = []; + const imports: StructuralAnalysis["imports"] = []; + const exports: StructuralAnalysis["exports"] = []; + + this.walkTopLevel(rootNode, functions, classes, imports, exports); + + return { functions, classes, imports, exports }; + } + + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { + const entries: CallGraphEntry[] = []; + const functionStack: string[] = []; + + const walkForCalls = (node: TreeSitterNode) => { + let pushedName = false; + + // Track entering method/constructor declarations + if ( + node.type === "method_declaration" || + node.type === "constructor_declaration" + ) { + const nameNode = node.childForFieldName("name"); + if (nameNode) { + functionStack.push(nameNode.text); + pushedName = true; + } + } + + // Extract method invocations: e.g. FetchFromDb(limit), Console.WriteLine(msg) + if (node.type === "invocation_expression") { + if (functionStack.length > 0) { + const callee = extractInvocationName(node); + if (callee) { + entries.push({ + caller: functionStack[functionStack.length - 1], + callee, + lineNumber: node.startPosition.row + 1, + }); + } + } + } + + // 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"); + if (typeNode) { + entries.push({ + caller: functionStack[functionStack.length - 1], + callee: `new ${typeNode.text}`, + lineNumber: node.startPosition.row + 1, + }); + } + } + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkForCalls(child); + } + + if (pushedName) { + functionStack.pop(); + } + }; + + walkForCalls(rootNode); + + return entries; + } + + // ---- Private helpers ---- + + /** + * Walk the top-level nodes of a compilation_unit, recursing into + * namespace bodies to find declarations. + */ + private walkTopLevel( + node: TreeSitterNode, + functions: StructuralAnalysis["functions"], + classes: StructuralAnalysis["classes"], + imports: StructuralAnalysis["imports"], + exports: StructuralAnalysis["exports"], + ): void { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child) continue; + + switch (child.type) { + case "using_directive": + this.extractUsing(child, imports); + break; + + case "namespace_declaration": + // Recurse into namespace body (declaration_list) + this.walkNamespaceBody(child, functions, classes, imports, exports); + break; + + case "file_scoped_namespace_declaration": + // File-scoped namespace: declarations are siblings at the root, + // not children of this node. Nothing to recurse into. + break; + + case "class_declaration": + this.extractClass(child, functions, classes, exports); + break; + + case "interface_declaration": + this.extractInterface(child, functions, classes, exports); + break; + } + } + } + + /** + * Walk into a namespace_declaration's body (declaration_list) to find + * classes, interfaces, and nested namespaces. + */ + private walkNamespaceBody( + nsNode: TreeSitterNode, + functions: StructuralAnalysis["functions"], + classes: StructuralAnalysis["classes"], + imports: StructuralAnalysis["imports"], + exports: StructuralAnalysis["exports"], + ): void { + const body = nsNode.childForFieldName("body"); + if (!body) return; + + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (!child) continue; + + switch (child.type) { + case "class_declaration": + this.extractClass(child, functions, classes, exports); + break; + + case "interface_declaration": + this.extractInterface(child, functions, classes, exports); + break; + + case "namespace_declaration": + // Nested namespaces + this.walkNamespaceBody(child, functions, classes, imports, exports); + break; + } + } + } + + private extractUsing( + node: TreeSitterNode, + imports: StructuralAnalysis["imports"], + ): void { + const source = extractUsingSource(node); + if (!source) return; + + imports.push({ + source, + specifiers: [lastComponent(source)], + lineNumber: node.startPosition.row + 1, + }); + } + + private extractClass( + node: TreeSitterNode, + functions: StructuralAnalysis["functions"], + classes: StructuralAnalysis["classes"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = node.childForFieldName("name"); + if (!nameNode) return; + + const methods: string[] = []; + const properties: string[] = []; + + const body = node.childForFieldName("body"); + if (body) { + this.extractClassBodyMembers(body, methods, properties, functions, exports); + } + + classes.push({ + name: nameNode.text, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + methods, + properties, + }); + + if (hasModifier(node, "public")) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + + private extractInterface( + node: TreeSitterNode, + functions: StructuralAnalysis["functions"], + classes: StructuralAnalysis["classes"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = node.childForFieldName("name"); + if (!nameNode) return; + + const methods: string[] = []; + const properties: string[] = []; + + const body = node.childForFieldName("body"); + if (body) { + // Interface body contains method_declaration nodes (signatures without bodies) + const methodNodes = findChildren(body, "method_declaration"); + for (const methodNode of methodNodes) { + const methNameNode = methodNode.childForFieldName("name"); + if (methNameNode) { + methods.push(methNameNode.text); + } + } + + // Interface can contain property_declaration nodes + const propNodes = findChildren(body, "property_declaration"); + for (const propNode of propNodes) { + const propNameNode = propNode.childForFieldName("name"); + if (propNameNode) { + properties.push(propNameNode.text); + } + } + } + + classes.push({ + name: nameNode.text, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + methods, + properties, + }); + + if (hasModifier(node, "public")) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + + /** + * Extract methods, constructors, properties, and fields from a + * class declaration_list body. + */ + private extractClassBodyMembers( + body: TreeSitterNode, + methods: string[], + properties: string[], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (!child) continue; + + switch (child.type) { + case "method_declaration": + this.extractMethod(child, methods, functions, exports); + break; + + case "constructor_declaration": + this.extractConstructor(child, methods, functions, exports); + break; + + case "property_declaration": + this.extractProperty(child, properties, exports); + break; + + case "field_declaration": + this.extractField(child, properties, exports); + break; + } + } + } + + private extractMethod( + node: TreeSitterNode, + methods: string[], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = node.childForFieldName("name"); + if (!nameNode) return; + + const paramsNode = node.childForFieldName("parameters"); + const params = extractParams(paramsNode ?? null); + const returnType = extractReturnType(node); + + methods.push(nameNode.text); + + functions.push({ + name: nameNode.text, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + params, + returnType, + }); + + if (hasModifier(node, "public")) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + + private extractConstructor( + node: TreeSitterNode, + methods: string[], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = node.childForFieldName("name"); + if (!nameNode) return; + + const paramsNode = node.childForFieldName("parameters"); + const params = extractParams(paramsNode ?? null); + + methods.push(nameNode.text); + + functions.push({ + name: nameNode.text, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + params, + // Constructors have no return type + }); + + if (hasModifier(node, "public")) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + + private extractProperty( + node: TreeSitterNode, + properties: string[], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = node.childForFieldName("name"); + if (!nameNode) return; + + properties.push(nameNode.text); + + if (hasModifier(node, "public")) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + + private extractField( + node: TreeSitterNode, + properties: string[], + exports: StructuralAnalysis["exports"], + ): void { + // field_declaration -> variable_declaration -> variable_declarator(s) + const varDecl = findChild(node, "variable_declaration"); + if (!varDecl) return; + + const declarators = findChildren(varDecl, "variable_declarator"); + for (const decl of declarators) { + // variable_declarator's first child is the identifier + const nameNode = findChild(decl, "identifier"); + if (nameNode) { + properties.push(nameNode.text); + + if (hasModifier(node, "public")) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + } + } +}