Live verification during planning surfaced two facts that change the shipping strategy: 1. tree-sitter-dart@1.0.0's prebuilt wasm uses the pre-`dylink.0` format and fails to load in web-tree-sitter@0.26.x (the version this project uses). Verified by directly loading the upstream wasm and catching the failure in getDylinkMetadata. 2. The grammar source itself is sound — rebuilding with the current tree-sitter-cli@0.26.x + wasi-sdk-29 toolchain produces a working dylink.0-format wasm that parses every construct the extractor needs. Revised packaging: ship the freshly-built wasm as a workspace-internal package (@understand-anything/tree-sitter-dart-wasm) rather than depending on the broken upstream npm artifact. No loader changes required; existing TreeSitterPlugin resolves it the same way it resolves other tree-sitter packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 KiB
Dart language support
Date: 2026-06-13
Status: Approved — ready for implementation plan
Scope: understand-anything-plugin/packages/core/{src/languages/configs,src/plugins/extractors,package.json}
Problem
Understand Anything currently ships 14 code-language configs (TypeScript,
JavaScript, Python, Go, Rust, Java, Ruby, PHP, Swift, Kotlin, Lua, C, C++, C#)
plus 25 non-code config-file parsers. Dart is absent. Any .dart file in a
scanned project is classified as plaintext by the language registry, gets no
structural analysis, and contributes no nodes or edges to the project knowledge
graph.
Dart is in widespread big-tech use (Google's Flutter — official cross-platform mobile language; production codebases at BMW, Toyota, Alibaba, ByteDance) and its absence is the single largest mobile/cross-platform gap in the current language gallery. Flutter codebases analyzed today produce empty graphs even though the project's whole point is to make codebases understandable.
Goal
Add deep Dart support — LanguageConfig + tree-sitter WASM grammar +
DartExtractor + vitest coverage — at parity with the recently landed Kotlin
support (PR #347). After this change, .dart files in a scanned project must
produce structural nodes (functions, classes, mixins, extensions, enums) and
call-graph edges identical in shape to what Kotlin/Java/Go produce today.
Non-Goals
- No Flutter framework config. The Flutter ecosystem (pubspec.yaml manifest
detection, widget vs service vs model layer hints) is a follow-up. The language
config alone unlocks structural analysis for both Flutter and non-Flutter Dart
code; framework-level detection is a separate, additive PR against
frameworks/andframework-registry.ts. - No schema extensions. Mixins, extensions, and enums are folded into the
existing
StructuralAnalysis.classes[]bucket. Addingmixins[]/extensions[]as first-class fields would require coordinated changes totypes.ts,graph-builder.ts, dashboard rendering, and every existing extractor's tests — out of scope here. Tracked as a future cross-cutting refactor. - No support for
part of/partmulti-file libraries. Each.dartfile is analyzed independently; cross-partrelationships would need a second pass over the project. Tracked as a follow-up. - No first-class modeling of Dart records or pattern matching. Both appear only inside function bodies and have no project-graph impact.
- No dashboard changes. The new language slots into the existing
config-driven pipeline; the dashboard already renders whatever
classes[]/functions[]the extractor produces.
Approach (chosen)
Strictly parallel to the Kotlin add (PR #347): six file changes, no edits to
shared types, registries, plugin loader, graph builder, or dashboard. The
existing config-driven TreeSitterPlugin picks the new language up unchanged.
Alternative considered and rejected:
- Shallow Swift-style stub (LanguageConfig only, no tree-sitter wiring):
smallest PR but produces no structural graph for
.dartfiles — fails the goal. The existing 14-language gallery already covers the shallow tier; the user-visible win is the deep tier. - Schema-extension approach (first-class
mixins[]/extensions[]): more accurate Dart modeling but touches every existing extractor's tests and the dashboard. High review risk; better as a separate, scoped follow-up.
File-level changes
| # | File | Change | Approx LOC |
|---|---|---|---|
| 1 | understand-anything-plugin/pnpm-workspace.yaml |
Register packages/tree-sitter-dart-wasm/* |
1 |
| 2 | .../packages/tree-sitter-dart-wasm/package.json |
New — workspace package metadata | ~6 |
| 3 | .../packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm |
New — vendored freshly-built wasm (~745 KB binary) | binary |
| 4 | .../packages/tree-sitter-dart-wasm/BUILD.md |
New — provenance + rebuild instructions | ~30 |
| 5 | .../packages/core/package.json |
Add "@understand-anything/tree-sitter-dart-wasm": "workspace:*" dependency |
1 |
| 6 | .../packages/core/src/languages/configs/dart.ts |
New — LanguageConfig with treeSitter field pointing at the workspace package |
~35 |
| 7 | .../packages/core/src/languages/configs/index.ts |
Import + register dartConfig in the code-languages block (both builtinLanguageConfigs array and the named re-export block) |
~4 |
| 8 | .../packages/core/src/plugins/extractors/dart-extractor.ts |
New — DartExtractor class implementing LanguageExtractor |
~400 |
| 9 | .../packages/core/src/plugins/extractors/index.ts |
Import DartExtractor, re-export it, and add new DartExtractor() to builtinExtractors |
~3 |
| 10 | .../packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts |
New — ~22 vitest cases | ~370 |
pnpm-lock.yaml regenerates automatically via pnpm install.
dartConfig shape
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;
Notes:
- Single
.dartextension; Flutter widgets share it. entryPointscovers both Flutter (lib/main.dart) and Dart CLI (bin/*.dart).barrelsmatches Dart's idiomatic top-level re-export files (lib/foo.dartre-exportinglib/src/*.dart).
WASM grammar source
Ship a freshly-built wasm as a workspace-internal package
@understand-anything/tree-sitter-dart-wasm, built from the tree-sitter-dart
grammar source. The grammar source is sound; only the prebuilt npm artifact is
ABI-incompatible with the current web-tree-sitter.
Why not the upstream tree-sitter-dart package directly
The published tree-sitter-dart@1.0.0 tarball does ship a .wasm, but it was
built in 2023-02 with a tree-sitter CLI that emitted the OLD WebAssembly dynamic
linking format. The wasm header is \0asm then a custom section named
"dylink" (no .0 suffix). The project's current web-tree-sitter@^0.26.6
expects the newer "dylink.0" format (the standardized name since tree-sitter
CLI ~0.22). Attempting to load the upstream wasm fails inside
getDylinkMetadata with a bare Error. Verified during design via a live
probe against web-tree-sitter@0.26.8 in the project's own node_modules.
@tree-sitter-grammars/tree-sitter-dart does not exist (404 on npm);
@driftlog/tree-sitter-dart@1.0.4 ships no wasm at all. There is no
WASM-shipping Dart grammar on the npm registry that works with the current
web-tree-sitter.
How the freshly-built wasm is sourced
Rebuilding the same grammar source with the current tree-sitter-cli@0.26.x +
wasi-sdk-29 toolchain produces a dylink.0-format wasm (~745 KB) that loads
cleanly. Confirmed during design: the rebuilt wasm parses every construct the
extractor needs (functions, classes, mixins, extensions, enums, imports,
exports, calls). The grammar.js itself is unchanged from the upstream package.
Packaging approach
Add a new workspace package at
understand-anything-plugin/packages/tree-sitter-dart-wasm/ containing:
tree-sitter-dart.wasm— the freshly-built artifact (vendored binary).package.json—{ "name": "@understand-anything/tree-sitter-dart-wasm", "version": "0.1.0", "main": "tree-sitter-dart.wasm" }.BUILD.md— short note documenting how the wasm was built (CLI version, grammar source SHA, wasi-sdk version) so the next maintainer can rebuild it.
Register the new workspace package in
understand-anything-plugin/pnpm-workspace.yaml. Add it as a dependency of
@understand-anything/core via "workspace:*". The existing TreeSitterPlugin
loader resolves it unchanged via
require.resolve("@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm")
— no loader code changes.
This approach was chosen over three alternatives:
- Depend on broken upstream: would fail at runtime; rejected.
- Modify the loader to support local file paths: more invasive, sets a precedent that complicates other languages.
- Publish under a third-party npm scope: cleaner long-term but requires external infra; can transition later if a published fix lands.
Tradeoff acknowledged: ~745 KB binary committed to git. Comparable in size to
the wasms already pulled in by tree-sitter-rust / tree-sitter-go at install
time (those just aren't committed). If amaanq/tree-sitter-dart later publishes
a refreshed npm release with a dylink.0 wasm, switching back is a two-line
change: delete the workspace package, depend on tree-sitter-dart directly,
flip the wasmPackage field.
DartExtractor — what it extracts
Implements the LanguageExtractor interface with languageIds = ["dart"].
Walks the tree-sitter AST and produces StructuralAnalysis +
CallGraphEntry[]. Follows the existing convention used by KotlinExtractor
and GoExtractor of pushing class/mixin methods to BOTH methods[] and the
top-level functions[] array so the call graph can resolve them.
Top-level AST nodes handled
| AST node | Maps to | Notes |
|---|---|---|
function_signature (top-level) |
functions[] |
name, params, returnType, lineRange |
class_definition |
classes[] |
walks class_body for methods + fields |
mixin_declaration |
classes[] |
folded in per chosen approach |
extension_declaration |
classes[] |
name may be absent → use target type name (extension on Foo → "on Foo") so the entry isn't dropped |
enum_declaration |
classes[] |
constants surfaced as properties |
import_or_export / library_import |
imports[] |
strips quotes from URI string; show / hide clauses captured as specifiers; as prefix becomes the sole specifier |
Top-level export directive |
exports[] |
URI as name, line number from the directive |
package_directive / library_name |
skipped | metadata, not graph members |
Visibility rule (Dart-specific)
Dart has no public / private keywords — names starting with _ are
file-private (library-private to be precise), everything else is exported. The
isExported(name) helper is a one-liner: !name.startsWith("_"). This is the
opposite of Kotlin (where the default is exported and the presence of a
modifier opts out). The Dart rule is name-based, not modifier-based, and
applies uniformly to top-level declarations AND class members.
An inline comment in the extractor must document this contrast explicitly,
because a reviewer comparing line-for-line against KotlinExtractor will
otherwise expect modifier inspection.
Class body walking
Mirrors KotlinExtractor.collectClassBody:
method_signature/function_signatureinsideclass_body→ push name to the class'smethods[]AND append a full entry to top-levelfunctions[](matches Kotlin/Swift/Go convention; required for call-graph resolution).field_declaration→properties[].- Constructor naming follows the source spelling so call sites resolve in the
call graph:
- Unnamed constructor
Foo(...)→ method name"Foo". - Named constructor
Foo.named(...)→ method name"Foo.named". - Factory named constructor
factory Foo.fromJson(...)→ method name"Foo.fromJson".
- Unnamed constructor
Call graph
Reuses the recursive-walk + function-stack pattern from KotlinExtractor:
- Push on
function_signature/method_signature; pop on exit. - On any node representing an invocation, emit
{ caller, callee, lineNumber }. Dart's grammar represents calls asassignable_expression > selector > arguments. The callee identifier is the named child immediately preceding theargumentsnode. Two shapes:- Bare call
foo(...)→ callee is theidentifierchild. - Method call
target.foo(...)→ callee is the lastidentifierin theselectorchain (analogous to Kotlin'snavigation_expressionhandling).
- Bare call
Imports — three forms
import 'package:flutter/material.dart';→source = "package:flutter/material.dart",specifiers = []import 'foo.dart' show Bar, Baz;→source = "foo.dart",specifiers = ["Bar", "Baz"]import 'foo.dart' as f;→source = "foo.dart",specifiers = ["f"]
Tests — dart-extractor.test.ts
~22 vitest cases, matching the bar set by kotlin-extractor.test.ts (22 cases,
364 lines). Each test parses a small Dart snippet through the real WASM grammar
(no mocks) and asserts on extractor output. Setup copies Kotlin's pattern
verbatim: createRequire + Parser.init() + Language.load(wasmPath) in
beforeAll, snippet-per-test parsing via a local parse() helper. The only
difference is the WASM path:
require.resolve("tree-sitter-dart/tree-sitter-dart.wasm").
Coverage matrix:
| Bucket | Cases | Examples |
|---|---|---|
| Functions | 3 | simple int add(int a, int b); no-args/no-return void noop(); async + generic Future<T> fetch<T>(String id) |
| Classes | 4 | plain class with fields + methods; class with named + factory constructors; abstract class; class with extends + with + implements |
| Mixins | 2 | mixin Foo {...}; mixin Foo on Bar {...} |
| Extensions | 2 | named extension StringX on String {...}; anonymous extension on int {...} |
| Enums | 2 | simple enum Color { red, green, blue }; enhanced enum with methods |
| Imports | 4 | package: URI; relative path; show clause; as prefix |
| Exports | 1 | top-level export 'foo.dart'; directive |
| Visibility | 2 | underscore-prefixed name is NOT in exports[]; non-underscore IS exported; covers both top-level and class-member cases |
| Call graph | 2 | top-level fn calling another top-level fn; method calling another method (a.b() shape) |
Existing test that should keep passing: tree-sitter-plugin.test.ts (the
end-to-end pipeline test). No new assertions required there — Dart enters the
same code path; if structural analysis works for .dart files in unit tests,
the integration path will follow.
Error handling
All inherited from the existing pipeline; no new failure modes are introduced:
- WASM load failure (package missing / corrupt):
TreeSitterPlugin.init()already catches and logs aconsole.debug"skipping structural analysis" message;.dartfiles fall back to LLM-only analysis. Same path Swift uses today (Swift has aLanguageConfigbut notreeSitterfield, so the loader silently skips it). - Parse failure on a malformed
.dartfile: tree-sitter returns a partial tree; the extractor walks what's present and returns whatever it found. MatchesKotlinExtractorbehavior. - Empty /
library/partonly files: extractor returns{ functions: [], classes: [], imports: [], exports: [] }. Not an error.
Edge cases handled in code
- Anonymous extension (
extension on Foo): the class entry'snameis set to"on Foo"rather than empty string. Without this, the entry would be dropped by the graph builder. WHY-comment required inline. - Constructor naming:
factory Foo.fromJson(...)→ method name"Foo.fromJson"(not"fromJson"), so call sites likeFoo.fromJson(map)resolve correctly in the call graph. - Underscore visibility on class members: applied identically to top-level
declarations and to declarations inside class/mixin/extension bodies. A
class _PrivateImplis not inexports[]. Aclass Publicwith a method_helper()has the class itself inexports[]but_helperis excluded. Non-underscore class members ARE pushed toexports[]alongside the class entry, matchingKotlinExtractor.collectClassBody's behavior of pushing exported members to the top-levelexports[]array.
Edge cases explicitly OUT of scope
Documented in code via short comments at the relevant walk site:
- Dart records
(int, String)and pattern matching — function-local only. part of/partmulti-file libraries — would require a second project-wide pass.
Verification
Before marking the implementation complete, run all of:
pnpm install # picks up tree-sitter-dart
pnpm --filter @understand-anything/core build # tsc clean
pnpm --filter @understand-anything/core test # all existing + 22 new Dart tests pass
pnpm --filter @understand-anything/skill build # no regressions
pnpm lint # clean
pnpm test # full suite, no regressions
Plus a manual smoke test: run /understand against a small Flutter sample
repo, then inspect .understand-anything/knowledge-graph.json to confirm it
contains Dart-derived class/function nodes and call-graph edges.
Open questions
None at design time. Three genuine unknowns were resolved during exploration:
- WASM availability: the upstream
tree-sitter-dart@1.0.0wasm uses the pre-dylink.0format and fails to load inweb-tree-sitter@0.26.x. A fresh build with the currenttree-sitter-cli@0.26.x+wasi-sdk-29produces adylink.0-format wasm that loads and parses correctly. Ship via a workspace-internal package; documented above. - Grammar node-type coverage: confirmed via inspection of
node-types.json(316 named types) and via a live AST probe on a Dart sample covering every construct the extractor handles. Concrete AST shapes for each construct are documented in the implementation plan. - Visibility semantics: Dart's name-based
_-prefix rule is the opposite of Kotlin's modifier-based rule; encoded as a one-lineisExportedhelper with an explanatory comment.