Merge pull request #435 from thejesh23/feat/dart-language-support

feat(core): add Dart language support via workspace-vendored WASM
This commit is contained in:
Yuxiang Lin
2026-06-16 11:06:53 +08:00
committed by GitHub
Unverified
17 changed files with 1661 additions and 17 deletions
+1 -1
View File
@@ -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"
},
+1 -1
View File
@@ -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"
},
+1 -1
View File
@@ -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"
},
+5
View File
@@ -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':
@@ -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"
},
+1 -1
View File
@@ -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",
@@ -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",
@@ -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", () => {
@@ -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;
@@ -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,
@@ -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<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();
});
});
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<String> 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<Square> 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();
});
});
});
@@ -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<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;
}
/**
* 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 = "<Class>"
* - two identifiers → named constructor, name = "<Class>.<named>"
*
* 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 { <unnamed "factory">, 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 <TargetType>"` 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 <TargetType>" 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;
}
}
@@ -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(),
];
@@ -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.
@@ -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"
}
+146 -10
View File
@@ -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