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:
thejesh
2026-06-13 04:58:56 -07:00
co-authored by Claude Sonnet 4.6
parent 072bab798c
commit 136f85c1c8
2 changed files with 180 additions and 5 deletions
@@ -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;