diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b4a2751..3f018c3 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/.copilot-plugin/plugin.json b/.copilot-plugin/plugin.json index bad1eb7..7d55745 100644 --- a/.copilot-plugin/plugin.json +++ b/.copilot-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 2a73c7d..d6f6791 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "understand-anything", "displayName": "Understand Anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b9b06e..37350c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@tree-sitter-grammars/tree-sitter-kotlin': specifier: 1.1.0 version: 1.1.0 + '@understand-anything/tree-sitter-dart-wasm': + specifier: workspace:* + version: link:../tree-sitter-dart-wasm fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -198,6 +201,8 @@ importers: specifier: ^3.1.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + understand-anything-plugin/packages/tree-sitter-dart-wasm: {} + packages: '@ampproject/remapping@2.3.0': diff --git a/understand-anything-plugin/.claude-plugin/plugin.json b/understand-anything-plugin/.claude-plugin/plugin.json index b4a2751..3f018c3 100644 --- a/understand-anything-plugin/.claude-plugin/plugin.json +++ b/understand-anything-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index a47eaac..67b9633 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@understand-anything/skill", - "version": "2.7.7", + "version": "2.8.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/understand-anything-plugin/packages/core/package.json b/understand-anything-plugin/packages/core/package.json index 7ea50ae..e54ce72 100644 --- a/understand-anything-plugin/packages/core/package.json +++ b/understand-anything-plugin/packages/core/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@tree-sitter-grammars/tree-sitter-kotlin": "1.1.0", + "@understand-anything/tree-sitter-dart-wasm": "workspace:*", "fuse.js": "^7.1.0", "ignore": "^7.0.5", "tree-sitter-c-sharp": "^0.23.1", diff --git a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts index 7a3c774..4ecad11 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts @@ -49,10 +49,10 @@ describe("LanguageRegistry", () => { }); describe("createDefault", () => { - it("registers all 40 built-in language configs", () => { + it("registers all 41 built-in language configs", () => { const registry = LanguageRegistry.createDefault(); const all = registry.getAllLanguages(); - expect(all.length).toBe(40); + expect(all.length).toBe(41); }); it("maps all expected extensions", () => { diff --git a/understand-anything-plugin/packages/core/src/languages/configs/dart.ts b/understand-anything-plugin/packages/core/src/languages/configs/dart.ts new file mode 100644 index 0000000..2d94a23 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/dart.ts @@ -0,0 +1,29 @@ +import type { LanguageConfig } from "../types.js"; + +export const dartConfig = { + id: "dart", + displayName: "Dart", + extensions: [".dart"], + treeSitter: { + wasmPackage: "@understand-anything/tree-sitter-dart-wasm", + wasmFile: "tree-sitter-dart.wasm", + }, + concepts: [ + "null safety", + "mixins", + "extensions", + "isolates", + "async/await", + "streams", + "factory constructors", + "named constructors", + "records", + "sealed classes", + ], + filePatterns: { + entryPoints: ["lib/main.dart", "bin/*.dart"], + barrels: ["lib/*.dart"], + tests: ["test/**/*_test.dart"], + config: ["pubspec.yaml", "analysis_options.yaml"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/index.ts b/understand-anything-plugin/packages/core/src/languages/configs/index.ts index 4893c4d..6a949e8 100644 --- a/understand-anything-plugin/packages/core/src/languages/configs/index.ts +++ b/understand-anything-plugin/packages/core/src/languages/configs/index.ts @@ -11,6 +11,7 @@ import { swiftConfig } from "./swift.js"; import { kotlinConfig } from "./kotlin.js"; import { cConfig } from "./c.js"; import { cppConfig } from "./cpp.js"; +import { dartConfig } from "./dart.js"; import { csharpConfig } from "./csharp.js"; import { luaConfig } from "./lua.js"; // Non-code language configs @@ -56,6 +57,7 @@ export const builtinLanguageConfigs: LanguageConfig[] = [ luaConfig, cConfig, cppConfig, + dartConfig, csharpConfig, // Non-code languages markdownConfig, @@ -101,6 +103,7 @@ export { luaConfig, cConfig, cppConfig, + dartConfig, csharpConfig, // Non-code languages markdownConfig, 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..dd69c9b --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -0,0 +1,675 @@ +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(); + }); + }); + + describe("extractStructure - parameter kinds", () => { + it("surfaces optional positional parameters", () => { + const { tree, parser, root } = parse(`void show([String? title, int count = 0]) {}\n`); + const result = extractor.extractStructure(root); + + expect(result.functions[0].params).toEqual(["title", "count"]); + tree.delete(); + parser.delete(); + }); + + it("surfaces named parameters wrapped in {...}", () => { + const { tree, parser, root } = parse(`void show({String? title, int count = 0}) {}\n`); + const result = extractor.extractStructure(root); + + expect(result.functions[0].params).toEqual(["title", "count"]); + tree.delete(); + parser.delete(); + }); + + it("mixes required and named parameters in one signature", () => { + const { tree, parser, root } = parse(`String join(List items, {String sep = ","}) => "";\n`); + const result = extractor.extractStructure(root); + + expect(result.functions[0].params).toEqual(["items", "sep"]); + tree.delete(); + parser.delete(); + }); + + it("extracts `this.field` constructor parameters as the field name", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + String y; + Foo(this.x, this.y); +} +`); + const result = extractor.extractStructure(root); + + const ctor = result.functions.find((f) => f.name === "Foo"); + expect(ctor).toBeDefined(); + expect(ctor!.params).toEqual(["x", "y"]); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - classes", () => { + it("extracts a class with fields and methods", () => { + const { tree, parser, root } = parse(`class Counter { + int count = 0; + String? label; + void increment() { count++; } + int get value => count; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Counter"); + expect(result.classes[0].methods).toContain("increment"); + // method declarations land in functions[] too (matching Kotlin convention) + expect(result.functions.map((f) => f.name)).toContain("increment"); + // Field extraction: `int count = 0;` and `String? label;` both parse as + // declaration > initialized_identifier_list > initialized_identifier > identifier + expect(result.classes[0].properties).toEqual( + expect.arrayContaining(["count", "label"]), + ); + // Getters are surfaced as methods (`int get value` → "value"). + expect(result.classes[0].methods).toContain("value"); + + tree.delete(); + parser.delete(); + }); + + it("extracts an empty class", () => { + const { tree, parser, root } = parse(`class Empty {}\n`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Empty"); + expect(result.classes[0].methods).toEqual([]); + + tree.delete(); + parser.delete(); + }); + + it("extracts an abstract class with method requirements", () => { + const { tree, parser, root } = parse(`abstract class Shape { + double area(); +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Shape"); + expect(result.classes[0].methods).toContain("area"); + + tree.delete(); + parser.delete(); + }); + + it("extracts a class with extends + with + implements clauses", () => { + const { tree, parser, root } = parse(`class Square extends Shape with Comparable implements Cloneable { + double side; + Square(this.side); + double area() => side * side; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Square"); + expect(result.classes[0].methods).toContain("area"); + + tree.delete(); + parser.delete(); + }); + + it("extracts comma-list field declarations as separate properties", () => { + const { tree, parser, root } = parse(`class Foo { int a, b, c; }\n`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].properties).toEqual(["a", "b", "c"]); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - getters and setters", () => { + it("surfaces a concrete getter as a method", () => { + const { tree, parser, root } = parse(`class Counter { + int _v = 0; + int get value => _v; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toContain("value"); + expect(result.functions.map((f) => f.name)).toContain("value"); + tree.delete(); + parser.delete(); + }); + + it("surfaces a concrete setter as a method", () => { + const { tree, parser, root } = parse(`class Counter { + int _v = 0; + set value(int x) => _v = x; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toContain("value"); + expect(result.functions.map((f) => f.name)).toContain("value"); + tree.delete(); + parser.delete(); + }); + + it("surfaces an abstract getter as a method", () => { + const { tree, parser, root } = parse(`abstract class Shape { + double get area; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toContain("area"); + tree.delete(); + parser.delete(); + }); + + it("surfaces an abstract setter as a method", () => { + const { tree, parser, root } = parse(`abstract class Box { + set width(int w); +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toContain("width"); + tree.delete(); + parser.delete(); + }); + + it("does NOT export an underscore-prefixed getter", () => { + const { tree, parser, root } = parse(`class Counter { + int _v = 0; + int get _internal => _v; + int get visible => _v; +} +`); + const result = extractor.extractStructure(root); + + const names = result.exports.map((e) => e.name); + expect(names).toContain("visible"); + expect(names).not.toContain("_internal"); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - constructors", () => { + it("treats an unnamed constructor as a method named after the class", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + Foo(this.x); +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toContain("Foo"); + tree.delete(); + parser.delete(); + }); + + it("treats a named constructor as Class.named", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + Foo.zero() : x = 0; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toContain("Foo.zero"); + tree.delete(); + parser.delete(); + }); + + it("treats a factory named constructor as Class.named", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + Foo(this.x); + factory Foo.fromString(String s) => Foo(int.parse(s)); +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].methods).toContain("Foo.fromString"); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - mixins", () => { + it("extracts a plain mixin as a class-like entry", () => { + const { tree, parser, root } = parse(`mixin Walker { + void walk() {} +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Walker"); + expect(result.classes[0].methods).toContain("walk"); + tree.delete(); + parser.delete(); + }); + + it("extracts a mixin with an `on` constraint", () => { + const { tree, parser, root } = parse(`mixin Runner on Walker { + void run() {} +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes[0].name).toBe("Runner"); + expect(result.classes[0].methods).toContain("run"); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - enums", () => { + it("extracts a simple enum and surfaces its constants as properties", () => { + const { tree, parser, root } = parse(`enum Color { red, green, blue }\n`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Color"); + expect(result.classes[0].properties).toEqual(["red", "green", "blue"]); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - extensions", () => { + it("extracts a named extension on String", () => { + const { tree, parser, root } = parse(`extension StringX on String { + String shout() => toUpperCase() + '!'; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("StringX"); + expect(result.classes[0].methods).toContain("shout"); + tree.delete(); + parser.delete(); + }); + + it("names an anonymous extension after its target type", () => { + const { tree, parser, root } = parse(`extension on int { + int squared() => this * this; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + // Anonymous extension on int → "on int" so it isn't dropped. + expect(result.classes[0].name).toBe("on int"); + expect(result.classes[0].methods).toContain("squared"); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - imports", () => { + it("extracts a package import with no specifiers", () => { + const { tree, parser, root } = parse(`import 'package:flutter/material.dart';\n`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(1); + expect(result.imports[0].source).toBe("package:flutter/material.dart"); + expect(result.imports[0].specifiers).toEqual([]); + tree.delete(); + parser.delete(); + }); + + it("extracts a relative import", () => { + const { tree, parser, root } = parse(`import './foo.dart';\n`); + const result = extractor.extractStructure(root); + + expect(result.imports[0].source).toBe("./foo.dart"); + tree.delete(); + parser.delete(); + }); + + it("extracts a `show` clause as specifiers", () => { + const { tree, parser, root } = parse(`import 'foo.dart' show Bar, Baz;\n`); + const result = extractor.extractStructure(root); + + expect(result.imports[0].source).toBe("foo.dart"); + expect(result.imports[0].specifiers).toEqual(["Bar", "Baz"]); + tree.delete(); + parser.delete(); + }); + + it("extracts an `as` prefix as the sole specifier", () => { + const { tree, parser, root } = parse(`import 'bar.dart' as b;\n`); + const result = extractor.extractStructure(root); + + expect(result.imports[0].source).toBe("bar.dart"); + expect(result.imports[0].specifiers).toEqual(["b"]); + tree.delete(); + parser.delete(); + }); + + it("does NOT include `hide` names as specifiers", () => { + const { tree, parser, root } = parse(`import 'foo.dart' hide Qux;\n`); + const result = extractor.extractStructure(root); + + expect(result.imports[0].source).toBe("foo.dart"); + expect(result.imports[0].specifiers).toEqual([]); + tree.delete(); + parser.delete(); + }); + + it("extracts a `dart:` SDK import", () => { + const { tree, parser, root } = parse(`import 'dart:io';\n`); + const result = extractor.extractStructure(root); + + expect(result.imports).toHaveLength(1); + expect(result.imports[0].source).toBe("dart:io"); + expect(result.imports[0].specifiers).toEqual([]); + tree.delete(); + parser.delete(); + }); + + it("preserves declaration order across multiple imports", () => { + const { tree, parser, root } = parse(`import 'dart:io'; +import 'package:flutter/material.dart'; +import './foo.dart'; +`); + const result = extractor.extractStructure(root); + + expect(result.imports.map((i) => i.source)).toEqual([ + "dart:io", + "package:flutter/material.dart", + "./foo.dart", + ]); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - exports", () => { + it("extracts a top-level export directive", () => { + const { tree, parser, root } = parse(`export 'shared.dart';\n`); + const result = extractor.extractStructure(root); + + const sharedExport = result.exports.find((e) => e.name === "shared.dart"); + expect(sharedExport).toBeDefined(); + tree.delete(); + parser.delete(); + }); + + it("extracts a `show` clause on an export directive (URI only)", () => { + const { tree, parser, root } = parse(`export 'shared.dart' show PublicApi;\n`); + const result = extractor.extractStructure(root); + + // We surface the export URI in exports[]; the show-list refinement is + // not modeled in the shared schema (export entries carry name + line). + const sharedExport = result.exports.find((e) => e.name === "shared.dart"); + expect(sharedExport).toBeDefined(); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractCallGraph", () => { + it("attributes a top-level call to its enclosing function", () => { + const { tree, parser, root } = parse(`int helper() => 1; +int caller() { + return helper(); +} +`); + const entries = extractor.extractCallGraph(root); + + const helperCall = entries.find((e) => e.callee === "helper"); + expect(helperCall).toBeDefined(); + expect(helperCall!.caller).toBe("caller"); + tree.delete(); + parser.delete(); + }); + + it("attributes a method call (x.foo()) to its enclosing function", () => { + const { tree, parser, root } = parse(`void run() { + "hi".toUpperCase(); +} +`); + const entries = extractor.extractCallGraph(root); + + const callees = entries.map((e) => e.callee); + expect(callees).toContain("toUpperCase"); + tree.delete(); + parser.delete(); + }); + + it("returns an empty array when there are no calls", () => { + const { tree, parser, root } = parse(`int a() => 1;\n`); + const entries = extractor.extractCallGraph(root); + expect(entries).toEqual([]); + tree.delete(); + parser.delete(); + }); + + it("records a `const Foo()` constructor as a call edge (Flutter widget shape)", () => { + const { tree, parser, root } = parse(`void main() { + runApp(const MyApp()); +} +`); + const entries = extractor.extractCallGraph(root); + + const callees = entries.map((e) => e.callee); + // Both the enclosing `runApp` call and the inner `MyApp` construction + // must surface — the latter is the dependency edge that motivates + // Flutter call-graph support. + expect(callees).toContain("runApp"); + expect(callees).toContain("MyApp"); + const myAppCall = entries.find((e) => e.callee === "MyApp"); + expect(myAppCall!.caller).toBe("main"); + tree.delete(); + parser.delete(); + }); + + it("records a `new Foo()` constructor as a call edge", () => { + const { tree, parser, root } = parse(`void main() { + var x = new Counter(1); +} +`); + const entries = extractor.extractCallGraph(root); + + const counterCall = entries.find((e) => e.callee === "Counter"); + expect(counterCall).toBeDefined(); + expect(counterCall!.caller).toBe("main"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a getter body to the getter", () => { + const { tree, parser, root } = parse(`class C { + int _v = 0; + int get value { + return helper(); + } +} +`); + const entries = extractor.extractCallGraph(root); + + const helperCall = entries.find((e) => e.callee === "helper"); + expect(helperCall).toBeDefined(); + expect(helperCall!.caller).toBe("value"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a setter body to the setter", () => { + const { tree, parser, root } = parse(`class C { + int _v = 0; + set value(int x) { + _v = clamp(x); + } +} +`); + const entries = extractor.extractCallGraph(root); + + const clampCall = entries.find((e) => e.callee === "clamp"); + expect(clampCall).toBeDefined(); + expect(clampCall!.caller).toBe("value"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a constructor body to the constructor", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + Foo(this.x) { + validate(x); + } +} +`); + const entries = extractor.extractCallGraph(root); + + const validateCall = entries.find((e) => e.callee === "validate"); + expect(validateCall).toBeDefined(); + expect(validateCall!.caller).toBe("Foo"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a factory constructor body to `Class.named`", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + Foo(this.x); + factory Foo.fromString(String s) { + return Foo(int.parse(s)); + } +} +`); + const entries = extractor.extractCallGraph(root); + + // Either the bare `Foo(...)` call inside the factory or the chained + // `int.parse(...)` must be attributed to the factory's qualified name. + const fromFactory = entries.filter((e) => e.caller === "Foo.fromString"); + expect(fromFactory.length).toBeGreaterThan(0); + tree.delete(); + parser.delete(); + }); + }); + + describe("extractStructure - visibility", () => { + it("does NOT export a top-level declaration whose name starts with _", () => { + const { tree, parser, root } = parse(`int _helper() => 1; +class _PrivateImpl {} +`); + const result = extractor.extractStructure(root); + + const names = result.exports.map((e) => e.name); + expect(names).not.toContain("_helper"); + expect(names).not.toContain("_PrivateImpl"); + tree.delete(); + parser.delete(); + }); + + it("DOES export a top-level declaration without an underscore prefix", () => { + const { tree, parser, root } = parse(`int helper() => 1; +class Public {} +`); + const result = extractor.extractStructure(root); + + const names = result.exports.map((e) => e.name); + expect(names).toEqual(expect.arrayContaining(["helper", "Public"])); + tree.delete(); + parser.delete(); + }); + + it("does NOT export class members whose names start with _", () => { + const { tree, parser, root } = parse(`class Counter { + void _helper() {} + void publicMethod() {} +} +`); + const result = extractor.extractStructure(root); + + const names = result.exports.map((e) => e.name); + expect(names).toContain("publicMethod"); + expect(names).not.toContain("_helper"); + 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 new file mode 100644 index 0000000..5605e40 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -0,0 +1,736 @@ +import type { StructuralAnalysis, CallGraphEntry } from "../../types.js"; +import type { LanguageExtractor, TreeSitterNode } from "./types.js"; +import { findChild, findChildren, getStringValue } from "./base-extractor.js"; + +/** + * Whether a Dart name is exported. + * + * Dart's visibility rule is name-based and the INVERSE of Kotlin's: names + * starting with `_` are library-private, everything else is exported. There + * is no `public` / `private` keyword to inspect — only the leading character. + */ +function isExported(name: string): boolean { + return !name.startsWith("_"); +} + +/** + * Extract the identifier name from a `function_signature` node. + * + * NOTE: this helper expects a `function_signature` node. The Dart grammar + * wraps the function_signature inside two different parent shapes: + * - `method_signature > function_signature` for CONCRETE class methods. + * - `declaration > function_signature` for ABSTRACT class methods (no body). + * Callers (`collectClassBody`) unwrap to the inner `function_signature` + * before invoking this helper. + */ +function extractFunctionName(sig: TreeSitterNode): string | null { + const id = findChild(sig, "identifier"); + return id ? id.text : null; +} + +/** + * Extract the user-visible name from a `formal_parameter` (or one of its + * specialized children). + * + * Three shapes seen in the AST: + * - Regular `Type name` → `formal_parameter > { type_identifier, identifier }` + * - This-init `this.field` → `formal_parameter > constructor_param > { this, ., identifier }` + * - Super-init `super.field` → `formal_parameter > super_formal_parameter > { super, ., identifier }` + * + * Strategy: scan all direct children for an `identifier`; if absent, recurse + * one level into `constructor_param` / `super_formal_parameter` and pick the + * LAST identifier (the field name in `this.field`). + */ +function extractParamName(paramNode: TreeSitterNode): string | null { + // Direct identifier child wins (regular `Type name` parameter). + const direct = findChild(paramNode, "identifier"); + if (direct) return direct.text; + + // Nested wrappers — pick the last identifier we can find inside. + for (let i = 0; i < paramNode.childCount; i++) { + const child = paramNode.child(i); + if (!child) continue; + if (child.type === "constructor_param" || child.type === "super_formal_parameter") { + let last: string | null = null; + for (let j = 0; j < child.childCount; j++) { + const inner = child.child(j); + if (inner && inner.type === "identifier") last = inner.text; + } + if (last) return last; + } + } + return null; +} + +/** + * Extract parameter names from a `formal_parameter_list`. + * + * Walks both required parameters (`formal_parameter` direct children) and the + * `optional_formal_parameters` wrapper, which the Dart grammar uses for BOTH + * optional positional `[...]` and named `{...}` parameters (the leading + * unnamed `[` vs `{` token distinguishes them — we don't need to for the + * project graph, both go into the same `params[]` list). + * + * Drops `this.x` and `super.x` initializer parameters' types and surfaces + * just the field name (see `extractParamName`). + */ +function extractParams(sig: TreeSitterNode): string[] { + const params: string[] = []; + const list = findChild(sig, "formal_parameter_list"); + if (!list) return params; + for (let i = 0; i < list.childCount; i++) { + const child = list.child(i); + if (!child) continue; + if (child.type === "formal_parameter") { + const name = extractParamName(child); + if (name) params.push(name); + } else if (child.type === "optional_formal_parameters") { + // Walk one level deeper — children are again `formal_parameter`. + for (const sub of findChildren(child, "formal_parameter")) { + const name = extractParamName(sub); + if (name) params.push(name); + } + } + } + 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; +} + +/** + * Push a method/function entry. Used by `collectClassBody` for both + * `method_signature` and `declaration > function_signature` shapes so a + * future change to the entry's fields lands in one place. + */ +function pushMethod( + declNode: TreeSitterNode, + sig: TreeSitterNode, + name: string, + methods: string[], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], +): void { + methods.push(name); + functions.push({ + name, + lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1], + params: extractParams(sig), + returnType: extractReturnType(sig), + }); + if (isExported(name)) { + exports.push({ name, lineNumber: declNode.startPosition.row + 1 }); + } +} + +/** + * Unwrap the string-literal text from `uri > string_literal` via + * `base-extractor.getStringValue` so the quote-stripping logic lives in + * exactly one place across all extractors. + */ +function uriText(uriNode: TreeSitterNode): string | null { + const lit = findChild(uriNode, "string_literal"); + if (!lit) return null; + return getStringValue(lit); +} + +/** + * Build a constructor's method-graph name from a constructor_signature / + * factory_constructor_signature node: + * - one identifier → unnamed constructor, name = "" + * - two identifiers → named constructor, name = "." + * + * Returns null when no identifier is present (defensive — should not happen + * for a real constructor declaration). + * + * Probe findings (2026-06-13): the plan's claimed AST shapes match exactly. + * - Unnamed: constructor_signature { identifier[Foo], formal_parameter_list } + * - Named: constructor_signature { identifier[Foo], identifier[zero], formal_parameter_list, ... } + * - Factory: factory_constructor_signature { , identifier[Foo], identifier[fromString], formal_parameter_list } + * extractReturnType returns undefined for all three (factory keyword is unnamed, + * so it is skipped; the loop stops at the first identifier). + */ +function constructorName(sig: TreeSitterNode): string | null { + const ids = findChildren(sig, "identifier"); + if (ids.length === 0) return null; + if (ids.length === 1) return ids[0].text; + return `${ids[0].text}.${ids[1].text}`; +} + +/** + * Walk a `class_body` (or `extension_body` / `enum_body`) and collect + * `method_signature` declarations into the class's `methods` array AND the + * top-level `functions` array, mirroring KotlinExtractor.collectClassBody. + * + * Field extraction: `int count = 0;` and `String? label;` inside a class body + * both parse as `declaration > initialized_identifier_list > initialized_identifier + * > identifier`. The nullable `?` is an unnamed sibling of `type_identifier`, + * so it does not affect this path. + */ +function collectClassBody( + body: TreeSitterNode, + methods: string[], + properties: string[], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], +): void { + for (let i = 0; i < body.childCount; i++) { + const member = body.child(i); + if (!member) continue; + + if (member.type === "method_signature") { + // Factory constructor lives inside method_signature. + const factory = findChild(member, "factory_constructor_signature"); + if (factory) { + const name = constructorName(factory); + if (name) { + pushMethod(member, factory, name, methods, functions, exports); + } + continue; + } + // Getter (`int get value`) — wrapped in method_signature with a + // sibling function_body. The name is the only identifier in getter_signature. + const getter = findChild(member, "getter_signature"); + if (getter) { + const name = extractFunctionName(getter); + if (name) pushMethod(member, getter, name, methods, functions, exports); + continue; + } + // Setter (`set value(int x)`) — wrapped in method_signature with a + // sibling function_body. The first identifier is the name; the + // formal_parameter_list holds the assigned value. + const setter = findChild(member, "setter_signature"); + if (setter) { + const name = extractFunctionName(setter); + if (name) pushMethod(member, setter, name, methods, functions, exports); + continue; + } + // Concrete method: `method_signature > function_signature`. + const inner = findChild(member, "function_signature"); + if (!inner) continue; + const name = extractFunctionName(inner); + if (!name) continue; + pushMethod(member, inner, name, methods, functions, exports); + } else if (member.type === "declaration") { + // Regular constructor: `declaration > constructor_signature`. + const ctor = findChild(member, "constructor_signature"); + if (ctor) { + const name = constructorName(ctor); + if (name) { + pushMethod(member, ctor, name, methods, functions, exports); + } + continue; + } + // Abstract getter (`int get area;`) — `declaration > getter_signature`. + const absGetter = findChild(member, "getter_signature"); + if (absGetter) { + const name = extractFunctionName(absGetter); + if (name) pushMethod(member, absGetter, name, methods, functions, exports); + continue; + } + // Abstract setter (`set width(int w);`) — `declaration > setter_signature`. + const absSetter = findChild(member, "setter_signature"); + if (absSetter) { + const name = extractFunctionName(absSetter); + if (name) pushMethod(member, absSetter, name, methods, functions, exports); + continue; + } + // Abstract method declarations (e.g. `double area();`) appear as + // `declaration > function_signature` — not wrapped in `method_signature`. + const fnSig = findChild(member, "function_signature"); + if (fnSig) { + const name = extractFunctionName(fnSig); + if (name) { + pushMethod(member, fnSig, name, methods, functions, exports); + } + continue; + } + // Field declaration — surface initialized_identifier names as properties. + // Comma-lists like `int a, b, c;` produce multiple initialized_identifier + // children inside a single initialized_identifier_list. + const list = findChild(member, "initialized_identifier_list"); + if (!list) continue; + for (const init of findChildren(list, "initialized_identifier")) { + const id = findChild(init, "identifier"); + if (id) properties.push(id.text); + } + } + } +} + +/** + * Dart extractor for tree-sitter structural analysis + call graph. + * + * Approach (matching `KotlinExtractor` convention): mixin / extension / enum + * declarations are folded into `StructuralAnalysis.classes[]` because the + * shared schema does not have a first-class slot for them. Extension + * declarations without a name surface as `"on "` so they aren't + * silently dropped. + */ +export class DartExtractor implements LanguageExtractor { + readonly languageIds = ["dart"]; + + extractStructure(rootNode: TreeSitterNode): StructuralAnalysis { + const functions: StructuralAnalysis["functions"] = []; + const classes: StructuralAnalysis["classes"] = []; + const imports: StructuralAnalysis["imports"] = []; + const exports: StructuralAnalysis["exports"] = []; + + 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; + case "class_definition": + this.extractClassLikeDeclaration(node, "class_body", classes, functions, exports); + break; + case "mixin_declaration": + this.extractClassLikeDeclaration(node, "class_body", classes, functions, exports); + break; + case "extension_declaration": + this.extractExtensionDeclaration(node, classes, functions, exports); + break; + case "enum_declaration": + this.extractEnumDeclaration(node, classes, exports); + break; + case "import_or_export": + this.extractImportOrExport(node, imports, 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 }); + } + } + + /** + * Extract a class-like declaration that uses a `class_body`-shaped member + * container. Used by `class_definition`, `mixin_declaration`, and (Task 8) + * `extension_declaration`. The only difference between these shapes is the + * body's node type name, which is passed in via `bodyNodeType`. + * + * When `nameOverride` is provided, it is used as the entry's name instead of + * looking up a leading `identifier` child — used by anonymous extensions, + * which have no name in the source. + */ + private extractClassLikeDeclaration( + declNode: TreeSitterNode, + bodyNodeType: string, + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + nameOverride?: string, + ): void { + let name: string; + if (nameOverride !== undefined) { + name = nameOverride; + } else { + const nameNode = findChild(declNode, "identifier"); + if (!nameNode) return; + name = nameNode.text; + } + + const methods: string[] = []; + const properties: string[] = []; + + const body = findChild(declNode, bodyNodeType); + if (body) { + collectClassBody(body, methods, properties, functions, exports); + } + + classes.push({ + name, + lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1], + methods, + properties, + }); + + if (isExported(name)) { + exports.push({ name, lineNumber: declNode.startPosition.row + 1 }); + } + } + + private extractExtensionDeclaration( + declNode: TreeSitterNode, + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + // Named extension — extractClassLikeDeclaration finds the leading identifier itself. + const idNode = findChild(declNode, "identifier"); + if (idNode) { + this.extractClassLikeDeclaration( + declNode, + "extension_body", + classes, + functions, + exports, + ); + return; + } + + // Anonymous extension — no `identifier` child. The on-type is the first + // `type_identifier`. Name the entry "on " so the graph + // builder doesn't drop it for having an empty name. + const onType = findChild(declNode, "type_identifier"); + if (!onType) return; + this.extractClassLikeDeclaration( + declNode, + "extension_body", + classes, + functions, + exports, + `on ${onType.text}`, + ); + } + + private extractEnumDeclaration( + declNode: TreeSitterNode, + classes: StructuralAnalysis["classes"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = findChild(declNode, "identifier"); + if (!nameNode) return; + const name = nameNode.text; + + const properties: string[] = []; + const body = findChild(declNode, "enum_body"); + if (body) { + for (const k of findChildren(body, "enum_constant")) { + const id = findChild(k, "identifier"); + if (id) properties.push(id.text); + } + } + + classes.push({ + name, + lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1], + methods: [], + properties, + }); + + if (isExported(name)) { + exports.push({ name, lineNumber: declNode.startPosition.row + 1 }); + } + } + + private extractImportOrExport( + declNode: TreeSitterNode, + imports: StructuralAnalysis["imports"], + exports: StructuralAnalysis["exports"], + ): void { + const libImport = findChild(declNode, "library_import"); + if (libImport) { + this.extractLibraryImport(libImport, imports); + return; + } + const libExport = findChild(declNode, "library_export"); + if (libExport) { + this.extractLibraryExport(libExport, declNode, exports); + } + } + + private extractLibraryImport( + libImport: TreeSitterNode, + imports: StructuralAnalysis["imports"], + ): void { + const spec = findChild(libImport, "import_specification"); + if (!spec) return; + + const configurable = findChild(spec, "configurable_uri"); + const uri = configurable ? findChild(configurable, "uri") : null; + if (!uri) return; + const source = uriText(uri); + if (!source) return; + + const specifiers: string[] = []; + + // Combinators come in two flavours: + // show Bar, Baz → leading keyword "show", names are specifiers + // hide Qux → leading keyword "hide", names are excluded — skip + const combinators = findChildren(spec, "combinator"); + for (const c of combinators) { + // Inspect the first child to determine show vs hide. The keyword is an + // unnamed token; use `child()` not `namedChild()`. + const first = c.child(0); + if (first && first.type === "hide") continue; + for (const id of findChildren(c, "identifier")) { + specifiers.push(id.text); + } + } + + // `as Foo` → direct `identifier` child of import_specification. + // Only treat as alias when there were no `show`/`hide` specifiers. + const asId = findChild(spec, "identifier"); + if (asId && specifiers.length === 0) { + specifiers.push(asId.text); + } + + imports.push({ + source, + specifiers, + lineNumber: libImport.startPosition.row + 1, + }); + } + + /** + * Extract an `export` directive's URI into `exports[]`. + * + * Takes both `libExport` (the `library_export` node containing the URI) + * and `outerNode` (the wrapping `import_or_export` node). The line number + * uses `outerNode.startPosition` because `library_export` may start one + * child deeper than the `export` keyword, while `import_or_export` is + * guaranteed to start at the keyword. + */ + private extractLibraryExport( + libExport: TreeSitterNode, + outerNode: TreeSitterNode, + exports: StructuralAnalysis["exports"], + ): void { + const configurable = findChild(libExport, "configurable_uri"); + const uri = configurable ? findChild(configurable, "uri") : null; + if (!uri) return; + const source = uriText(uri); + if (!source) return; + exports.push({ + name: source, + lineNumber: outerNode.startPosition.row + 1, + }); + } + + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { + const entries: CallGraphEntry[] = []; + const functionStack: string[] = []; + + /** + * Walk a single node, recursing into its children. Detects call sites + * (selector nodes containing argument_part) and records them against the + * current function on the stack. + * + * In Dart's AST, `function_signature` and `function_body` are SIBLINGS + * within their parent (program, class_body, etc.), NOT parent/child. This + * differs from Kotlin where `function_declaration` wraps both signature and + * body. We handle this by scanning siblings at the parent level: + * `walkSiblings` iterates the children of a container, remembers the name + * from each `function_signature` / `method_signature`, and pushes it onto + * the stack only for the duration of the following `function_body`. + */ + const walkNode = (node: TreeSitterNode) => { + if ( + node.type === "selector" && + findChild(node, "argument_part") && + functionStack.length > 0 + ) { + // A call site: selector containing argument_part. + const callee = this.extractCalleeName(node); + if (callee) { + entries.push({ + caller: functionStack[functionStack.length - 1], + callee, + lineNumber: node.startPosition.row + 1, + }); + } + } + + // Constructor-call shapes that bypass the `selector > argument_part` + // pattern: + // const Foo(...) → `const_object_expression { const_builtin, type_identifier, arguments }` + // new Foo(...) → `new_expression { (unnamed `new`), type_identifier, arguments }` + // Both are extremely common in Flutter widget trees; without this branch + // the construction edge would be silently dropped. The callee is the + // `type_identifier` child. + if ( + (node.type === "const_object_expression" || + node.type === "new_expression") && + functionStack.length > 0 + ) { + const typeNode = findChild(node, "type_identifier"); + if (typeNode) { + entries.push({ + caller: functionStack[functionStack.length - 1], + callee: typeNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + + walkSiblings(node); + }; + + /** + * Iterate a node's children, pairing each function_signature / + * method_signature with its subsequent function_body sibling. + */ + const walkSiblings = (parent: TreeSitterNode) => { + let pendingName: string | null = null; + + for (let i = 0; i < parent.childCount; i++) { + const child = parent.child(i); + if (!child) continue; + + if (child.type === "function_signature") { + pendingName = extractFunctionName(child); + // Recurse into signature (no calls expected, but stay complete). + walkSiblings(child); + } else if (child.type === "method_signature") { + // method_signature wraps one of: + // function_signature → normal method + // getter_signature → getter (with body) + // setter_signature → setter (with body) + // constructor_signature → constructor (with body) + // factory_constructor_signature → factory (with body) + // All five carry the name as their first `identifier` child (factory + // ctors carry two — class + named — handled by `constructorName`). + // Without this dispatch, ctor/factory/getter/setter bodies were + // walked with an empty functionStack and their internal calls were + // dropped from the graph. + const fn = + findChild(child, "function_signature") ?? + findChild(child, "getter_signature") ?? + findChild(child, "setter_signature"); + if (fn) { + pendingName = extractFunctionName(fn); + } else { + const ctor = + findChild(child, "constructor_signature") ?? + findChild(child, "factory_constructor_signature"); + if (ctor) pendingName = constructorName(ctor); + } + walkSiblings(child); + } else if (child.type === "function_body") { + // Consume pendingName: push for the duration of this body. + const pushed = pendingName !== null; + if (pendingName) { + functionStack.push(pendingName); + pendingName = null; + } + walkNode(child); + if (pushed) functionStack.pop(); + } else { + // For every other node (including selector nodes at this level), + // do NOT clear pendingName — anonymous tokens (`;`, `{`, etc.) + // appear between the signature and body and must not reset the + // pending name. + walkNode(child); + } + } + }; + + walkSiblings(rootNode); + return entries; + } + + /** + * Find the callee name for a `selector` node that contains an + * `argument_part`. Look at the parent's children: + * - Bare call `foo(...)`: the previous sibling is an `identifier`. + * - Method call `target.foo(...)`: the previous sibling is itself a + * `selector` wrapping `unconditional_assignable_selector` with the + * method-name `identifier`. + * + * Probe finding (2026-06-13): the plan's claimed AST shapes match exactly. + * - Bare call: return_statement > identifier[helper] + selector(argument_part) + * - Method call: expression_statement > string_literal + selector(unconditional_assignable_selector > identifier[toUpperCase]) + selector(argument_part) + * The plan claimed `expression_statement` as parent for bare calls but the + * actual parent for `return helper()` is `return_statement`. This does not + * affect the strategy since we only look at the preceding sibling, not the + * parent type. + * + * IMPORTANT: web-tree-sitter returns a NEW wrapper object each time `.child(i)` + * is called — node identity (`===`) does NOT work for sibling lookup. We + * compare by `startIndex` (byte offset) which is stable and unique per node. + */ + private extractCalleeName(callSelector: TreeSitterNode): string | null { + const parent = callSelector.parent; + if (!parent) return null; + + // Find this selector's index in the parent using startIndex (not ===). + let myIdx = -1; + for (let i = 0; i < parent.childCount; i++) { + const c = parent.child(i); + if (c && c.startIndex === callSelector.startIndex) { + myIdx = i; + break; + } + } + if (myIdx <= 0) return null; + + const prev = parent.child(myIdx - 1); + if (!prev) return null; + + if (prev.type === "identifier") return prev.text; + + if (prev.type === "selector") { + // Method call shape: previous selector wraps unconditional_assignable_selector. + const inner = findChild(prev, "unconditional_assignable_selector"); + if (inner) { + // Pick the LAST identifier inside the inner selector — that's the + // method name (earlier identifiers, if any, are receiver fragments). + let last: string | null = null; + for (let i = 0; i < inner.childCount; i++) { + const child = inner.child(i); + if (child && child.type === "identifier") last = child.text; + } + return last; + } + } + + return null; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts index 4f0b5a3..8fbe736 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts @@ -9,6 +9,7 @@ export { RubyExtractor } from "./ruby-extractor.js"; export { PhpExtractor } from "./php-extractor.js"; export { CppExtractor } from "./cpp-extractor.js"; export { CSharpExtractor } from "./csharp-extractor.js"; +export { DartExtractor } from "./dart-extractor.js"; export { KotlinExtractor } from "./kotlin-extractor.js"; import type { LanguageExtractor } from "./types.js"; @@ -21,6 +22,7 @@ import { RubyExtractor } from "./ruby-extractor.js"; import { PhpExtractor } from "./php-extractor.js"; import { CppExtractor } from "./cpp-extractor.js"; import { CSharpExtractor } from "./csharp-extractor.js"; +import { DartExtractor } from "./dart-extractor.js"; import { KotlinExtractor } from "./kotlin-extractor.js"; export const builtinExtractors: LanguageExtractor[] = [ @@ -33,5 +35,6 @@ export const builtinExtractors: LanguageExtractor[] = [ new PhpExtractor(), new CppExtractor(), new CSharpExtractor(), + new DartExtractor(), new KotlinExtractor(), ]; diff --git a/understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md b/understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md new file mode 100644 index 0000000..d6c636f --- /dev/null +++ b/understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md @@ -0,0 +1,47 @@ +# tree-sitter-dart WASM (vendored) + +This directory ships a pre-built `tree-sitter-dart.wasm` because the upstream +npm release does not. + +## Why vendored + +The published `tree-sitter-dart@1.0.0` (2023-02-24) tarball does include a +`tree-sitter-dart.wasm`, but it was built with a pre-`dylink.0` tree-sitter +CLI. `web-tree-sitter@0.26.x` — the loader this project uses — expects the +newer `dylink.0` custom-section name and refuses to load the older format +(failure surfaces in `getDylinkMetadata`). + +Rebuilding the same upstream grammar.js with a current +`tree-sitter-cli@0.26.x` produces a `dylink.0` wasm that loads cleanly. + +## How to rebuild + +```bash +npm install -g tree-sitter-cli@latest +cd /tmp && npm pack tree-sitter-dart@1.0.0 +tar xzf tree-sitter-dart-1.0.0.tgz +cd package +tree-sitter build --wasm +cp tree-sitter-dart.wasm \ + /path/to/understand-anything-plugin/packages/tree-sitter-dart-wasm/ +``` + +Verify the resulting wasm: + +```bash +head -c 30 tree-sitter-dart.wasm | xxd | head -1 +# Expect: ...dylin / k.0... +``` + +## Provenance + +- Grammar source: `tree-sitter-dart@1.0.0` (publisher: amaanq) — `grammar.js` + unchanged, only the wasm artifact is regenerated. +- Built with: `tree-sitter-cli@0.26.x`, `wasi-sdk-29-arm64-macos`. +- License: MIT, inherited from tree-sitter-dart@1.0.0 (publisher: amaanq). + +## When to remove this package + +If amaanq publishes a refreshed `tree-sitter-dart` with a `dylink.0` wasm, +this workspace package can be deleted and the dependency in +`@understand-anything/core` flipped to the upstream package. diff --git a/understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json b/understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json new file mode 100644 index 0000000..595b436 --- /dev/null +++ b/understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json @@ -0,0 +1,9 @@ +{ + "name": "@understand-anything/tree-sitter-dart-wasm", + "version": "0.1.0", + "type": "module", + "description": "Vendored tree-sitter-dart WASM grammar built with the modern dylink.0 ABI for use with web-tree-sitter@^0.26.", + "main": "tree-sitter-dart.wasm", + "files": ["tree-sitter-dart.wasm", "BUILD.md"], + "license": "MIT" +} diff --git a/understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm b/understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm new file mode 100644 index 0000000..4154b05 Binary files /dev/null and b/understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm differ diff --git a/understand-anything-plugin/pnpm-lock.yaml b/understand-anything-plugin/pnpm-lock.yaml index 16b6eed..e885e21 100644 --- a/understand-anything-plugin/pnpm-lock.yaml +++ b/understand-anything-plugin/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@understand-anything/core': specifier: workspace:* version: link:packages/core + graphology: + specifier: ~0.26.0 + version: 0.26.0(graphology-types@0.24.8) + graphology-communities-louvain: + specifier: ^2.0.2 + version: 2.0.2(graphology-types@0.24.8) devDependencies: '@types/node': specifier: ^22.0.0 @@ -24,6 +30,12 @@ importers: packages/core: dependencies: + '@tree-sitter-grammars/tree-sitter-kotlin': + specifier: 1.1.0 + version: 1.1.0 + '@understand-anything/tree-sitter-dart-wasm': + specifier: workspace:* + version: link:../tree-sitter-dart-wasm fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -133,7 +145,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + version: 4.2.2(vite@6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@types/d3-force': specifier: ^3.0.10 version: 3.0.10 @@ -145,7 +157,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) @@ -156,12 +168,14 @@ importers: specifier: ^5.7.0 version: 5.9.3 vite: - specifier: ^6.0.0 - version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + specifier: ^6.4.2 + version: 6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) vitest: specifier: ^3.1.0 version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + packages/tree-sitter-dart-wasm: {} + packages: '@ampproject/remapping@2.3.0': @@ -482,66 +496,79 @@ packages: resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.0': resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.0': resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.0': resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.0': resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.0': resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.0': resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.0': resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.0': resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.0': resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.0': resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.0': resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.0': resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.0': resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} @@ -611,24 +638,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -663,6 +694,14 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tree-sitter-grammars/tree-sitter-kotlin@1.1.0': + resolution: {integrity: sha512-vlVXaxEE8t2kpJgfZpa8XVvxcnKw9AYtRTgy7KWjsDmAsadk06RxAT80IXOgGQnmM9i/orQn1nD84gPNUHu6DQ==} + peerDependencies: + tree-sitter: ^0.22.4 + peerDependenciesMeta: + tree-sitter: + optional: true + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1084,6 +1123,11 @@ packages: peerDependencies: graphology-types: '>=0.24.0' + graphology@0.26.0: + resolution: {integrity: sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==} + peerDependencies: + graphology-types: '>=0.24.0' + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1207,24 +1251,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1385,6 +1433,11 @@ packages: node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + npm-check-updates@17.1.18: + resolution: {integrity: sha512-bkUy2g4v1i+3FeUf5fXMLbxmV95eG4/sS7lYE32GrUeVgQRfQEk39gpskksFunyaxQgTIdrvYbnuNbO/pSUSqw==} + engines: {node: ^18.18.0 || >=20.0.0, npm: '>=8.12.1'} + hasBin: true + obliterator@2.0.5: resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} @@ -1759,6 +1812,46 @@ packages: yaml: optional: true + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2231,12 +2324,18 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(vite@6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite: 6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + + '@tree-sitter-grammars/tree-sitter-kotlin@1.1.0': + dependencies: + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 + npm-check-updates: 17.1.18 '@types/babel__core@7.20.5': dependencies: @@ -2333,7 +2432,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -2341,7 +2440,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite: 6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -2692,6 +2791,11 @@ snapshots: graphology-types: 0.24.8 obliterator: 2.0.5 + graphology@0.26.0(graphology-types@0.24.8): + dependencies: + events: 3.3.0 + graphology-types: 0.24.8 + has-flag@4.0.0: {} hast-util-to-jsx-runtime@2.3.6: @@ -3101,6 +3205,8 @@ snapshots: node-releases@2.0.36: {} + npm-check-updates@17.1.18: {} + obliterator@2.0.5: {} package-json-from-dist@1.0.1: {} @@ -3444,7 +3550,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite: 6.4.3(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -3465,7 +3571,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite: 6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -3510,6 +3616,36 @@ snapshots: lightningcss: 1.32.0 yaml: 2.8.3 + vite@6.4.3(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.32.0 + yaml: 2.8.3 + + vite@6.4.3(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.32.0 + yaml: 2.8.3 + vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3