mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
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:
@@ -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,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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+5
@@ -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,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,
|
||||
|
||||
+675
@@ -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"
|
||||
}
|
||||
Binary file not shown.
+146
-10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user