mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
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:
co-authored by
Claude Opus 4.6
parent
4eb35b0437
commit
48e1daa838
+696
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user