mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(core): add tree-sitter analyzer plugin for TS/JS
Add TreeSitterPlugin using web-tree-sitter (WASM-based) to extract structural information from TypeScript and JavaScript files. The plugin implements the AnalyzerPlugin interface and extracts functions, classes, imports, exports, and call graphs via AST traversal. Uses web-tree-sitter instead of native tree-sitter for cross-platform compatibility (no native compilation required). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,5 +12,10 @@
|
||||
"@types/node": "^25.5.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tree-sitter-javascript": "^0.25.0",
|
||||
"tree-sitter-typescript": "^0.23.2",
|
||||
"web-tree-sitter": "^0.26.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./types.js";
|
||||
export * from "./persistence/index.js";
|
||||
export { TreeSitterPlugin } from "./plugins/tree-sitter-plugin.js";
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { TreeSitterPlugin } from "./tree-sitter-plugin.js";
|
||||
|
||||
describe("TreeSitterPlugin", () => {
|
||||
let plugin: TreeSitterPlugin;
|
||||
|
||||
beforeAll(async () => {
|
||||
plugin = new TreeSitterPlugin();
|
||||
await plugin.init();
|
||||
});
|
||||
|
||||
describe("analyzeFile", () => {
|
||||
it("should extract function declarations from TypeScript", () => {
|
||||
const code = `
|
||||
function greet(name: string): string {
|
||||
return "Hello " + name;
|
||||
}
|
||||
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
`;
|
||||
const result = plugin.analyzeFile("test.ts", code);
|
||||
|
||||
expect(result.functions).toHaveLength(2);
|
||||
expect(result.functions[0].name).toBe("greet");
|
||||
expect(result.functions[0].params).toEqual(["name"]);
|
||||
expect(result.functions[0].returnType).toBe("string");
|
||||
expect(result.functions[0].lineRange[0]).toBeGreaterThan(0);
|
||||
|
||||
expect(result.functions[1].name).toBe("add");
|
||||
expect(result.functions[1].params).toEqual(["a", "b"]);
|
||||
expect(result.functions[1].returnType).toBe("number");
|
||||
});
|
||||
|
||||
it("should extract arrow functions assigned to variables", () => {
|
||||
const code = `
|
||||
const multiply = (a: number, b: number): number => a * b;
|
||||
const log = (msg: string) => { console.log(msg); };
|
||||
`;
|
||||
const result = plugin.analyzeFile("test.ts", code);
|
||||
|
||||
expect(result.functions).toHaveLength(2);
|
||||
expect(result.functions[0].name).toBe("multiply");
|
||||
expect(result.functions[0].params).toEqual(["a", "b"]);
|
||||
expect(result.functions[0].returnType).toBe("number");
|
||||
|
||||
expect(result.functions[1].name).toBe("log");
|
||||
expect(result.functions[1].params).toEqual(["msg"]);
|
||||
});
|
||||
|
||||
it("should extract class declarations with methods and properties", () => {
|
||||
const code = `
|
||||
class Calculator {
|
||||
private value: number;
|
||||
public name: string;
|
||||
|
||||
constructor(initial: number) {
|
||||
this.value = initial;
|
||||
}
|
||||
|
||||
add(n: number): number {
|
||||
return this.value + n;
|
||||
}
|
||||
|
||||
subtract(n: number): number {
|
||||
return this.value - n;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = plugin.analyzeFile("test.ts", code);
|
||||
|
||||
expect(result.classes).toHaveLength(1);
|
||||
const cls = result.classes[0];
|
||||
expect(cls.name).toBe("Calculator");
|
||||
expect(cls.methods).toContain("constructor");
|
||||
expect(cls.methods).toContain("add");
|
||||
expect(cls.methods).toContain("subtract");
|
||||
expect(cls.properties).toContain("value");
|
||||
expect(cls.properties).toContain("name");
|
||||
expect(cls.lineRange[0]).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should extract import statements with specifiers and source", () => {
|
||||
const code = `
|
||||
import { foo, bar } from './utils';
|
||||
import * as path from 'path';
|
||||
import type { MyType } from './types';
|
||||
import defaultExport from './module';
|
||||
`;
|
||||
const result = plugin.analyzeFile("test.ts", code);
|
||||
|
||||
expect(result.imports).toHaveLength(4);
|
||||
|
||||
expect(result.imports[0].source).toBe("./utils");
|
||||
expect(result.imports[0].specifiers).toEqual(["foo", "bar"]);
|
||||
expect(result.imports[0].lineNumber).toBeGreaterThan(0);
|
||||
|
||||
expect(result.imports[1].source).toBe("path");
|
||||
expect(result.imports[1].specifiers).toEqual(["* as path"]);
|
||||
|
||||
expect(result.imports[2].source).toBe("./types");
|
||||
expect(result.imports[2].specifiers).toEqual(["MyType"]);
|
||||
|
||||
expect(result.imports[3].source).toBe("./module");
|
||||
expect(result.imports[3].specifiers).toEqual(["defaultExport"]);
|
||||
});
|
||||
|
||||
it("should extract export names", () => {
|
||||
const code = `
|
||||
export function greet(name: string): string {
|
||||
return "Hello " + name;
|
||||
}
|
||||
|
||||
export const add = (a: number, b: number): number => a + b;
|
||||
|
||||
export class Logger {}
|
||||
|
||||
const helper = () => true;
|
||||
export { helper };
|
||||
`;
|
||||
const result = plugin.analyzeFile("test.ts", code);
|
||||
|
||||
const exportNames = result.exports.map((e) => e.name);
|
||||
expect(exportNames).toContain("greet");
|
||||
expect(exportNames).toContain("add");
|
||||
expect(exportNames).toContain("Logger");
|
||||
expect(exportNames).toContain("helper");
|
||||
expect(result.exports.length).toBe(4);
|
||||
});
|
||||
|
||||
it("should extract default exports", () => {
|
||||
const code = `
|
||||
export default class AppController {}
|
||||
`;
|
||||
const result = plugin.analyzeFile("test.ts", code);
|
||||
|
||||
expect(result.classes).toHaveLength(1);
|
||||
expect(result.classes[0].name).toBe("AppController");
|
||||
|
||||
const exportNames = result.exports.map((e) => e.name);
|
||||
expect(exportNames).toContain("default");
|
||||
});
|
||||
|
||||
it("should extract functions from JavaScript files", () => {
|
||||
const code = `
|
||||
function hello() {
|
||||
return "world";
|
||||
}
|
||||
|
||||
const double = (x) => x * 2;
|
||||
`;
|
||||
const result = plugin.analyzeFile("test.js", code);
|
||||
|
||||
expect(result.functions).toHaveLength(2);
|
||||
expect(result.functions[0].name).toBe("hello");
|
||||
expect(result.functions[0].params).toEqual([]);
|
||||
|
||||
expect(result.functions[1].name).toBe("double");
|
||||
expect(result.functions[1].params).toEqual(["x"]);
|
||||
});
|
||||
|
||||
it("should handle a comprehensive TypeScript file", () => {
|
||||
const code = `
|
||||
import { EventEmitter } from 'events';
|
||||
import type { Config } from './config';
|
||||
|
||||
export interface Options {
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export class Server extends EventEmitter {
|
||||
private port: number;
|
||||
|
||||
constructor(port: number) {
|
||||
super();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
console.log("Starting on port " + this.port);
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export function createServer(port: number): Server {
|
||||
return new Server(port);
|
||||
}
|
||||
|
||||
export const DEFAULT_PORT = 3000;
|
||||
`;
|
||||
const result = plugin.analyzeFile("server.ts", code);
|
||||
|
||||
expect(result.imports.length).toBeGreaterThanOrEqual(2);
|
||||
expect(result.classes).toHaveLength(1);
|
||||
expect(result.classes[0].name).toBe("Server");
|
||||
expect(result.classes[0].methods).toContain("constructor");
|
||||
expect(result.classes[0].methods).toContain("start");
|
||||
expect(result.classes[0].methods).toContain("stop");
|
||||
expect(result.classes[0].properties).toContain("port");
|
||||
|
||||
expect(result.functions.some((f) => f.name === "createServer")).toBe(true);
|
||||
|
||||
const exportNames = result.exports.map((e) => e.name);
|
||||
expect(exportNames).toContain("Server");
|
||||
expect(exportNames).toContain("createServer");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveImports", () => {
|
||||
it("should resolve relative imports to absolute paths", () => {
|
||||
const code = `
|
||||
import { foo } from './utils';
|
||||
import { bar } from '../shared/helpers';
|
||||
import * as path from 'path';
|
||||
`;
|
||||
const result = plugin.resolveImports("/project/src/index.ts", code);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
|
||||
// Relative imports should be resolved
|
||||
expect(result[0].source).toBe("./utils");
|
||||
expect(result[0].resolvedPath).toContain("utils");
|
||||
expect(result[0].specifiers).toEqual(["foo"]);
|
||||
|
||||
expect(result[1].source).toBe("../shared/helpers");
|
||||
expect(result[1].resolvedPath).toContain("shared");
|
||||
expect(result[1].specifiers).toEqual(["bar"]);
|
||||
|
||||
// External packages keep their original path
|
||||
expect(result[2].source).toBe("path");
|
||||
expect(result[2].resolvedPath).toBe("path");
|
||||
expect(result[2].specifiers).toEqual(["* as path"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractCallGraph", () => {
|
||||
it("should extract function calls within functions", () => {
|
||||
const code = `
|
||||
function greet(name: string): string {
|
||||
return formatMessage("Hello " + name);
|
||||
}
|
||||
|
||||
function formatMessage(msg: string): string {
|
||||
return msg.trim();
|
||||
}
|
||||
|
||||
function main() {
|
||||
const result = greet("World");
|
||||
console.log(result);
|
||||
}
|
||||
`;
|
||||
const result = plugin.extractCallGraph!("test.ts", code);
|
||||
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
const greetCall = result.find(
|
||||
(e) => e.caller === "main" && e.callee === "greet",
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
|
||||
const formatCall = result.find(
|
||||
(e) => e.caller === "greet" && e.callee === "formatMessage",
|
||||
);
|
||||
expect(formatCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin metadata", () => {
|
||||
it("should have correct name", () => {
|
||||
expect(plugin.name).toBe("tree-sitter");
|
||||
});
|
||||
|
||||
it("should support typescript and javascript", () => {
|
||||
expect(plugin.languages).toContain("typescript");
|
||||
expect(plugin.languages).toContain("javascript");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,652 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve, extname } from "node:path";
|
||||
import type {
|
||||
AnalyzerPlugin,
|
||||
StructuralAnalysis,
|
||||
ImportResolution,
|
||||
CallGraphEntry,
|
||||
} from "../types.js";
|
||||
|
||||
// web-tree-sitter uses CJS internally; we need createRequire for .wasm resolution
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
type TreeSitterParser = import("web-tree-sitter").Parser;
|
||||
type TreeSitterLanguage = import("web-tree-sitter").Language;
|
||||
type TreeSitterNode = import("web-tree-sitter").Node;
|
||||
|
||||
function languageKeyFromPath(filePath: string): string {
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
switch (ext) {
|
||||
case ".ts":
|
||||
return "typescript";
|
||||
case ".tsx":
|
||||
return "tsx";
|
||||
case ".js":
|
||||
case ".mjs":
|
||||
case ".cjs":
|
||||
case ".jsx":
|
||||
return "javascript";
|
||||
default:
|
||||
throw new Error(`Unsupported file extension: ${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverse an AST tree, calling the visitor for each node.
|
||||
*/
|
||||
function traverse(
|
||||
node: TreeSitterNode,
|
||||
visitor: (node: TreeSitterNode) => void,
|
||||
): void {
|
||||
visitor(node);
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child) traverse(child, visitor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the string fragment (unquoted value) from a string node.
|
||||
*/
|
||||
function getStringValue(node: TreeSitterNode): string {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child && child.type === "string_fragment") {
|
||||
return child.text;
|
||||
}
|
||||
}
|
||||
// Fallback: strip quotes
|
||||
const text = node.text;
|
||||
return text.replace(/^['"`]|['"`]$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameter names from a formal_parameters node.
|
||||
*/
|
||||
function extractParams(paramsNode: TreeSitterNode | null): string[] {
|
||||
if (!paramsNode) return [];
|
||||
const params: string[] = [];
|
||||
for (let i = 0; i < paramsNode.childCount; i++) {
|
||||
const child = paramsNode.child(i);
|
||||
if (!child) continue;
|
||||
if (
|
||||
child.type === "required_parameter" ||
|
||||
child.type === "optional_parameter"
|
||||
) {
|
||||
const ident =
|
||||
child.childForFieldName("pattern") ??
|
||||
child.childForFieldName("name");
|
||||
if (ident) {
|
||||
params.push(ident.text);
|
||||
} else {
|
||||
// Fallback: first identifier child
|
||||
for (let j = 0; j < child.childCount; j++) {
|
||||
const c = child.child(j);
|
||||
if (c && c.type === "identifier") {
|
||||
params.push(c.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (child.type === "identifier") {
|
||||
// JavaScript parameters (no type annotation)
|
||||
params.push(child.text);
|
||||
} else if (
|
||||
child.type === "rest_pattern" ||
|
||||
child.type === "rest_element"
|
||||
) {
|
||||
const ident = child.children.find(
|
||||
(c) => c.type === "identifier",
|
||||
);
|
||||
if (ident) params.push("..." + ident.text);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract return type annotation from a function-like node.
|
||||
*/
|
||||
function extractReturnType(
|
||||
node: TreeSitterNode,
|
||||
): string | undefined {
|
||||
const typeAnnotation = node.childForFieldName("return_type");
|
||||
if (typeAnnotation && typeAnnotation.type === "type_annotation") {
|
||||
const text = typeAnnotation.text;
|
||||
return text.startsWith(":") ? text.slice(1).trim() : text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract import specifiers from an import_clause node.
|
||||
*/
|
||||
function extractImportSpecifiers(
|
||||
importClause: TreeSitterNode,
|
||||
): string[] {
|
||||
const specifiers: string[] = [];
|
||||
|
||||
for (let i = 0; i < importClause.childCount; i++) {
|
||||
const child = importClause.child(i);
|
||||
if (!child) continue;
|
||||
|
||||
if (child.type === "named_imports") {
|
||||
for (let j = 0; j < child.childCount; j++) {
|
||||
const spec = child.child(j);
|
||||
if (spec && spec.type === "import_specifier") {
|
||||
const alias = spec.childForFieldName("alias");
|
||||
const name = spec.childForFieldName("name");
|
||||
specifiers.push(
|
||||
alias ? alias.text : name ? name.text : spec.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (child.type === "namespace_import") {
|
||||
const ident = child.children.find(
|
||||
(c) => c.type === "identifier",
|
||||
);
|
||||
if (ident) specifiers.push("* as " + ident.text);
|
||||
} else if (child.type === "identifier") {
|
||||
// default import: import foo from '...'
|
||||
specifiers.push(child.text);
|
||||
}
|
||||
}
|
||||
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
export class TreeSitterPlugin implements AnalyzerPlugin {
|
||||
readonly name = "tree-sitter";
|
||||
readonly languages = ["typescript", "javascript"];
|
||||
|
||||
// Pre-loaded parser constructor and languages (set by init())
|
||||
private _ParserClass:
|
||||
| (new () => TreeSitterParser)
|
||||
| null = null;
|
||||
private _languages = new Map<string, TreeSitterLanguage>();
|
||||
private _initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the plugin by loading the WASM module and all language grammars.
|
||||
* Must be called (and awaited) before any synchronous methods.
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
const mod = await import("web-tree-sitter");
|
||||
const ParserCls = mod.Parser;
|
||||
const LanguageCls = mod.Language;
|
||||
|
||||
await ParserCls.init();
|
||||
this._ParserClass = ParserCls as unknown as new () => TreeSitterParser;
|
||||
|
||||
// Pre-load all supported language grammars
|
||||
const tsWasm = require.resolve(
|
||||
"tree-sitter-typescript/tree-sitter-typescript.wasm",
|
||||
);
|
||||
const tsxWasm = require.resolve(
|
||||
"tree-sitter-typescript/tree-sitter-tsx.wasm",
|
||||
);
|
||||
const jsWasm = require.resolve(
|
||||
"tree-sitter-javascript/tree-sitter-javascript.wasm",
|
||||
);
|
||||
|
||||
const [tsLang, tsxLang, jsLang] = await Promise.all([
|
||||
LanguageCls.load(tsWasm),
|
||||
LanguageCls.load(tsxWasm),
|
||||
LanguageCls.load(jsWasm),
|
||||
]);
|
||||
|
||||
this._languages.set("typescript", tsLang);
|
||||
this._languages.set("tsx", tsxLang);
|
||||
this._languages.set("javascript", jsLang);
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a parser set to the appropriate language for the given file.
|
||||
* This is synchronous because all languages are pre-loaded during init().
|
||||
*/
|
||||
private getParser(filePath: string): TreeSitterParser {
|
||||
if (!this._initialized || !this._ParserClass) {
|
||||
throw new Error(
|
||||
"TreeSitterPlugin.init() must be called before use",
|
||||
);
|
||||
}
|
||||
const langKey = languageKeyFromPath(filePath);
|
||||
const lang = this._languages.get(langKey);
|
||||
if (!lang) {
|
||||
throw new Error(`Language not loaded: ${langKey}`);
|
||||
}
|
||||
const parser = new this._ParserClass();
|
||||
parser.setLanguage(lang);
|
||||
return parser;
|
||||
}
|
||||
|
||||
analyzeFile(
|
||||
filePath: string,
|
||||
content: string,
|
||||
): StructuralAnalysis {
|
||||
const parser = this.getParser(filePath);
|
||||
const tree = parser.parse(content);
|
||||
if (!tree) {
|
||||
parser.delete();
|
||||
return { functions: [], classes: [], imports: [], exports: [] };
|
||||
}
|
||||
|
||||
const functions: StructuralAnalysis["functions"] = [];
|
||||
const classes: StructuralAnalysis["classes"] = [];
|
||||
const imports: StructuralAnalysis["imports"] = [];
|
||||
const exports: StructuralAnalysis["exports"] = [];
|
||||
const exportedNames = new Set<string>();
|
||||
|
||||
const root = tree.rootNode;
|
||||
for (let i = 0; i < root.childCount; i++) {
|
||||
const node = root.child(i);
|
||||
if (!node) continue;
|
||||
this.processTopLevelNode(
|
||||
node,
|
||||
functions,
|
||||
classes,
|
||||
imports,
|
||||
exports,
|
||||
exportedNames,
|
||||
);
|
||||
}
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
|
||||
return { functions, classes, imports, exports };
|
||||
}
|
||||
|
||||
resolveImports(
|
||||
filePath: string,
|
||||
content: string,
|
||||
): ImportResolution[] {
|
||||
const analysis = this.analyzeFile(filePath, content);
|
||||
const dir = dirname(filePath);
|
||||
|
||||
return analysis.imports.map((imp) => {
|
||||
let resolvedPath: string;
|
||||
if (
|
||||
imp.source.startsWith("./") ||
|
||||
imp.source.startsWith("../")
|
||||
) {
|
||||
resolvedPath = resolve(dir, imp.source);
|
||||
} else {
|
||||
resolvedPath = imp.source;
|
||||
}
|
||||
return {
|
||||
source: imp.source,
|
||||
resolvedPath,
|
||||
specifiers: imp.specifiers,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
extractCallGraph(
|
||||
filePath: string,
|
||||
content: string,
|
||||
): CallGraphEntry[] {
|
||||
const parser = this.getParser(filePath);
|
||||
const tree = parser.parse(content);
|
||||
if (!tree) {
|
||||
parser.delete();
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries: CallGraphEntry[] = [];
|
||||
const functionStack: string[] = [];
|
||||
|
||||
const walkForCalls = (node: TreeSitterNode) => {
|
||||
const isFunctionLike =
|
||||
node.type === "function_declaration" ||
|
||||
node.type === "method_definition" ||
|
||||
node.type === "arrow_function" ||
|
||||
node.type === "function_expression";
|
||||
|
||||
let pushedName = false;
|
||||
if (isFunctionLike) {
|
||||
let name: string | undefined;
|
||||
if (node.type === "function_declaration") {
|
||||
name = (
|
||||
node.childForFieldName("name") ??
|
||||
node.children.find((c) => c.type === "identifier")
|
||||
)?.text;
|
||||
} else if (node.type === "method_definition") {
|
||||
name = node.children.find(
|
||||
(c) => c.type === "property_identifier",
|
||||
)?.text;
|
||||
} else if (
|
||||
node.type === "arrow_function" ||
|
||||
node.type === "function_expression"
|
||||
) {
|
||||
const parent = node.parent;
|
||||
if (parent && parent.type === "variable_declarator") {
|
||||
name = parent.childForFieldName("name")?.text;
|
||||
}
|
||||
}
|
||||
if (name) {
|
||||
functionStack.push(name);
|
||||
pushedName = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === "call_expression") {
|
||||
const callee = node.childForFieldName("function");
|
||||
if (callee && functionStack.length > 0) {
|
||||
entries.push({
|
||||
caller: functionStack[functionStack.length - 1],
|
||||
callee: callee.text,
|
||||
lineNumber: node.startPosition.row + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child) walkForCalls(child);
|
||||
}
|
||||
|
||||
if (pushedName) {
|
||||
functionStack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
walkForCalls(tree.rootNode);
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// ---- Private extraction helpers ----
|
||||
|
||||
private processTopLevelNode(
|
||||
node: TreeSitterNode,
|
||||
functions: StructuralAnalysis["functions"],
|
||||
classes: StructuralAnalysis["classes"],
|
||||
imports: StructuralAnalysis["imports"],
|
||||
exports: StructuralAnalysis["exports"],
|
||||
exportedNames: Set<string>,
|
||||
): void {
|
||||
switch (node.type) {
|
||||
case "function_declaration":
|
||||
this.extractFunction(node, functions);
|
||||
break;
|
||||
|
||||
case "class_declaration":
|
||||
this.extractClass(node, classes);
|
||||
break;
|
||||
|
||||
case "lexical_declaration":
|
||||
case "variable_declaration":
|
||||
this.extractVariableDeclarations(node, functions);
|
||||
break;
|
||||
|
||||
case "import_statement":
|
||||
this.extractImport(node, imports);
|
||||
break;
|
||||
|
||||
case "export_statement":
|
||||
this.processExportStatement(
|
||||
node,
|
||||
functions,
|
||||
classes,
|
||||
imports,
|
||||
exports,
|
||||
exportedNames,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private extractFunction(
|
||||
node: TreeSitterNode,
|
||||
functions: StructuralAnalysis["functions"],
|
||||
): void {
|
||||
const nameNode =
|
||||
node.childForFieldName("name") ??
|
||||
node.children.find((c) => c.type === "identifier");
|
||||
if (!nameNode) return;
|
||||
|
||||
const params = extractParams(
|
||||
node.childForFieldName("parameters") ??
|
||||
node.children.find(
|
||||
(c) => c.type === "formal_parameters",
|
||||
) ??
|
||||
null,
|
||||
);
|
||||
const returnType = extractReturnType(node);
|
||||
|
||||
functions.push({
|
||||
name: nameNode.text,
|
||||
lineRange: [
|
||||
node.startPosition.row + 1,
|
||||
node.endPosition.row + 1,
|
||||
],
|
||||
params,
|
||||
returnType,
|
||||
});
|
||||
}
|
||||
|
||||
private extractClass(
|
||||
node: TreeSitterNode,
|
||||
classes: StructuralAnalysis["classes"],
|
||||
): void {
|
||||
const nameNode = node.children.find(
|
||||
(c) =>
|
||||
c.type === "type_identifier" || c.type === "identifier",
|
||||
);
|
||||
if (!nameNode) return;
|
||||
|
||||
const methods: string[] = [];
|
||||
const properties: string[] = [];
|
||||
|
||||
const classBody = node.children.find(
|
||||
(c) => c.type === "class_body",
|
||||
);
|
||||
if (classBody) {
|
||||
for (let j = 0; j < classBody.childCount; j++) {
|
||||
const member = classBody.child(j);
|
||||
if (!member) continue;
|
||||
|
||||
if (member.type === "method_definition") {
|
||||
const methodName = member.children.find(
|
||||
(c) => c.type === "property_identifier",
|
||||
);
|
||||
if (methodName) methods.push(methodName.text);
|
||||
} else if (
|
||||
member.type === "public_field_definition" ||
|
||||
member.type === "property_definition"
|
||||
) {
|
||||
const propName = member.children.find(
|
||||
(c) => c.type === "property_identifier",
|
||||
);
|
||||
if (propName) properties.push(propName.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
classes.push({
|
||||
name: nameNode.text,
|
||||
lineRange: [
|
||||
node.startPosition.row + 1,
|
||||
node.endPosition.row + 1,
|
||||
],
|
||||
methods,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
|
||||
private extractVariableDeclarations(
|
||||
node: TreeSitterNode,
|
||||
functions: StructuralAnalysis["functions"],
|
||||
): void {
|
||||
for (let j = 0; j < node.childCount; j++) {
|
||||
const child = node.child(j);
|
||||
if (!child || child.type !== "variable_declarator") continue;
|
||||
|
||||
const nameNode = child.childForFieldName("name");
|
||||
const valueNode = child.childForFieldName("value");
|
||||
|
||||
if (
|
||||
nameNode &&
|
||||
valueNode &&
|
||||
(valueNode.type === "arrow_function" ||
|
||||
valueNode.type === "function_expression" ||
|
||||
valueNode.type === "function")
|
||||
) {
|
||||
const params = extractParams(
|
||||
valueNode.childForFieldName("parameters") ??
|
||||
valueNode.children.find(
|
||||
(c) => c.type === "formal_parameters",
|
||||
) ??
|
||||
null,
|
||||
);
|
||||
const returnType = extractReturnType(valueNode);
|
||||
|
||||
functions.push({
|
||||
name: nameNode.text,
|
||||
lineRange: [
|
||||
node.startPosition.row + 1,
|
||||
node.endPosition.row + 1,
|
||||
],
|
||||
params,
|
||||
returnType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractImport(
|
||||
node: TreeSitterNode,
|
||||
imports: StructuralAnalysis["imports"],
|
||||
): void {
|
||||
const sourceNode = node.children.find(
|
||||
(c) => c.type === "string",
|
||||
);
|
||||
if (!sourceNode) return;
|
||||
|
||||
const source = getStringValue(sourceNode);
|
||||
const specifiers: string[] = [];
|
||||
|
||||
const importClause = node.children.find(
|
||||
(c) => c.type === "import_clause",
|
||||
);
|
||||
if (importClause) {
|
||||
specifiers.push(...extractImportSpecifiers(importClause));
|
||||
}
|
||||
|
||||
imports.push({
|
||||
source,
|
||||
specifiers,
|
||||
lineNumber: node.startPosition.row + 1,
|
||||
});
|
||||
}
|
||||
|
||||
private processExportStatement(
|
||||
node: TreeSitterNode,
|
||||
functions: StructuralAnalysis["functions"],
|
||||
classes: StructuralAnalysis["classes"],
|
||||
_imports: StructuralAnalysis["imports"],
|
||||
exports: StructuralAnalysis["exports"],
|
||||
exportedNames: Set<string>,
|
||||
): void {
|
||||
for (let j = 0; j < node.childCount; j++) {
|
||||
const child = node.child(j);
|
||||
if (!child) continue;
|
||||
|
||||
switch (child.type) {
|
||||
case "function_declaration": {
|
||||
this.extractFunction(child, functions);
|
||||
const nameNode =
|
||||
child.childForFieldName("name") ??
|
||||
child.children.find((c) => c.type === "identifier");
|
||||
if (nameNode && !exportedNames.has(nameNode.text)) {
|
||||
exports.push({
|
||||
name: nameNode.text,
|
||||
lineNumber: node.startPosition.row + 1,
|
||||
});
|
||||
exportedNames.add(nameNode.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "class_declaration": {
|
||||
this.extractClass(child, classes);
|
||||
const nameNode = child.children.find(
|
||||
(c) =>
|
||||
c.type === "type_identifier" ||
|
||||
c.type === "identifier",
|
||||
);
|
||||
if (nameNode && !exportedNames.has(nameNode.text)) {
|
||||
const isDefault = node.children.some(
|
||||
(c) => c.type === "default",
|
||||
);
|
||||
const exportName = isDefault
|
||||
? "default"
|
||||
: nameNode.text;
|
||||
exports.push({
|
||||
name: exportName,
|
||||
lineNumber: node.startPosition.row + 1,
|
||||
});
|
||||
exportedNames.add(exportName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "lexical_declaration":
|
||||
case "variable_declaration": {
|
||||
this.extractVariableDeclarations(child, functions);
|
||||
for (let k = 0; k < child.childCount; k++) {
|
||||
const declarator = child.child(k);
|
||||
if (
|
||||
declarator &&
|
||||
declarator.type === "variable_declarator"
|
||||
) {
|
||||
const nameNode =
|
||||
declarator.childForFieldName("name");
|
||||
if (
|
||||
nameNode &&
|
||||
!exportedNames.has(nameNode.text)
|
||||
) {
|
||||
exports.push({
|
||||
name: nameNode.text,
|
||||
lineNumber: node.startPosition.row + 1,
|
||||
});
|
||||
exportedNames.add(nameNode.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "export_clause": {
|
||||
for (let k = 0; k < child.childCount; k++) {
|
||||
const spec = child.child(k);
|
||||
if (spec && spec.type === "export_specifier") {
|
||||
const alias = spec.childForFieldName("alias");
|
||||
const name = spec.childForFieldName("name");
|
||||
const exportName = alias
|
||||
? alias.text
|
||||
: name
|
||||
? name.text
|
||||
: spec.text;
|
||||
if (!exportedNames.has(exportName)) {
|
||||
exports.push({
|
||||
name: exportName,
|
||||
lineNumber: node.startPosition.row + 1,
|
||||
});
|
||||
exportedNames.add(exportName);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+67
@@ -16,6 +16,16 @@ importers:
|
||||
version: 3.2.4(@types/node@25.5.0)
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
tree-sitter-javascript:
|
||||
specifier: ^0.25.0
|
||||
version: 0.25.0
|
||||
tree-sitter-typescript:
|
||||
specifier: ^0.23.2
|
||||
version: 0.23.2
|
||||
web-tree-sitter:
|
||||
specifier: ^0.26.6
|
||||
version: 0.26.6
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^25.5.0
|
||||
@@ -429,6 +439,14 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
node-addon-api@8.6.0:
|
||||
resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@@ -490,6 +508,30 @@ packages:
|
||||
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tree-sitter-javascript@0.23.1:
|
||||
resolution: {integrity: sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==}
|
||||
peerDependencies:
|
||||
tree-sitter: ^0.21.1
|
||||
peerDependenciesMeta:
|
||||
tree-sitter:
|
||||
optional: true
|
||||
|
||||
tree-sitter-javascript@0.25.0:
|
||||
resolution: {integrity: sha512-1fCbmzAskZkxcZzN41sFZ2br2iqTYP3tKls1b/HKGNPQUVOpsUxpmGxdN/wMqAk3jYZnYBR1dd/y/0avMeU7dw==}
|
||||
peerDependencies:
|
||||
tree-sitter: ^0.25.0
|
||||
peerDependenciesMeta:
|
||||
tree-sitter:
|
||||
optional: true
|
||||
|
||||
tree-sitter-typescript@0.23.2:
|
||||
resolution: {integrity: sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==}
|
||||
peerDependencies:
|
||||
tree-sitter: ^0.21.0
|
||||
peerDependenciesMeta:
|
||||
tree-sitter:
|
||||
optional: true
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -571,6 +613,9 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
web-tree-sitter@0.26.6:
|
||||
resolution: {integrity: sha512-fSPR7VBW/fZQdUSp/bXTDLT+i/9dwtbnqgEBMzowrM4U3DzeCwDbY3MKo0584uQxID4m/1xpLflrlT/rLIRPew==}
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -864,6 +909,10 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
node-addon-api@8.6.0: {}
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pathval@2.0.1: {}
|
||||
@@ -936,6 +985,22 @@ snapshots:
|
||||
|
||||
tinyspy@4.0.4: {}
|
||||
|
||||
tree-sitter-javascript@0.23.1:
|
||||
dependencies:
|
||||
node-addon-api: 8.6.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
tree-sitter-javascript@0.25.0:
|
||||
dependencies:
|
||||
node-addon-api: 8.6.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
tree-sitter-typescript@0.23.2:
|
||||
dependencies:
|
||||
node-addon-api: 8.6.0
|
||||
node-gyp-build: 4.8.4
|
||||
tree-sitter-javascript: 0.23.1
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
@@ -1014,6 +1079,8 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
web-tree-sitter@0.26.6: {}
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
|
||||
Reference in New Issue
Block a user