From 48e1daa838c0a4b966b26196aafb78dfc9970b49 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Wed, 15 Apr 2026 19:13:59 +0800 Subject: [PATCH] feat: add CppExtractor for tree-sitter C/C++ structural analysis Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__tests__/cpp-extractor.test.ts | 696 ++++++++++++++++++ .../src/plugins/extractors/cpp-extractor.ts | 501 +++++++++++++ 2 files changed, 1197 insertions(+) create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/cpp-extractor.test.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/cpp-extractor.ts diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/cpp-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/cpp-extractor.test.ts new file mode 100644 index 0000000..3dd30ca --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/cpp-extractor.test.ts @@ -0,0 +1,696 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { createRequire } from "node:module"; +import { CppExtractor } from "../cpp-extractor.js"; + +const require = createRequire(import.meta.url); + +// Load tree-sitter + C++ grammar once +let Parser: any; +let Language: any; +let cppLang: 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-cpp/tree-sitter-cpp.wasm", + ); + cppLang = await Language.load(wasmPath); +}); + +function parse(code: string) { + const parser = new Parser(); + parser.setLanguage(cppLang); + const tree = parser.parse(code); + const root = tree.rootNode; + return { tree, parser, root }; +} + +describe("CppExtractor", () => { + const extractor = new CppExtractor(); + + it("has correct languageIds", () => { + expect(extractor.languageIds).toEqual(["cpp"]); + }); + + // ---- Functions ---- + + describe("extractStructure - functions", () => { + it("extracts top-level functions with params and return types", () => { + const { tree, parser, root } = parse(` +int add(int a, int b) { + return a + b; +} + +void greet(const char* name) { + printf("Hello %s", name); +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(2); + + expect(result.functions[0].name).toBe("add"); + expect(result.functions[0].params).toEqual(["a", "b"]); + expect(result.functions[0].returnType).toBe("int"); + + expect(result.functions[1].name).toBe("greet"); + expect(result.functions[1].params).toEqual(["name"]); + expect(result.functions[1].returnType).toBe("void"); + + tree.delete(); + parser.delete(); + }); + + it("extracts functions with no params", () => { + const { tree, parser, root } = parse(` +int get_value() { + return 42; +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("get_value"); + expect(result.functions[0].params).toEqual([]); + expect(result.functions[0].returnType).toBe("int"); + + tree.delete(); + parser.delete(); + }); + + it("reports correct line ranges for multi-line functions", () => { + const { tree, parser, root } = parse(` +int multiline( + 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(); + }); + + it("handles pointer and reference parameters", () => { + const { tree, parser, root } = parse(` +void process(int* ptr, const char& ref, int arr[]) { +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].params).toEqual(["ptr", "ref", "arr"]); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Classes ---- + + describe("extractStructure - classes", () => { + it("extracts class with properties and method declarations", () => { + const { tree, parser, root } = parse(` +class Server { +public: + std::string host; + int port; + + void start(); + int getPort() { return port; } +}; +`); + 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).toContain("start"); + expect(result.classes[0].methods).toContain("getPort"); + + tree.delete(); + parser.delete(); + }); + + it("respects access specifiers for exports", () => { + const { tree, parser, root } = parse(` +class Foo { +private: + int secret; + void hidden(); +public: + int visible; + void exposed(); +}; +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + // Public members should be exported + expect(exportNames).toContain("exposed"); + // Private members should NOT be exported (except the class name itself) + expect(exportNames).not.toContain("hidden"); + expect(exportNames).not.toContain("secret"); + // The class itself is always exported + expect(exportNames).toContain("Foo"); + + tree.delete(); + parser.delete(); + }); + + it("defaults class members to private access", () => { + const { tree, parser, root } = parse(` +class Priv { + int x; + void secret(); +}; +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("Priv"); + // Members without access specifier in a class default to private + expect(exportNames).not.toContain("secret"); + + tree.delete(); + parser.delete(); + }); + + it("handles inline method definitions (function_definition inside class)", () => { + const { tree, parser, root } = parse(` +class Calculator { +public: + int add(int a, int b) { return a + b; } +}; +`); + const result = extractor.extractStructure(root); + + // Inline method should appear in both classes.methods and functions + expect(result.classes[0].methods).toContain("add"); + + const addFn = result.functions.find((f) => f.name === "add"); + expect(addFn).toBeDefined(); + expect(addFn!.params).toEqual(["a", "b"]); + expect(addFn!.returnType).toBe("int"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Structs ---- + + describe("extractStructure - structs", () => { + it("extracts struct with fields", () => { + const { tree, parser, root } = parse(` +struct Point { + int x; + int y; +}; +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Point"); + expect(result.classes[0].properties).toEqual(["x", "y"]); + expect(result.classes[0].methods).toEqual([]); + + tree.delete(); + parser.delete(); + }); + + it("defaults struct members to public access and exports them", () => { + const { tree, parser, root } = parse(` +struct Config { + int port; + void init(); +}; +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + // Struct members default to public + expect(exportNames).toContain("Config"); + expect(exportNames).toContain("init"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Includes (imports) ---- + + describe("extractStructure - includes", () => { + it("extracts system includes (angle brackets)", () => { + const { tree, parser, root } = parse(` +#include +#include +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("iostream"); + expect(result.imports[0].specifiers).toEqual(["iostream"]); + expect(result.imports[1].source).toBe("vector"); + + tree.delete(); + parser.delete(); + }); + + it("extracts local includes (quoted)", () => { + const { tree, parser, root } = parse(` +#include "config.h" +#include "utils/helper.h" +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("config.h"); + expect(result.imports[0].specifiers).toEqual(["config.h"]); + expect(result.imports[1].source).toBe("utils/helper.h"); + + tree.delete(); + parser.delete(); + }); + + it("reports correct import line numbers", () => { + const { tree, parser, root } = parse(` +#include +#include "config.h" +`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(2); + expect(result.imports[0].lineNumber).toBe(2); + expect(result.imports[1].lineNumber).toBe(3); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Namespaces ---- + + describe("extractStructure - namespaces", () => { + it("extracts functions inside namespaces", () => { + const { tree, parser, root } = parse(` +namespace utils { + int add(int a, int b) { + return a + b; + } + + void log(const char* msg) {} +} +`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(2); + const names = result.functions.map((f) => f.name); + expect(names).toContain("add"); + expect(names).toContain("log"); + + tree.delete(); + parser.delete(); + }); + + it("extracts classes inside namespaces", () => { + const { tree, parser, root } = parse(` +namespace models { + class User { + public: + std::string name; + int id; + }; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("User"); + expect(result.classes[0].properties).toEqual(["name", "id"]); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Out-of-class method definitions ---- + + describe("extractStructure - out-of-class methods", () => { + it("associates out-of-class method with its class", () => { + const { tree, parser, root } = parse(` +class Server { +public: + void start(); +}; + +void Server::start() { + // implementation +} +`); + const result = extractor.extractStructure(root); + + // The class should have start as a method (from both declaration and definition) + expect(result.classes[0].methods).toContain("start"); + + // The out-of-class definition should appear in functions + const startFn = result.functions.find((f) => f.name === "start"); + expect(startFn).toBeDefined(); + expect(startFn!.returnType).toBe("void"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Exports ---- + + describe("extractStructure - exports", () => { + it("exports non-static functions and not static ones", () => { + const { tree, parser, root } = parse(` +int public_fn(int x) { return x; } + +static void private_fn() {} +`); + const result = extractor.extractStructure(root); + + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("public_fn"); + expect(exportNames).not.toContain("private_fn"); + + tree.delete(); + parser.delete(); + }); + + it("reports correct export line numbers", () => { + const { tree, parser, root } = parse(` +struct Point { + int x; + int y; +}; + +int compute(int n) { return n * 2; } +`); + const result = extractor.extractStructure(root); + + const pointExport = result.exports.find((e) => e.name === "Point"); + expect(pointExport?.lineNumber).toBe(2); + + const computeExport = result.exports.find((e) => e.name === "compute"); + expect(computeExport?.lineNumber).toBe(7); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Call Graph ---- + + describe("extractCallGraph", () => { + it("extracts simple function calls", () => { + const { tree, parser, root } = parse(` +void helper(int x) {} + +int main() { + helper(42); +} +`); + const result = extractor.extractCallGraph(root); + + const mainCalls = result.filter((e) => e.caller === "main"); + expect(mainCalls.some((e) => e.callee === "helper")).toBe(true); + + tree.delete(); + parser.delete(); + }); + + it("extracts multiple calls from one function", () => { + const { tree, parser, root } = parse(` +void foo() {} +void bar() {} + +int main() { + foo(); + bar(); +} +`); + const result = extractor.extractCallGraph(root); + + const mainCalls = result.filter((e) => e.caller === "main"); + expect(mainCalls).toHaveLength(2); + expect(mainCalls.some((e) => e.callee === "foo")).toBe(true); + expect(mainCalls.some((e) => e.callee === "bar")).toBe(true); + + tree.delete(); + parser.delete(); + }); + + it("extracts calls inside namespace functions", () => { + const { tree, parser, root } = parse(` +int baz(int x) { return x; } + +namespace ns { + void inner() { + baz(42); + } +} +`); + const result = extractor.extractCallGraph(root); + + expect(result.some((e) => e.caller === "inner" && e.callee === "baz")).toBe(true); + + tree.delete(); + parser.delete(); + }); + + it("reports correct line numbers for calls", () => { + const { tree, parser, root } = parse(` +int main() { + 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 of functions (no caller)", () => { + const { tree, parser, root } = parse(` +int x = compute(); +`); + const result = extractor.extractCallGraph(root); + + // Top-level initializers have no enclosing function + expect(result).toHaveLength(0); + + tree.delete(); + parser.delete(); + }); + + it("tracks member function calls (field_expression)", () => { + const { tree, parser, root } = parse(` +void process() { + obj.method(); +} +`); + const result = extractor.extractCallGraph(root); + + expect(result).toHaveLength(1); + expect(result[0].caller).toBe("process"); + expect(result[0].callee).toBe("method"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Comprehensive C++ test ---- + + describe("comprehensive C++ file", () => { + it("handles the full C++ test scenario from the spec", () => { + const { tree, parser, root } = parse(`#include +#include "config.h" + +class Server { +public: + std::string host; + int port; + + void start(); + int getPort() { return port; } +}; + +void Server::start() { + std::cout << "starting" << std::endl; +} + +namespace utils { + int add(int a, int b) { + return a + b; + } +} +`); + const result = extractor.extractStructure(root); + + // Imports: 2 includes + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("iostream"); + expect(result.imports[1].source).toBe("config.h"); + + // Classes: Server + 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).toContain("start"); + expect(result.classes[0].methods).toContain("getPort"); + + // Functions: getPort (inline), start (out-of-class), add (namespace) + expect(result.functions).toHaveLength(3); + const fnNames = result.functions.map((f) => f.name).sort(); + expect(fnNames).toEqual(["add", "getPort", "start"]); + + // add() params + const addFn = result.functions.find((f) => f.name === "add"); + expect(addFn?.params).toEqual(["a", "b"]); + expect(addFn?.returnType).toBe("int"); + + // getPort() inline + const getPortFn = result.functions.find((f) => f.name === "getPort"); + expect(getPortFn?.params).toEqual([]); + expect(getPortFn?.returnType).toBe("int"); + + // Exports: Server, start, getPort, add (all non-static/public) + const exportNames = result.exports.map((e) => e.name).sort(); + expect(exportNames).toContain("Server"); + expect(exportNames).toContain("start"); + expect(exportNames).toContain("getPort"); + expect(exportNames).toContain("add"); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Comprehensive pure C test ---- + + describe("comprehensive pure C file", () => { + it("handles pure C code with structs and functions", () => { + const { tree, parser, root } = parse(`#include +#include "helper.h" + +struct Point { + int x; + int y; +}; + +void print_point(struct Point* p) { + printf("(%d, %d)", p->x, p->y); +} + +int main() { + struct Point p = {1, 2}; + print_point(&p); + return 0; +} +`); + const result = extractor.extractStructure(root); + + // Imports: 2 includes + expect(result.imports).toHaveLength(2); + expect(result.imports[0].source).toBe("stdio.h"); + expect(result.imports[0].specifiers).toEqual(["stdio.h"]); + expect(result.imports[1].source).toBe("helper.h"); + + // Classes: Point (struct mapped to class) + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Point"); + expect(result.classes[0].properties).toEqual(["x", "y"]); + expect(result.classes[0].methods).toEqual([]); + + // Functions: print_point and main + expect(result.functions).toHaveLength(2); + const fnNames = result.functions.map((f) => f.name).sort(); + expect(fnNames).toEqual(["main", "print_point"]); + + // print_point params + const printFn = result.functions.find((f) => f.name === "print_point"); + expect(printFn?.params).toEqual(["p"]); + expect(printFn?.returnType).toBe("void"); + + // main params + const mainFn = result.functions.find((f) => f.name === "main"); + expect(mainFn?.params).toEqual([]); + expect(mainFn?.returnType).toBe("int"); + + // Exports: non-static functions + struct name + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("Point"); + expect(exportNames).toContain("print_point"); + expect(exportNames).toContain("main"); + + // Call graph + const calls = extractor.extractCallGraph(root); + + // print_point calls printf + const printCalls = calls.filter((e) => e.caller === "print_point"); + expect(printCalls.some((e) => e.callee === "printf")).toBe(true); + + // main calls print_point + const mainCalls = calls.filter((e) => e.caller === "main"); + expect(mainCalls.some((e) => e.callee === "print_point")).toBe(true); + + tree.delete(); + parser.delete(); + }); + + it("handles pure C code without any classes or structs", () => { + const { tree, parser, root } = parse(` +#include + +int factorial(int n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +} + +int main() { + int result = factorial(5); + return 0; +} +`); + const result = extractor.extractStructure(root); + + // No classes in pure C without structs + expect(result.classes).toHaveLength(0); + + // Functions + expect(result.functions).toHaveLength(2); + expect(result.functions[0].name).toBe("factorial"); + expect(result.functions[0].params).toEqual(["n"]); + expect(result.functions[1].name).toBe("main"); + + // Call graph: factorial is recursive, main calls factorial + const calls = extractor.extractCallGraph(root); + expect(calls.some((e) => e.caller === "factorial" && e.callee === "factorial")).toBe(true); + expect(calls.some((e) => e.caller === "main" && e.callee === "factorial")).toBe(true); + + tree.delete(); + parser.delete(); + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/cpp-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/cpp-extractor.ts new file mode 100644 index 0000000..48b151e --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/cpp-extractor.ts @@ -0,0 +1,501 @@ +import type { StructuralAnalysis, CallGraphEntry } from "../../types.js"; +import type { LanguageExtractor, TreeSitterNode } from "./types.js"; +import { findChild, findChildren } from "./base-extractor.js"; + +/** + * Recursively unwrap nested declarators (pointer_declarator, reference_declarator, + * array_declarator) to find the leaf identifier name. + * + * C/C++ parameter declarators can be deeply nested: + * `char** pp` => pointer_declarator -> pointer_declarator -> identifier("pp") + * `const std::string& ref` => reference_declarator -> identifier("ref") + * `int arr[]` => array_declarator -> identifier("arr") + */ +function unwrapDeclaratorName(node: TreeSitterNode): string | null { + if (node.type === "identifier" || node.type === "field_identifier") { + return node.text; + } + // Dig into the nested declarator field + const inner = node.childForFieldName("declarator"); + if (inner) { + return unwrapDeclaratorName(inner); + } + // Fallback: look for direct identifier/field_identifier child + const id = findChild(node, "identifier") ?? findChild(node, "field_identifier"); + return id ? id.text : null; +} + +/** + * Extract the function/method name from a function_declarator node. + * + * The declarator field can be: + * - `identifier` for free functions: `int baz(int y)` + * - `field_identifier` for in-class declarations/definitions: `void start();` + * - `qualified_identifier` for out-of-class definitions: `void Server::start()` + * + * For qualified_identifier, we extract just the final name (e.g., "start"), + * but also return the qualifier (e.g., "Server") to associate methods with classes. + */ +function extractFuncDeclName( + funcDecl: TreeSitterNode, +): { name: string; qualifier: string | null } | null { + const declNode = funcDecl.childForFieldName("declarator"); + if (!declNode) return null; + + if (declNode.type === "identifier" || declNode.type === "field_identifier") { + return { name: declNode.text, qualifier: null }; + } + + if (declNode.type === "qualified_identifier") { + const nameNode = declNode.childForFieldName("name"); + // The qualifier is the namespace_identifier before :: + const nsNode = findChild(declNode, "namespace_identifier"); + return { + name: nameNode ? nameNode.text : declNode.text, + qualifier: nsNode ? nsNode.text : null, + }; + } + + return { name: declNode.text, qualifier: null }; +} + +/** + * Extract parameter names from a parameter_list node. + * + * Each parameter_declaration has a `declarator` field which may be an identifier, + * pointer_declarator, reference_declarator, or array_declarator. We recursively + * unwrap to find the actual name. + */ +function extractParams(paramsNode: TreeSitterNode | null): string[] { + if (!paramsNode) return []; + const params: string[] = []; + + const decls = findChildren(paramsNode, "parameter_declaration"); + for (const decl of decls) { + const declNode = decl.childForFieldName("declarator"); + if (declNode) { + const name = unwrapDeclaratorName(declNode); + if (name) { + params.push(name); + } + } + } + + return params; +} + +/** + * Extract the return type text from a function_definition node. + * + * The return type is the `type` named field on function_definition. + * Can be primitive_type, qualified_identifier, type_identifier, etc. + */ +function extractReturnType(node: TreeSitterNode): string | undefined { + const typeNode = node.childForFieldName("type"); + if (typeNode) { + return typeNode.text; + } + return undefined; +} + +/** + * Check if a function_definition has a `storage_class_specifier` child with "static". + */ +function isStatic(node: TreeSitterNode): boolean { + const storage = findChild(node, "storage_class_specifier"); + return storage !== null && storage.text === "static"; +} + +/** + * C/C++ extractor for tree-sitter structural analysis and call graph extraction. + * + * Handles: + * - Free functions (function_definition) + * - Classes (class_specifier) with methods, properties, and access specifiers + * - Structs (struct_specifier) with fields + * - #include directives mapped to imports + * - Namespaces (namespace_definition) with recursive traversal + * - Out-of-class method definitions (e.g., void Server::start()) + * - Call graph extraction from call_expression nodes + * + * C/C++ has no formal export syntax. Non-static top-level functions and + * public class/struct members are treated as exports. + */ +export class CppExtractor implements LanguageExtractor { + readonly languageIds = ["cpp"]; + + extractStructure(rootNode: TreeSitterNode): StructuralAnalysis { + const functions: StructuralAnalysis["functions"] = []; + const classes: StructuralAnalysis["classes"] = []; + const imports: StructuralAnalysis["imports"] = []; + const exports: StructuralAnalysis["exports"] = []; + + // Track methods associated with classes via out-of-class definitions + const methodsByClass = new Map(); + + this.walkTopLevel(rootNode, functions, classes, imports, exports, methodsByClass); + + // Attach out-of-class methods to their corresponding classes + for (const cls of classes) { + const methods = methodsByClass.get(cls.name); + if (methods) { + for (const m of methods) { + if (!cls.methods.includes(m)) { + cls.methods.push(m); + } + } + } + } + + return { functions, classes, imports, exports }; + } + + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { + const entries: CallGraphEntry[] = []; + const functionStack: string[] = []; + + const walkForCalls = (node: TreeSitterNode) => { + let pushedName = false; + + // Track entering function_definition + if (node.type === "function_definition") { + const name = this.extractFunctionName(node); + if (name) { + functionStack.push(name); + pushedName = true; + } + } + + // Extract call_expression nodes + if (node.type === "call_expression") { + if (functionStack.length > 0) { + const callee = this.extractCalleeName(node); + if (callee) { + entries.push({ + caller: functionStack[functionStack.length - 1], + callee, + 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 top-level declarations. Recurses into namespace_definition bodies + * to find nested declarations. + */ + private walkTopLevel( + parentNode: TreeSitterNode, + functions: StructuralAnalysis["functions"], + classes: StructuralAnalysis["classes"], + imports: StructuralAnalysis["imports"], + exports: StructuralAnalysis["exports"], + methodsByClass: Map, + ): void { + for (let i = 0; i < parentNode.childCount; i++) { + const node = parentNode.child(i); + if (!node) continue; + + switch (node.type) { + case "preproc_include": + this.extractInclude(node, imports); + break; + + case "class_specifier": + this.extractClassOrStruct(node, "class", classes, functions, exports); + break; + + case "struct_specifier": + this.extractClassOrStruct(node, "struct", classes, functions, exports); + break; + + case "function_definition": + this.extractFunctionDef(node, functions, exports, methodsByClass); + break; + + case "namespace_definition": { + // Recurse into namespace body (declaration_list) + const body = findChild(node, "declaration_list"); + if (body) { + this.walkTopLevel(body, functions, classes, imports, exports, methodsByClass); + } + break; + } + + case "declaration": { + // A top-level ";" terminated statement — could be a class/struct with a trailing ; + // e.g., `class Foo { ... };` parses the class_specifier as a child of a + // declaration in some contexts. Check for nested class/struct specifiers. + const innerClass = findChild(node, "class_specifier"); + if (innerClass) { + this.extractClassOrStruct(innerClass, "class", classes, functions, exports); + } + const innerStruct = findChild(node, "struct_specifier"); + if (innerStruct) { + this.extractClassOrStruct(innerStruct, "struct", classes, functions, exports); + } + break; + } + } + } + } + + /** + * Extract the simple function name from a function_definition. + * For qualified names (e.g., Server::start), returns just the method name. + */ + private extractFunctionName(node: TreeSitterNode): string | null { + const declNode = node.childForFieldName("declarator"); + if (!declNode || declNode.type !== "function_declarator") return null; + + const info = extractFuncDeclName(declNode); + return info ? info.name : null; + } + + /** + * Extract #include directives and map them to the imports array. + * + * `preproc_include` has a `path` field that is either: + * - `system_lib_string` for angle-bracket includes: `` + * - `string_literal` for quoted includes: `"myfile.h"` + */ + private extractInclude( + node: TreeSitterNode, + imports: StructuralAnalysis["imports"], + ): void { + const pathNode = node.childForFieldName("path"); + if (!pathNode) return; + + let source: string; + if (pathNode.type === "system_lib_string") { + // Strip angle brackets: -> iostream + source = pathNode.text.replace(/^<|>$/g, ""); + } else if (pathNode.type === "string_literal") { + // Extract content from string: "myfile.h" -> myfile.h + const content = findChild(pathNode, "string_content"); + source = content ? content.text : pathNode.text.replace(/^"|"$/g, ""); + } else { + source = pathNode.text; + } + + imports.push({ + source, + specifiers: [source], + lineNumber: node.startPosition.row + 1, + }); + } + + /** + * Extract class_specifier or struct_specifier into the classes array. + * + * Processes: + * - Properties (field_declaration without function_declarator) + * - Method declarations (field_declaration with function_declarator) + * - Method definitions (function_definition inside the class body) + * - Access specifiers (public/private/protected) + * + * Public members of classes and all members of structs (default public) + * are treated as exports. + */ + private extractClassOrStruct( + node: TreeSitterNode, + kind: "class" | "struct", + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = node.childForFieldName("name"); + if (!nameNode) return; + + const className = nameNode.text; + const methods: string[] = []; + const properties: string[] = []; + + const body = node.childForFieldName("body"); + if (body && body.type === "field_declaration_list") { + // Default access: public for struct, private for class + let currentAccess = kind === "struct" ? "public" : "private"; + + for (let j = 0; j < body.childCount; j++) { + const member = body.child(j); + if (!member) continue; + + if (member.type === "access_specifier") { + // Update current access level + const specChild = member.child(0); + if (specChild) { + currentAccess = specChild.text; + } + continue; + } + + if (member.type === "field_declaration") { + const declNode = member.childForFieldName("declarator"); + if (declNode && declNode.type === "function_declarator") { + // Method declaration (no body) + const info = extractFuncDeclName(declNode); + if (info) { + methods.push(info.name); + if (currentAccess === "public") { + exports.push({ + name: info.name, + lineNumber: member.startPosition.row + 1, + }); + } + } + } else if (declNode) { + // Property (field_identifier or other declarator) + const name = unwrapDeclaratorName(declNode); + if (name) { + properties.push(name); + } + } + } + + if (member.type === "function_definition") { + // Inline method definition + const funcDecl = member.childForFieldName("declarator"); + if (funcDecl && funcDecl.type === "function_declarator") { + const info = extractFuncDeclName(funcDecl); + if (info) { + methods.push(info.name); + + // Also add to functions list with params/return type + const paramsNode = funcDecl.childForFieldName("parameters"); + functions.push({ + name: info.name, + lineRange: [ + member.startPosition.row + 1, + member.endPosition.row + 1, + ], + params: extractParams(paramsNode), + returnType: extractReturnType(member), + }); + + if (currentAccess === "public") { + exports.push({ + name: info.name, + lineNumber: member.startPosition.row + 1, + }); + } + } + } + } + } + } + + classes.push({ + name: className, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + methods, + properties, + }); + + // The class/struct name itself is an export (non-anonymous types are always exported in C/C++ headers) + exports.push({ + name: className, + lineNumber: node.startPosition.row + 1, + }); + } + + /** + * Extract a free function or out-of-class method definition. + * + * For qualified names (e.g., `void Server::start()`), the method is: + * - Added to the functions array + * - Tracked in methodsByClass for later association with the class + * - Exported if non-static + * + * Static functions are NOT exported. + */ + private extractFunctionDef( + node: TreeSitterNode, + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + methodsByClass: Map, + ): void { + const funcDecl = node.childForFieldName("declarator"); + if (!funcDecl || funcDecl.type !== "function_declarator") return; + + const info = extractFuncDeclName(funcDecl); + if (!info) return; + + const paramsNode = funcDecl.childForFieldName("parameters"); + const params = extractParams(paramsNode); + const returnType = extractReturnType(node); + + functions.push({ + name: info.name, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + params, + returnType, + }); + + // Track out-of-class method definitions (e.g., void Server::start()) + if (info.qualifier) { + if (!methodsByClass.has(info.qualifier)) { + methodsByClass.set(info.qualifier, []); + } + methodsByClass.get(info.qualifier)!.push(info.name); + } + + // Non-static top-level functions are exports + if (!isStatic(node)) { + exports.push({ + name: info.name, + lineNumber: node.startPosition.row + 1, + }); + } + } + + /** + * Extract the callee name from a call_expression. + * + * Handles: + * - Plain function call: `printf(...)` -> "printf" + * - Member call via field_expression: `p->method()` -> "p->method" + * - Scoped call: `std::cout << ...` -> qualified name text + */ + private extractCalleeName(callNode: TreeSitterNode): string | null { + const funcNode = callNode.child(0); + if (!funcNode) return null; + + if (funcNode.type === "identifier") { + return funcNode.text; + } + + if (funcNode.type === "field_expression") { + const field = funcNode.childForFieldName("field"); + return field ? field.text : funcNode.text; + } + + if (funcNode.type === "qualified_identifier") { + return funcNode.text; + } + + return funcNode.text; + } +}