From 6e500cbfdf076aa5de37a75e31f79ff3f09799f0 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Wed, 15 Apr 2026 18:42:55 +0800 Subject: [PATCH] feat: add JavaExtractor for tree-sitter Java structural analysis Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__tests__/java-extractor.test.ts | 568 ++++++++++++++++++ .../src/plugins/extractors/java-extractor.ts | 449 ++++++++++++++ 2 files changed, 1017 insertions(+) create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/java-extractor.test.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/java-extractor.ts diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/java-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/java-extractor.test.ts new file mode 100644 index 0000000..17d14ad --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/java-extractor.test.ts @@ -0,0 +1,568 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { createRequire } from "node:module"; +import { JavaExtractor } from "../java-extractor.js"; + +const require = createRequire(import.meta.url); + +// Load tree-sitter + Java grammar once +let Parser: any; +let Language: any; +let javaLang: 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-java/tree-sitter-java.wasm", + ); + javaLang = await Language.load(wasmPath); +}); + +function parse(code: string) { + const parser = new Parser(); + parser.setLanguage(javaLang); + const tree = parser.parse(code); + const root = tree.rootNode; + return { tree, parser, root }; +} + +describe("JavaExtractor", () => { + const extractor = new JavaExtractor(); + + it("has correct languageIds", () => { + expect(extractor.languageIds).toEqual(["java"]); + }); + + // ---- Methods/Constructors (mapped to functions) ---- + + describe("extractStructure - functions (methods & constructors)", () => { + it("extracts methods with params and return types", () => { + const { tree, parser, root } = parse(`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(`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(`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(`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(`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(2); + expect(result.functions[0].lineRange[1]).toBe(8); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Classes ---- + + describe("extractStructure - classes", () => { + it("extracts class with methods and fields", () => { + const { tree, parser, root } = parse(`public class Server { + private String host; + private int port; + 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"]); + expect(result.classes[0].methods).toEqual(["start", "stop"]); + expect(result.classes[0].lineRange[0]).toBe(1); + + tree.delete(); + parser.delete(); + }); + + it("extracts empty class", () => { + const { tree, parser, root } = parse(`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(`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(`interface Repository { + List findAll(); + User findById(int id); +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Repository"); + 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(`interface Marker { +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Marker"); + expect(result.classes[0].methods).toEqual([]); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Imports ---- + + describe("extractStructure - imports", () => { + it("extracts regular imports", () => { + const { tree, parser, root } = parse(`import java.util.List; +import java.util.Map; +public class Foo {} +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("java.util.List"); + expect(result.imports[0].specifiers).toEqual(["List"]); + expect(result.imports[0].lineNumber).toBe(1); + expect(result.imports[1].source).toBe("java.util.Map"); + expect(result.imports[1].specifiers).toEqual(["Map"]); + expect(result.imports[1].lineNumber).toBe(2); + + tree.delete(); + parser.delete(); + }); + + it("extracts wildcard imports", () => { + const { tree, parser, root } = parse(`import java.util.*; +public class Foo {} +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(1); + expect(result.imports[0].source).toBe("java.util"); + expect(result.imports[0].specifiers).toEqual(["*"]); + + tree.delete(); + parser.delete(); + }); + + it("reports correct import line numbers", () => { + const { tree, parser, root } = parse(`import java.util.List; + +import java.util.Map; +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, and constructor", () => { + const { tree, parser, root } = parse(`public class UserService { + private String name; + public UserService(String name) { + this.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 + // The constructor is also named UserService, check it's listed + const userServiceExports = result.exports.filter( + (e) => e.name === "UserService", + ); + expect(userServiceExports.length).toBe(2); // class + constructor + 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(`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(`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(`public interface Repository { + void save(); +} +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("Repository"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Call Graph ---- + + describe("extractCallGraph", () => { + it("extracts simple method calls", () => { + const { tree, parser, root } = parse(`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. System.out.println)", () => { + const { tree, parser, root } = parse(`public class Foo { + private void log(String message) { + System.out.println(message); + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(1); + expect(result[0].caller).toBe("log"); + expect(result[0].callee).toBe("System.out.println"); + + tree.delete(); + parser.delete(); + }); + + it("extracts object creation expressions", () => { + const { tree, parser, root } = parse(`public class Foo { + public void create() { + Bar 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(`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(`public class Foo { + public void run() { + foo(); + bar(); + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(2); + expect(result[0].lineNumber).toBe(3); + expect(result[1].lineNumber).toBe(4); + + tree.delete(); + parser.delete(); + }); + + it("ignores calls outside methods (no caller)", () => { + // Java doesn't really allow top-level calls, but field initializers + // can have method calls. We skip those without a method context. + const { tree, parser, root } = parse(`public class Foo { + private String value = String.valueOf(42); +} +`); + const result = extractor.extractCallGraph(root); + + // No enclosing method, so these are skipped + expect(result).toHaveLength(0); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Comprehensive ---- + + describe("comprehensive Java file", () => { + it("handles a realistic Java module", () => { + const { tree, parser, root } = parse(`import java.util.List; +import java.util.Map; + +public class UserService { + private String name; + private int maxRetries; + + public UserService(String name) { + this.name = name; + } + + public List getUsers(int limit) { + return fetchFromDb(limit); + } + + private void log(String message) { + System.out.println(message); + } +} + +interface Repository { + 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( + ["UserService", "getUsers", "log"].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, Repository + expect(result.classes).toHaveLength(2); + + const userService = result.classes.find( + (c) => c.name === "UserService", + ); + expect(userService).toBeDefined(); + expect(userService!.methods.sort()).toEqual( + ["UserService", "getUsers", "log"].sort(), + ); + expect(userService!.properties.sort()).toEqual( + ["name", "maxRetries"].sort(), + ); + + const repository = result.classes.find( + (c) => c.name === "Repository", + ); + expect(repository).toBeDefined(); + expect(repository!.methods).toEqual(["findAll", "findById"]); + expect(repository!.properties).toEqual([]); + + // Imports: 2 (java.util.List, java.util.Map) + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("java.util.List"); + expect(result.imports[0].specifiers).toEqual(["List"]); + expect(result.imports[1].source).toBe("java.util.Map"); + expect(result.imports[1].specifiers).toEqual(["Map"]); + + // Exports: UserService (class), UserService (constructor), getUsers (public method) + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("UserService"); + expect(exportNames).toContain("getUsers"); + expect(exportNames).not.toContain("log"); // private + expect(exportNames).not.toContain("name"); // private field + expect(exportNames).not.toContain("maxRetries"); // 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 === "System.out.println"), + ).toBe(true); + + tree.delete(); + parser.delete(); + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/java-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/java-extractor.ts new file mode 100644 index 0000000..4ac3a4f --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/java-extractor.ts @@ -0,0 +1,449 @@ +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 Java `formal_parameters` node. + * + * Each `formal_parameter` child has a `name` field (identifier) and a `type` field. + */ +function extractParams(paramsNode: TreeSitterNode | null): string[] { + if (!paramsNode) return []; + const params: string[] = []; + + const declarations = findChildren(paramsNode, "formal_parameter"); + for (const decl of declarations) { + const nameNode = decl.childForFieldName("name"); + if (nameNode) { + params.push(nameNode.text); + } + } + + // Also handle spread_parameter (varargs): e.g. `String... args` + const spreadParams = findChildren(paramsNode, "spread_parameter"); + for (const spread of spreadParams) { + const nameNode = spread.childForFieldName("name"); + if (nameNode) { + params.push(nameNode.text); + } + } + + return params; +} + +/** + * Extract the return type text from a method_declaration node. + * + * In tree-sitter-java, the return type is the `type` named field on method_declaration. + * It can be a type_identifier, generic_type, void_type, integral_type, etc. + */ +function extractReturnType(node: TreeSitterNode): string | undefined { + const typeNode = node.childForFieldName("type"); + if (!typeNode) return undefined; + return typeNode.text; +} + +/** + * Check if a node has a `modifiers` child containing a specific modifier keyword. + */ +function hasModifier(node: TreeSitterNode, modifier: string): boolean { + const modifiers = findChild(node, "modifiers"); + if (!modifiers) return false; + for (let i = 0; i < modifiers.childCount; i++) { + const child = modifiers.child(i); + if (child && child.text === modifier) return true; + } + return false; +} + +/** + * Extract the full dotted path from a scoped_identifier node. + * + * Java's scoped_identifier nests recursively: + * `java.util.List` is scoped_identifier(scope: scoped_identifier(scope: identifier "java", + * name: identifier "util"), name: identifier "List") + * + * This returns the full path as a dotted string. + */ +function extractScopedIdentifierPath(node: TreeSitterNode): string { + return node.text; +} + +/** + * Get the last component of a dotted import path. + * e.g. "java.util.List" -> "List" + */ +function lastComponent(path: string): string { + const parts = path.split("."); + return parts[parts.length - 1]; +} + +/** + * Java extractor for tree-sitter structural analysis and call graph extraction. + * + * Handles classes, interfaces, methods, constructors, fields, imports, + * visibility-based exports, and call graphs for Java source code. + * + * Java-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. + * - Exports are determined by the `public` modifier on classes, methods, + * constructors, and fields. + * - Fields are extracted as `properties` from `field_declaration` nodes. + */ +export class JavaExtractor implements LanguageExtractor { + readonly languageIds = ["java"]; + + extractStructure(rootNode: TreeSitterNode): StructuralAnalysis { + const functions: StructuralAnalysis["functions"] = []; + const classes: StructuralAnalysis["classes"] = []; + const imports: StructuralAnalysis["imports"] = []; + const exports: StructuralAnalysis["exports"] = []; + + for (let i = 0; i < rootNode.childCount; i++) { + const node = rootNode.child(i); + if (!node) continue; + + switch (node.type) { + case "import_declaration": + this.extractImport(node, imports); + break; + + case "class_declaration": + this.extractClass(node, functions, classes, exports); + break; + + case "interface_declaration": + this.extractInterface(node, functions, classes, exports); + break; + } + } + + 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), System.out.println(msg) + if (node.type === "method_invocation") { + if (functionStack.length > 0) { + const callee = this.extractMethodInvocationName(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) { + const typeNode = node.childForFieldName("type"); + 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 ---- + + /** + * Extract the callee name from a method_invocation node. + * + * Handles: + * - Plain method call: `fetchFromDb(limit)` -> "fetchFromDb" + * - Qualified call: `System.out.println(msg)` -> "System.out.println" + */ + private extractMethodInvocationName(node: TreeSitterNode): string | null { + const nameNode = node.childForFieldName("name"); + if (!nameNode) return null; + + const objectNode = node.childForFieldName("object"); + if (objectNode) { + return `${objectNode.text}.${nameNode.text}`; + } + + return nameNode.text; + } + + private extractImport( + node: TreeSitterNode, + imports: StructuralAnalysis["imports"], + ): void { + // Check for asterisk (wildcard) import: `import java.util.*;` + const hasAsterisk = findChild(node, "asterisk") !== null; + + const scopedId = findChild(node, "scoped_identifier"); + if (!scopedId) return; + + const fullPath = extractScopedIdentifierPath(scopedId); + + if (hasAsterisk) { + // Wildcard import: source is the full scope, specifier is "*" + imports.push({ + source: fullPath, + specifiers: ["*"], + lineNumber: node.startPosition.row + 1, + }); + } else { + // Regular import: source is the full path, specifier is the last component + imports.push({ + source: fullPath, + specifiers: [lastComponent(fullPath)], + 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 also contain constant_declaration (fields) + const fields = findChildren(body, "constant_declaration"); + for (const field of fields) { + const declarators = findChildren(field, "variable_declarator"); + for (const decl of declarators) { + const declName = decl.childForFieldName("name"); + if (declName) { + properties.push(declName.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, and fields from a class_body node. + */ + 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 "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 extractField( + node: TreeSitterNode, + properties: string[], + exports: StructuralAnalysis["exports"], + ): void { + const declarators = findChildren(node, "variable_declarator"); + for (const decl of declarators) { + const nameNode = decl.childForFieldName("name"); + if (nameNode) { + properties.push(nameNode.text); + + if (hasModifier(node, "public")) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + } + } +}