feat: add CppExtractor for tree-sitter C/C++ structural analysis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-04-15 19:13:59 +08:00
co-authored by Claude Opus 4.6
parent 4eb35b0437
commit 48e1daa838
2 changed files with 1197 additions and 0 deletions
@@ -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 <iostream>
#include <vector>
`);
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 <iostream>
#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 <iostream>
#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 <stdio.h>
#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 <stdlib.h>
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();
});
});
});
@@ -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<string, string[]>();
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<string, string[]>,
): 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: `<iostream>`
* - `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> -> 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<string, string[]>,
): 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;
}
}