mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(core): DartExtractor — top-level function extraction
Add TDD tests and implement extractTopLevelFunction with helpers for extracting function name, params, and return type (including generics where the grammar emits type_identifier + type_arguments as siblings). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
072bab798c
commit
136f85c1c8
+77
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { createRequire } from "node:module";
|
||||
import { DartExtractor } from "../dart-extractor.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
let Parser: any;
|
||||
let Language: any;
|
||||
let dartLang: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
const mod = await import("web-tree-sitter");
|
||||
Parser = mod.Parser;
|
||||
Language = mod.Language;
|
||||
await Parser.init();
|
||||
const wasmPath = require.resolve(
|
||||
"@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm",
|
||||
);
|
||||
dartLang = await Language.load(wasmPath);
|
||||
});
|
||||
|
||||
function parse(code: string) {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(dartLang);
|
||||
const tree = parser.parse(code);
|
||||
const root = tree.rootNode;
|
||||
return { tree, parser, root };
|
||||
}
|
||||
|
||||
describe("DartExtractor", () => {
|
||||
const extractor = new DartExtractor();
|
||||
|
||||
it("has correct languageIds", () => {
|
||||
expect(extractor.languageIds).toEqual(["dart"]);
|
||||
});
|
||||
|
||||
describe("extractStructure - functions", () => {
|
||||
it("extracts a simple top-level function with params and return type", () => {
|
||||
const { tree, parser, root } = parse(`int add(int a, int b) => a + b;\n`);
|
||||
const result = extractor.extractStructure(root);
|
||||
|
||||
expect(result.functions).toHaveLength(1);
|
||||
expect(result.functions[0].name).toBe("add");
|
||||
expect(result.functions[0].params).toEqual(["a", "b"]);
|
||||
expect(result.functions[0].returnType).toBe("int");
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
});
|
||||
|
||||
it("extracts a function with no params and void return type", () => {
|
||||
const { tree, parser, root } = parse(`void noop() {}\n`);
|
||||
const result = extractor.extractStructure(root);
|
||||
|
||||
expect(result.functions).toHaveLength(1);
|
||||
expect(result.functions[0].name).toBe("noop");
|
||||
expect(result.functions[0].params).toEqual([]);
|
||||
expect(result.functions[0].returnType).toBe("void");
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
});
|
||||
|
||||
it("extracts an async function with a generic return type", () => {
|
||||
const { tree, parser, root } = parse(`Future<String> fetch(String url) async { return ""; }\n`);
|
||||
const result = extractor.extractStructure(root);
|
||||
|
||||
expect(result.functions).toHaveLength(1);
|
||||
expect(result.functions[0].name).toBe("fetch");
|
||||
expect(result.functions[0].params).toEqual(["url"]);
|
||||
expect(result.functions[0].returnType).toBe("Future<String>");
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,79 @@ function isExported(name: string): boolean {
|
||||
return !name.startsWith("_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the identifier name from a `function_signature` node.
|
||||
*
|
||||
* NOTE: for `method_signature` (class-body method declarations), callers
|
||||
* must first unwrap to the inner `function_signature` child before invoking
|
||||
* this helper — the Dart grammar layers `method_signature > function_signature`
|
||||
* and `findChild(..., "identifier")` would otherwise miss the function name.
|
||||
*/
|
||||
function extractFunctionName(sig: TreeSitterNode): string | null {
|
||||
const id = findChild(sig, "identifier");
|
||||
return id ? id.text : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameter names from a `formal_parameter_list`. Each
|
||||
* `formal_parameter` child carries the parameter name as its `identifier`
|
||||
* child; we ignore the type annotation.
|
||||
*
|
||||
* Currently only required positional parameters (`formal_parameter` direct
|
||||
* children) are surfaced. Dart's optional positional (`[...]`) and named
|
||||
* (`{...}`) parameters are wrapped in `optional_formal_parameters` and
|
||||
* `named_parameter_list` container nodes respectively; supporting those is
|
||||
* left for a follow-up — the project-graph use case does not currently
|
||||
* distinguish parameter kinds.
|
||||
*/
|
||||
function extractParams(sig: TreeSitterNode): string[] {
|
||||
const params: string[] = [];
|
||||
const paramList = findChild(sig, "formal_parameter_list");
|
||||
if (!paramList) return params;
|
||||
for (const p of findChildren(paramList, "formal_parameter")) {
|
||||
const id = findChild(p, "identifier");
|
||||
if (id) params.push(id.text);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the return type from a function_signature. The return type is the
|
||||
* sequence of NAMED children that appear before the function name
|
||||
* (`identifier`) or `formal_parameter_list`. If there is no such child, the
|
||||
* function has no declared return type (Dart infers it).
|
||||
*
|
||||
* Common shapes seen during AST probing:
|
||||
* `int add(int a, int b)` → [type_identifier "int"]
|
||||
* `void noop()` → [void_type]
|
||||
* `Future<String> fetch()`→ [type_identifier "Future", type_arguments "<String>"]
|
||||
*
|
||||
* For generic types the grammar emits the base type and the type arguments as
|
||||
* separate sibling nodes, so we collect ALL nodes before `identifier` and
|
||||
* concatenate their text to reconstruct the full type spelling.
|
||||
*/
|
||||
function extractReturnType(sig: TreeSitterNode): string | undefined {
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < sig.childCount; i++) {
|
||||
const child = sig.child(i);
|
||||
if (!child || !child.isNamed) continue;
|
||||
if (
|
||||
child.type === "identifier" ||
|
||||
child.type === "formal_parameter_list" ||
|
||||
child.type === "type_parameters"
|
||||
) {
|
||||
// Reached the function NAME (`identifier`), the parameter list, or the
|
||||
// generic-parameter list (`type_parameters` is the function's own
|
||||
// generics, e.g. `<T>` in `T fn<T>(T x)`). Anything we passed before
|
||||
// this point WAS the return type; if we hit this stop without having
|
||||
// collected anything, the function has no declared return type.
|
||||
break;
|
||||
}
|
||||
parts.push(child.text);
|
||||
}
|
||||
return parts.length > 0 ? parts.join("") : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dart extractor for tree-sitter structural analysis + call graph.
|
||||
*
|
||||
@@ -31,15 +104,40 @@ export class DartExtractor implements LanguageExtractor {
|
||||
const imports: StructuralAnalysis["imports"] = [];
|
||||
const exports: StructuralAnalysis["exports"] = [];
|
||||
|
||||
// Implementation lands in subsequent tasks.
|
||||
void rootNode;
|
||||
void findChild;
|
||||
void findChildren;
|
||||
void isExported;
|
||||
for (let i = 0; i < rootNode.childCount; i++) {
|
||||
const node = rootNode.child(i);
|
||||
if (!node) continue;
|
||||
|
||||
switch (node.type) {
|
||||
case "function_signature":
|
||||
this.extractTopLevelFunction(node, functions, exports);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { functions, classes, imports, exports };
|
||||
}
|
||||
|
||||
// ---- Private helpers ----
|
||||
|
||||
private extractTopLevelFunction(
|
||||
sig: TreeSitterNode,
|
||||
functions: StructuralAnalysis["functions"],
|
||||
exports: StructuralAnalysis["exports"],
|
||||
): void {
|
||||
const name = extractFunctionName(sig);
|
||||
if (!name) return;
|
||||
functions.push({
|
||||
name,
|
||||
lineRange: [sig.startPosition.row + 1, sig.endPosition.row + 1],
|
||||
params: extractParams(sig),
|
||||
returnType: extractReturnType(sig),
|
||||
});
|
||||
if (isExported(name)) {
|
||||
exports.push({ name, lineNumber: sig.startPosition.row + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
|
||||
// Implementation lands in a later task.
|
||||
void rootNode;
|
||||
|
||||
Reference in New Issue
Block a user