From dc5d174092edbf7614f1939f7fbcc0f7ef826812 Mon Sep 17 00:00:00 2001 From: Tirth Kanani Date: Sun, 14 Jun 2026 18:01:57 +0100 Subject: [PATCH] fix(typescript-extractor): capture parenless arrow params and abstract classes A single-parameter arrow function written without parentheses (`x => f(x)`) exposes its lone parameter under the `parameter` (singular) field, not inside a `formal_parameters` node, so the extractor silently dropped it. Now read that field first. `abstract class Foo {}` parses as `abstract_class_declaration`, a node type neither processTopLevelNode nor processExportStatement matched, so abstract classes (and their methods/exports) vanished from the structural graph. Both switches now handle `abstract_class_declaration` alongside `class_declaration`; extractClass works unchanged. Adds typescript-extractor.test.ts covering both cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/typescript-extractor.test.ts | 99 +++++++++++++++++++ .../extractors/typescript-extractor.ts | 21 ++-- 2 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/typescript-extractor.test.ts diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/typescript-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/typescript-extractor.test.ts new file mode 100644 index 00000000..17570d1f --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/typescript-extractor.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { createRequire } from "node:module"; +import { TypeScriptExtractor } from "../typescript-extractor.js"; + +const require = createRequire(import.meta.url); + +// Load tree-sitter + TypeScript grammar once +let Parser: any; +let Language: any; +let tsLang: 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-typescript/tree-sitter-typescript.wasm", + ); + tsLang = await Language.load(wasmPath); +}); + +function parse(code: string) { + const parser = new Parser(); + parser.setLanguage(tsLang); + const tree = parser.parse(code); + const root = tree.rootNode; + return { tree, parser, root }; +} + +describe("TypeScriptExtractor", () => { + const extractor = new TypeScriptExtractor(); + + it("has correct languageIds", () => { + expect(extractor.languageIds).toEqual(["typescript", "javascript"]); + }); + + describe("extractStructure - arrow functions", () => { + it("captures the param of a parenless single-param arrow function", () => { + const { tree, parser, root } = parse(`const g = x => doThing(x);`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("g"); + // Today: params is [] because the lone param lives under field + // `parameter` (singular) and is not wrapped in `formal_parameters`. + expect(result.functions[0].params).toEqual(["x"]); + + tree.delete(); + parser.delete(); + }); + + it("still captures parenthesised arrow params", () => { + const { tree, parser, root } = parse( + `const add = (a, b) => a + b;`, + ); + const result = extractor.extractStructure(root); + + expect(result.functions[0].name).toBe("add"); + expect(result.functions[0].params).toEqual(["a", "b"]); + + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - abstract classes", () => { + it("extracts an exported abstract class and its methods", () => { + const { tree, parser, root } = parse( + `export abstract class Service { run(): void {} }`, + ); + const result = extractor.extractStructure(root); + + // Today: classes is [] and exports is [] because the node type is + // `abstract_class_declaration`, not `class_declaration`. + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Service"); + expect(result.classes[0].methods).toEqual(["run"]); + expect(result.exports.some((e) => e.name === "Service")).toBe(true); + + tree.delete(); + parser.delete(); + }); + + it("extracts a non-exported abstract class", () => { + const { tree, parser, root } = parse( + `abstract class Foo { bar(): void {} }`, + ); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Foo"); + expect(result.classes[0].methods).toEqual(["bar"]); + + tree.delete(); + parser.delete(); + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/typescript-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/typescript-extractor.ts index f8dd4810..8afecfaf 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/typescript-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/typescript-extractor.ts @@ -209,6 +209,7 @@ export class TypeScriptExtractor implements LanguageExtractor { break; case "class_declaration": + case "abstract_class_declaration": this.extractClass(node, classes); break; @@ -330,13 +331,16 @@ export class TypeScriptExtractor implements LanguageExtractor { valueNode.type === "function_expression" || valueNode.type === "function") ) { - const params = extractParams( - valueNode.childForFieldName("parameters") ?? - valueNode.children.find( - (c) => c.type === "formal_parameters", - ) ?? - null, - ); + const singleParam = valueNode.childForFieldName("parameter"); + const params = singleParam + ? [singleParam.text] + : extractParams( + valueNode.childForFieldName("parameters") ?? + valueNode.children.find( + (c) => c.type === "formal_parameters", + ) ?? + null, + ); const returnType = extractReturnType(valueNode); functions.push({ @@ -416,7 +420,8 @@ export class TypeScriptExtractor implements LanguageExtractor { break; } - case "class_declaration": { + case "class_declaration": + case "abstract_class_declaration": { this.extractClass(child, classes); const nameNode = child.children.find( (c) =>