diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts new file mode 100644 index 0000000..ce44fc9 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -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 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"); + + tree.delete(); + parser.delete(); + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index a8724f4..94de626 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -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 fetch()`→ [type_identifier "Future", type_arguments ""] + * + * 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. `` in `T fn(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;