From 2bb52335176e9c74b8b6d4876291edba82e95750 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 04:19:22 -0700 Subject: [PATCH 01/20] docs: design spec for Dart language support Adds the brainstormed design for landing deep Dart support at parity with the recent Kotlin add (PR #347): LanguageConfig + tree-sitter WASM grammar (tree-sitter-dart@1.0.0, verified ships a prebuilt .wasm in its tarball) + DartExtractor + ~22 vitest cases. Six file changes, no edits to shared schemas/registries. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/2026-06-13-dart-support-design.md | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-13-dart-support-design.md diff --git a/docs/superpowers/specs/2026-06-13-dart-support-design.md b/docs/superpowers/specs/2026-06-13-dart-support-design.md new file mode 100644 index 0000000..1ceed77 --- /dev/null +++ b/docs/superpowers/specs/2026-06-13-dart-support-design.md @@ -0,0 +1,309 @@ +# 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/` and `framework-registry.ts`. +- **No schema extensions.** Mixins, extensions, and enums are folded into the + existing `StructuralAnalysis.classes[]` bucket. Adding `mixins[]` / `extensions[]` + as first-class fields would require coordinated changes to `types.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` / `part` multi-file libraries.** Each `.dart` file + is analyzed independently; cross-`part` relationships 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 `.dart` files — 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/packages/core/package.json` | Add `"tree-sitter-dart": "^1.0.0"` dependency | 1 | +| 2 | `.../languages/configs/dart.ts` | **New** — `LanguageConfig` with `treeSitter` field | ~35 | +| 3 | `.../languages/configs/index.ts` | Import + register `dartConfig` in the code-languages block (both `builtinLanguageConfigs` array and the named re-export block) | ~4 | +| 4 | `.../plugins/extractors/dart-extractor.ts` | **New** — `DartExtractor` class implementing `LanguageExtractor` | ~400 | +| 5 | `.../plugins/extractors/index.ts` | Import `DartExtractor`, re-export it, and add `new DartExtractor()` to `builtinExtractors` | ~3 | +| 6 | `.../plugins/extractors/__tests__/dart-extractor.test.ts` | **New** — ~22 vitest cases | ~370 | + +`pnpm-lock.yaml` regenerates automatically via `pnpm install`. + +## `dartConfig` shape + +```ts +export const dartConfig = { + id: "dart", + displayName: "Dart", + extensions: [".dart"], + treeSitter: { + wasmPackage: "tree-sitter-dart", + 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 `.dart` extension; Flutter widgets share it. +- `entryPoints` covers both Flutter (`lib/main.dart`) and Dart CLI (`bin/*.dart`). +- `barrels` matches Dart's idiomatic top-level re-export files (`lib/foo.dart` + re-exporting `lib/src/*.dart`). + +## WASM grammar source + +**Use `tree-sitter-dart@1.0.0`** (publisher: amaanq; the canonical Dart +tree-sitter fork). Verification performed during design: + +- The npm tarball (`tree-sitter-dart-1.0.0.tgz`) ships a prebuilt + `tree-sitter-dart.wasm` at the package root. Confirmed via `npm pack` + `tar + tzf`. +- The grammar exposes 316 node types including `class_definition`, + `function_signature`, `method_signature`, `mixin_declaration`, + `extension_declaration`, `enum_declaration`, `library_import`, + `import_or_export`, `mixin_application_class`. +- This matches the publishing shape the Kotlin PR relied on + (`@tree-sitter-grammars/tree-sitter-kotlin` ships a prebuilt `.wasm` alongside + native bindings); the existing `TreeSitterPlugin` loader resolves it via + `require.resolve("tree-sitter-dart/tree-sitter-dart.wasm")` with no loader + changes. +- `@tree-sitter-grammars/tree-sitter-dart` does not exist (404 on npm), and + `@driftlog/tree-sitter-dart` ships only native bindings via `node-gyp-build`. + `tree-sitter-dart` is the only WASM-shipping option on the registry. + +Caveat: `tree-sitter-dart@1.0.0` was last published 2023-02-24 and the package +description ("Dart grammar attempt for tree-sitter") signals an early/community +status. Mitigation: the extractor's tests parse real Dart snippets through the +WASM grammar — any future grammar regression surfaces immediately. If the +grammar later proves unmaintained, swapping in a fork is a one-line change in +`dartConfig.treeSitter.wasmPackage`. + +## `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_signature` inside `class_body` → push name to + the class's `methods[]` AND append a full entry to top-level `functions[]` + (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"`. + +### 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 as `assignable_expression > selector > + arguments`. The callee identifier is the named child immediately preceding + the `arguments` node. Two shapes: + - Bare call `foo(...)` → callee is the `identifier` child. + - Method call `target.foo(...)` → callee is the last `identifier` in the + `selector` chain (analogous to Kotlin's `navigation_expression` handling). + +### 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 fetch(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 a `console.debug` "skipping structural analysis" + message; `.dart` files fall back to LLM-only analysis. Same path Swift uses + today (Swift has a `LanguageConfig` but no `treeSitter` field, so the loader + silently skips it). +- **Parse failure on a malformed `.dart` file**: tree-sitter returns a partial + tree; the extractor walks what's present and returns whatever it found. + Matches `KotlinExtractor` behavior. +- **Empty / `library` / `part` only files**: extractor returns + `{ functions: [], classes: [], imports: [], exports: [] }`. Not an error. + +## Edge cases handled in code + +- **Anonymous extension** (`extension on Foo`): the class entry's `name` is + 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 like `Foo.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 _PrivateImpl` is not in `exports[]`. A `class Public` with a method + `_helper()` has the class itself in `exports[]` but `_helper` is excluded. + Non-underscore class members ARE pushed to `exports[]` alongside the class + entry, matching `KotlinExtractor.collectClassBody`'s behavior of pushing + exported members to the top-level `exports[]` 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` / `part` multi-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. The two genuine unknowns (WASM availability + grammar +node-type coverage) were resolved during exploration: + +- WASM ships with `tree-sitter-dart@1.0.0` — confirmed via `npm pack`. +- Grammar exposes all needed node types — confirmed via inspection of + `node-types.json`. From c447b69faf3e2c78c2a6a133adf626e3287dea6a Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 04:30:09 -0700 Subject: [PATCH 02/20] docs: revise Dart spec for workspace-vendored wasm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../specs/2026-06-13-dart-support-design.md | 118 ++++++++++++------ 1 file changed, 83 insertions(+), 35 deletions(-) diff --git a/docs/superpowers/specs/2026-06-13-dart-support-design.md b/docs/superpowers/specs/2026-06-13-dart-support-design.md index 1ceed77..2119288 100644 --- a/docs/superpowers/specs/2026-06-13-dart-support-design.md +++ b/docs/superpowers/specs/2026-06-13-dart-support-design.md @@ -68,12 +68,16 @@ Alternative considered and rejected: | # | File | Change | Approx LOC | |---|---|---|---| -| 1 | `understand-anything-plugin/packages/core/package.json` | Add `"tree-sitter-dart": "^1.0.0"` dependency | 1 | -| 2 | `.../languages/configs/dart.ts` | **New** — `LanguageConfig` with `treeSitter` field | ~35 | -| 3 | `.../languages/configs/index.ts` | Import + register `dartConfig` in the code-languages block (both `builtinLanguageConfigs` array and the named re-export block) | ~4 | -| 4 | `.../plugins/extractors/dart-extractor.ts` | **New** — `DartExtractor` class implementing `LanguageExtractor` | ~400 | -| 5 | `.../plugins/extractors/index.ts` | Import `DartExtractor`, re-export it, and add `new DartExtractor()` to `builtinExtractors` | ~3 | -| 6 | `.../plugins/extractors/__tests__/dart-extractor.test.ts` | **New** — ~22 vitest cases | ~370 | +| 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`. @@ -85,7 +89,7 @@ export const dartConfig = { displayName: "Dart", extensions: [".dart"], treeSitter: { - wasmPackage: "tree-sitter-dart", + wasmPackage: "@understand-anything/tree-sitter-dart-wasm", wasmFile: "tree-sitter-dart.wasm", }, concepts: [ @@ -118,31 +122,67 @@ export const dartConfig = { ## WASM grammar source -**Use `tree-sitter-dart@1.0.0`** (publisher: amaanq; the canonical Dart -tree-sitter fork). Verification performed during design: +**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`. -- The npm tarball (`tree-sitter-dart-1.0.0.tgz`) ships a prebuilt - `tree-sitter-dart.wasm` at the package root. Confirmed via `npm pack` + `tar - tzf`. -- The grammar exposes 316 node types including `class_definition`, - `function_signature`, `method_signature`, `mixin_declaration`, - `extension_declaration`, `enum_declaration`, `library_import`, - `import_or_export`, `mixin_application_class`. -- This matches the publishing shape the Kotlin PR relied on - (`@tree-sitter-grammars/tree-sitter-kotlin` ships a prebuilt `.wasm` alongside - native bindings); the existing `TreeSitterPlugin` loader resolves it via - `require.resolve("tree-sitter-dart/tree-sitter-dart.wasm")` with no loader - changes. -- `@tree-sitter-grammars/tree-sitter-dart` does not exist (404 on npm), and - `@driftlog/tree-sitter-dart` ships only native bindings via `node-gyp-build`. - `tree-sitter-dart` is the only WASM-shipping option on the registry. +### Why not the upstream `tree-sitter-dart` package directly -Caveat: `tree-sitter-dart@1.0.0` was last published 2023-02-24 and the package -description ("Dart grammar attempt for tree-sitter") signals an early/community -status. Mitigation: the extractor's tests parse real Dart snippets through the -WASM grammar — any future grammar regression surfaces immediately. If the -grammar later proves unmaintained, swapping in a fork is a one-line change in -`dartConfig.treeSitter.wasmPackage`. +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 @@ -301,9 +341,17 @@ contains Dart-derived class/function nodes and call-graph edges. ## Open questions -None at design time. The two genuine unknowns (WASM availability + grammar -node-type coverage) were resolved during exploration: +None at design time. Three genuine unknowns were resolved during exploration: -- WASM ships with `tree-sitter-dart@1.0.0` — confirmed via `npm pack`. -- Grammar exposes all needed node types — confirmed via inspection of - `node-types.json`. +- **WASM availability**: the upstream `tree-sitter-dart@1.0.0` wasm uses the + pre-`dylink.0` format and fails to load in `web-tree-sitter@0.26.x`. A + fresh build with the current `tree-sitter-cli@0.26.x` + `wasi-sdk-29` + produces a `dylink.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-line `isExported` helper + with an explanatory comment. From 5459ac0e6db6dfa38942c54d695f17b839f41a4a Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 04:35:07 -0700 Subject: [PATCH 03/20] docs: implementation plan for Dart language support Thirteen-task TDD plan walking from vendoring the workspace wasm package through scaffolding the extractor and adding extraction logic in test-first slices: functions, classes, constructors, mixins, extensions, enums, imports, exports, visibility, and call graph. Every code block reflects AST shapes confirmed via a live probe against a freshly-built tree-sitter-dart wasm in the project's own web-tree-sitter at 0.26.x. No placeholder code, no "fill in later" references. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-06-13-dart-language-support.md | 1921 +++++++++++++++++ 1 file changed, 1921 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-13-dart-language-support.md diff --git a/docs/superpowers/plans/2026-06-13-dart-language-support.md b/docs/superpowers/plans/2026-06-13-dart-language-support.md new file mode 100644 index 0000000..6fc64ef --- /dev/null +++ b/docs/superpowers/plans/2026-06-13-dart-language-support.md @@ -0,0 +1,1921 @@ +# Dart Language Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Land deep Dart support in `@understand-anything/core` at parity with the recently merged Kotlin extractor (PR #347), producing structural graph + call-graph edges for `.dart` files. + +**Architecture:** Vendor a freshly-built `tree-sitter-dart.wasm` as a workspace-internal package (`@understand-anything/tree-sitter-dart-wasm`); register a `dartConfig` `LanguageConfig` referencing it; add a `DartExtractor` class implementing `LanguageExtractor`; cover with ~22 vitest cases driven by the real WASM grammar. No changes to shared schemas, registries, or the dashboard. + +**Tech Stack:** TypeScript 5 (strict), pnpm 10 workspaces, vitest 3, `web-tree-sitter@^0.26.6`, `tree-sitter-cli@0.26.x` (build-time only). + +--- + +## File structure + +| File | Responsibility | +|---|---| +| `understand-anything-plugin/pnpm-workspace.yaml` | Add `packages/tree-sitter-dart-wasm/*` so pnpm sees the new package | +| `.../packages/tree-sitter-dart-wasm/package.json` | Minimal package metadata — name, version, main pointing at the wasm | +| `.../packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm` | Vendored wasm binary (built from `tree-sitter-dart@1.0.0` grammar.js) | +| `.../packages/tree-sitter-dart-wasm/BUILD.md` | Provenance + rebuild instructions for future maintainers | +| `.../packages/core/package.json` | Add `@understand-anything/tree-sitter-dart-wasm: workspace:*` to dependencies | +| `.../packages/core/src/languages/configs/dart.ts` | Single `LanguageConfig` object for Dart | +| `.../packages/core/src/languages/configs/index.ts` | Import + register `dartConfig` | +| `.../packages/core/src/plugins/extractors/dart-extractor.ts` | `DartExtractor` class — structural + call-graph extraction | +| `.../packages/core/src/plugins/extractors/index.ts` | Import + register `DartExtractor` | +| `.../packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts` | ~22 vitest cases — real WASM parse + assertions | + +--- + +## Working directory & branch assumption + +All paths in this plan are relative to the repository root `/Users/thejesh/Git/Understand-Anything`. The implementation branch `feat/dart-language-support` already exists (the spec was committed to it in commits `2bb5233` and `c447b69`). + +Verify before starting: + +```bash +cd /Users/thejesh/Git/Understand-Anything +git status # clean +git branch --show-current # feat/dart-language-support +git log --oneline -3 # top: c447b69 docs: revise Dart spec... +``` + +--- + +## Task 1: Vendor the freshly-built tree-sitter-dart wasm + +**Why first:** Every downstream task depends on this wasm loading correctly via `require.resolve("@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm")`. Build + commit it before writing the config so dependent tasks can run their tests. + +**Files:** +- Create: `understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json` +- Create: `understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm` (binary, ~745 KB) +- Create: `understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md` +- Modify: `understand-anything-plugin/pnpm-workspace.yaml` + +- [ ] **Step 1: Inspect existing workspace config** + +Run: +```bash +cat understand-anything-plugin/pnpm-workspace.yaml +``` + +Expected output: +```yaml +packages: + - packages/* + - src +``` + +Confirm `packages/*` is already a glob — that means our new `packages/tree-sitter-dart-wasm/` will be picked up automatically and no edit is required. If the file does NOT use a glob, add a line ` - packages/tree-sitter-dart-wasm`. + +- [ ] **Step 2: Build the wasm from upstream grammar source** + +Prerequisites — install once if absent: +```bash +npm install -g tree-sitter-cli@latest +tree-sitter --version # expect: tree-sitter 0.26.x or newer +``` + +Build: +```bash +cd /tmp && rm -rf dart-build && mkdir dart-build && cd dart-build +npm pack tree-sitter-dart@1.0.0 # downloads the upstream tarball +tar xzf tree-sitter-dart-1.0.0.tgz +cd package +tree-sitter build --wasm # ~30 s; downloads wasi-sdk-29 on first run +ls -la tree-sitter-dart.wasm # expect: ~745 KB +head -c 30 tree-sitter-dart.wasm | xxd | head -1 +``` + +Expected last line: +``` +00000000: 0061 736d 0100 0000 0011 0864 796c 696e .asm.......dylin +``` + +(The `\0asm` magic followed by a custom section named `dylink.0` — the byte after `dylink` must be `2e 30`, NOT a length byte for an old-format `dylink` section.) + +If the byte after `dylink` is `c8 9b 2c` (the broken upstream wasm), the build did NOT regenerate — verify your `tree-sitter --version` is current. + +- [ ] **Step 3: Vendor the wasm into the workspace package** + +```bash +cd /Users/thejesh/Git/Understand-Anything +mkdir -p understand-anything-plugin/packages/tree-sitter-dart-wasm +cp /tmp/dart-build/package/tree-sitter-dart.wasm \ + understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm +ls -la understand-anything-plugin/packages/tree-sitter-dart-wasm/ +``` + +Expected: the wasm file is present, ~745 KB. + +- [ ] **Step 4: Write the package metadata** + +Create `understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json`: + +```json +{ + "name": "@understand-anything/tree-sitter-dart-wasm", + "version": "0.1.0", + "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" +} +``` + +- [ ] **Step 5: Write the BUILD provenance note** + +Create `understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md`: + +```markdown +# 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`. + +## 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. +``` + +- [ ] **Step 6: Run pnpm install to wire the workspace package** + +```bash +cd understand-anything-plugin +pnpm install 2>&1 | tail -5 +``` + +Expected: `Done in ` with no errors mentioning the new package. The package is now resolvable via `require.resolve("@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm")` from any workspace member that depends on it (which we'll wire in Task 3). + +- [ ] **Step 7: Commit** + +```bash +cd /Users/thejesh/Git/Understand-Anything +git add understand-anything-plugin/packages/tree-sitter-dart-wasm/ \ + understand-anything-plugin/pnpm-lock.yaml \ + understand-anything-plugin/pnpm-workspace.yaml +git commit -m "$(cat <<'EOF' +feat(tree-sitter-dart-wasm): vendor freshly-built dart WASM grammar + +The upstream tree-sitter-dart@1.0.0 ships a pre-`dylink.0` wasm that +fails to load in web-tree-sitter@0.26.x. The grammar source itself is +sound — rebuilding with the current tree-sitter-cli + wasi-sdk produces +a working dylink.0 wasm. Vendor that artifact as a workspace-internal +package so @understand-anything/core can depend on it via workspace:*. + +BUILD.md documents the provenance and rebuild instructions. +EOF +)" +``` + +--- + +## Task 2: Add the dartConfig LanguageConfig + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/languages/configs/dart.ts` +- Modify: `understand-anything-plugin/packages/core/src/languages/configs/index.ts` +- Modify: `understand-anything-plugin/packages/core/package.json` (add workspace dep) + +- [ ] **Step 1: Add the workspace dependency to core** + +Edit `understand-anything-plugin/packages/core/package.json` — add **one line** inside the `"dependencies"` block, in alphabetical position (before `fuse.js`): + +```json +"dependencies": { + "@understand-anything/tree-sitter-dart-wasm": "workspace:*", + "@tree-sitter-grammars/tree-sitter-kotlin": "1.1.0", + ... +} +``` + +Note: pnpm's workspace protocol uses `workspace:*` — same as how core would reference any other internal package. + +Run: +```bash +cd understand-anything-plugin +pnpm install 2>&1 | tail -3 +``` + +Expected: clean install, no warnings mentioning the workspace package. + +- [ ] **Step 2: Create the dart.ts config file** + +Create `understand-anything-plugin/packages/core/src/languages/configs/dart.ts`: + +```ts +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; +``` + +- [ ] **Step 3: Register dartConfig in the configs index** + +Edit `understand-anything-plugin/packages/core/src/languages/configs/index.ts`. Three places to edit: + +(a) Add the import alongside the other code-language imports (alphabetical-ish, between `cppConfig` and `csharpConfig` is fine): + +```ts +import { dartConfig } from "./dart.js"; +``` + +(b) Add `dartConfig` to the `builtinLanguageConfigs` array, inside the "Code languages" block (place between `cppConfig` and `csharpConfig`): + +```ts + // Code languages + typescriptConfig, + javascriptConfig, + pythonConfig, + goConfig, + rustConfig, + javaConfig, + rubyConfig, + phpConfig, + swiftConfig, + kotlinConfig, + luaConfig, + cConfig, + cppConfig, + dartConfig, + csharpConfig, +``` + +(c) Add `dartConfig` to the named re-export block in the same position. + +- [ ] **Step 4: Build core to verify TypeScript compiles** + +```bash +cd understand-anything-plugin +pnpm --filter @understand-anything/core build 2>&1 | tail -5 +``` + +Expected: `Done` with no tsc errors. + +- [ ] **Step 5: Write a smoke test that the config is registered and the grammar loads** + +This test is a sanity check — it doesn't exercise the extractor (Task 3 onwards does that). Append it to the existing test file +`understand-anything-plugin/packages/core/src/languages/__tests__/language-registry.test.ts` (look at the existing tests there for style; if no test file exists, the build step's import of `dartConfig` is enough sanity for this task). + +Run: +```bash +pnpm --filter @understand-anything/core test 2>&1 | tail -10 +``` + +Expected: all existing tests still pass. No regressions. + +- [ ] **Step 6: Verify the wasm actually loads via the existing TreeSitterPlugin** + +Write a one-off Node script at `/tmp/verify-dart-wasm.mjs`: + +```js +import { createRequire } from "node:module"; +import * as ts from "web-tree-sitter"; + +const require = createRequire(import.meta.url); +await ts.Parser.init(); +const wasmPath = require.resolve( + "@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm", +); +const Lang = await ts.Language.load(wasmPath); +const p = new ts.Parser(); +p.setLanguage(Lang); +const tree = p.parse("void main() { print('hi'); }"); +console.log("rootType:", tree.rootNode.type); +console.log("firstChild:", tree.rootNode.namedChild(0)?.type); +``` + +Run from inside core: +```bash +cd understand-anything-plugin/packages/core +cp /tmp/verify-dart-wasm.mjs ./verify-dart-wasm.mjs +node verify-dart-wasm.mjs +rm verify-dart-wasm.mjs +``` + +Expected output: +``` +rootType: program +firstChild: function_signature +``` + +If you instead see an `Error: ... at getDylinkMetadata`, the wasm is the wrong ABI — go back to Task 1, Step 2 and verify the build produced a `dylink.0` artifact. + +- [ ] **Step 7: Commit** + +```bash +git add understand-anything-plugin/packages/core/package.json \ + understand-anything-plugin/packages/core/src/languages/configs/dart.ts \ + understand-anything-plugin/packages/core/src/languages/configs/index.ts \ + understand-anything-plugin/pnpm-lock.yaml +git commit -m "$(cat <<'EOF' +feat(core): register dart LanguageConfig + +Adds the Dart language config and wires it into builtinLanguageConfigs +so .dart files are recognized by the language registry. References the +vendored @understand-anything/tree-sitter-dart-wasm package for grammar +loading. + +No extractor yet — structural extraction lands in the next commit. +EOF +)" +``` + +--- + +## Task 3: Scaffold DartExtractor + register it + +**Why before TDD steps:** Subsequent TDD tasks need an importable `DartExtractor` class to add tests against. This task creates the empty shell + registration; the next tasks fill in extraction logic test-first. + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts` +- Modify: `understand-anything-plugin/packages/core/src/plugins/extractors/index.ts` + +- [ ] **Step 1: Create the skeleton DartExtractor** + +Create `understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts`: + +```ts +import type { StructuralAnalysis, CallGraphEntry } from "../../types.js"; +import type { LanguageExtractor, TreeSitterNode } from "./types.js"; +import { findChild, findChildren } 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("_"); +} + +/** + * Dart extractor for tree-sitter structural analysis + call graph. + * + * Approach (matching `KotlinExtractor` convention): mixin / extension / enum + * declarations are folded into `StructuralAnalysis.classes[]` because the + * shared schema does not have a first-class slot for them. Extension + * declarations without a name surface as `"on "` so they aren't + * silently dropped. + */ +export class DartExtractor implements LanguageExtractor { + readonly languageIds = ["dart"]; + + extractStructure(rootNode: TreeSitterNode): StructuralAnalysis { + const functions: StructuralAnalysis["functions"] = []; + const classes: StructuralAnalysis["classes"] = []; + const imports: StructuralAnalysis["imports"] = []; + const exports: StructuralAnalysis["exports"] = []; + + // Implementation lands in subsequent tasks. + void rootNode; + void findChild; + void findChildren; + void isExported; + + return { functions, classes, imports, exports }; + } + + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { + // Implementation lands in a later task. + void rootNode; + return []; + } +} +``` + +- [ ] **Step 2: Register DartExtractor in the extractors index** + +Edit `understand-anything-plugin/packages/core/src/plugins/extractors/index.ts`. Three edits: + +(a) Add the named re-export beside the others: + +```ts +export { DartExtractor } from "./dart-extractor.js"; +``` + +(b) Add the import beside the others: + +```ts +import { DartExtractor } from "./dart-extractor.js"; +``` + +(c) Add `new DartExtractor()` to `builtinExtractors` (place between `CppExtractor` and `CSharpExtractor`): + +```ts +export const builtinExtractors: LanguageExtractor[] = [ + new TypeScriptExtractor(), + new PythonExtractor(), + new GoExtractor(), + new RustExtractor(), + new JavaExtractor(), + new RubyExtractor(), + new PhpExtractor(), + new CppExtractor(), + new DartExtractor(), + new CSharpExtractor(), + new KotlinExtractor(), +]; +``` + +- [ ] **Step 3: Build + test (must still pass)** + +```bash +pnpm --filter @understand-anything/core build 2>&1 | tail -3 +pnpm --filter @understand-anything/core test 2>&1 | tail -5 +``` + +Expected: tsc clean, all existing tests pass. (Skeleton extractor returns empty results, no behavior change for non-Dart files.) + +- [ ] **Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/index.ts +git commit -m "feat(core): scaffold DartExtractor + register in builtinExtractors + +Empty extractor that satisfies the LanguageExtractor interface so the +plugin pipeline can load it. Real extraction logic lands in subsequent +TDD commits. +" +``` + +--- + +## Task 4: TDD — top-level function extraction + +From here through Task 12 follow strict TDD: write failing test, verify it fails for the right reason, implement minimum to pass, verify pass, commit. Each task corresponds to a roughly-coherent slice of extractor behavior. + +**Reference test setup** — every test file in `__tests__/` uses the same `beforeAll` + `parse()` helper shape. Establish it once in Step 1, then re-use across Tasks 4–12. + +**Files (all of Tasks 4–12):** +- Create: `understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts` +- Modify: `understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts` + +- [ ] **Step 1: Create the test-file scaffold** + +Create `understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts`: + +```ts +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"]); + }); +}); +``` + +Run: +```bash +pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -10 +``` + +Expected: 1 test passes (the `languageIds` assertion), no errors. If `beforeAll` errors, the wasm path is wrong — fix before continuing. + +- [ ] **Step 2: Write the failing function-extraction tests** + +Append inside the `describe("DartExtractor", …)` block: + +```ts + describe("extractStructure - functions", () => { + it("extracts a simple top-level function with params and return type", () => { + const { tree, parser, root } = parse(`int add(int a, int b) => a + b;\n`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("add"); + expect(result.functions[0].params).toEqual(["a", "b"]); + expect(result.functions[0].returnType).toBe("int"); + + tree.delete(); + parser.delete(); + }); + + it("extracts a function with no params and void return type", () => { + const { tree, parser, root } = parse(`void noop() {}\n`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("noop"); + expect(result.functions[0].params).toEqual([]); + expect(result.functions[0].returnType).toBe("void"); + + tree.delete(); + parser.delete(); + }); + + it("extracts an async function with a generic return type", () => { + const { tree, parser, root } = parse(`Future fetch(String url) async { return ""; }\n`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("fetch"); + expect(result.functions[0].params).toEqual(["url"]); + expect(result.functions[0].returnType).toBe("Future"); + + tree.delete(); + parser.delete(); + }); + }); +``` + +Run: +```bash +pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -10 +``` + +Expected: 3 new tests FAIL because the extractor returns empty `functions`. The `languageIds` test still passes. + +- [ ] **Step 3: Implement function extraction** + +In `dart-extractor.ts`, replace the `extractStructure` body. The AST shape (verified live): + +- A top-level function appears as `program > function_signature` followed by a **sibling** `function_body`. (Not parent/child — `function_body` is a separate top-level node.) +- `function_signature` children: an optional return-type node (`type_identifier` or `void_type` or a generic `type` subtree), then `identifier` (the name), then `formal_parameter_list`. + +Add this helper at the top of the file (after the existing `isExported` helper): + +```ts +/** + * Extract the identifier name from a function_signature / method_signature + * node. The name is the first `identifier` child after any return-type + * subtree. + */ +function extractFunctionName(sig: TreeSitterNode): string | null { + const id = findChild(sig, "identifier"); + return id ? id.text : null; +} + +/** + * Extract parameter names from a `formal_parameter_list`. Each + * `formal_parameter` child carries the parameter name as its `identifier` + * child; we ignore the type annotation. + */ +function extractParams(sig: TreeSitterNode): string[] { + const params: string[] = []; + const paramList = findChild(sig, "formal_parameter_list"); + if (!paramList) return params; + for (const p of findChildren(paramList, "formal_parameter")) { + const id = findChild(p, "identifier"); + if (id) params.push(id.text); + } + return params; +} + +/** + * Extract the return type from a function_signature. The return type is the + * first NAMED child whose type is NOT `identifier` or `formal_parameter_list` + * or `type_parameters`. If there is no such child, the function has no + * declared return type (Dart infers it). + * + * Common shapes seen during AST probing: + * `int add(int a, int b)` → type_identifier "int" + * `void noop()` → void_type + * `Future fetch()`→ type_identifier "Future" wrapped in a type with type_arguments + */ +function extractReturnType(sig: TreeSitterNode): string | undefined { + 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 name / params without seeing a return type. + return undefined; + } + // This is the return type node. Its full text (including generics) is + // what we want. + return child.text; + } + return undefined; +} +``` + +Now replace `extractStructure`: + +```ts + 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; + } + } + + 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 }); + } + } +``` + +The four `void X;` lines from the Task 3 skeleton inside `extractStructure` are gone now (replaced by the real body). Leave the `void rootNode;` line in `extractCallGraph` for now — it'll be replaced when Task 12 implements call-graph extraction. + +- [ ] **Step 4: Run the function tests and verify pass** + +```bash +pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -15 +``` + +Expected: all 4 tests (including `languageIds`) pass. If one fails, inspect actual vs expected — adjust the helper if the AST shape for that case differs from what was assumed. + +- [ ] **Step 5: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — top-level function extraction" +``` + +--- + +## Task 5: TDD — class extraction (plain, abstract, with inheritance) + +**Files:** +- Modify: `dart-extractor.ts`, `dart-extractor.test.ts` + +- [ ] **Step 1: Write the failing class tests** + +Append inside `describe("DartExtractor", …)`: + +```ts + 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"); + + tree.delete(); + parser.delete(); + }); + + it("extracts an empty class", () => { + const { tree, parser, root } = parse(`class Empty {}\n`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Empty"); + expect(result.classes[0].methods).toEqual([]); + + tree.delete(); + parser.delete(); + }); + + it("extracts an abstract class with method requirements", () => { + const { tree, parser, root } = parse(`abstract class Shape { + double area(); +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Shape"); + expect(result.classes[0].methods).toContain("area"); + + tree.delete(); + parser.delete(); + }); + + it("extracts a class with extends + with + implements clauses", () => { + const { tree, parser, root } = parse(`class Square extends Shape with Comparable implements Cloneable { + double side; + Square(this.side); + double area() => side * side; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Square"); + expect(result.classes[0].methods).toContain("area"); + + tree.delete(); + parser.delete(); + }); + }); +``` + +Run tests — expect 4 new failures. + +- [ ] **Step 2: Implement class extraction** + +Class AST shape (verified live): + +- `program > class_definition { identifier(name), class_body }`. Inheritance clauses (`extends Foo`, `with Mixin`, `implements Iface`) appear as siblings between the `identifier` and `class_body`. We ignore them for now (out of scope for this task — captured at the class node's text level if ever needed; not required for the graph). +- `class_body > method_signature { function_signature { return_type, identifier, formal_parameter_list } }` followed by a sibling `function_body` (mirroring the top-level shape). +- `class_body > declaration { type_identifier, initialized_identifier_list { initialized_identifier { identifier(name) } } }` — this is a field declaration. + +Add a helper for class-body walking (after the function helpers): + +```ts +/** + * 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. + */ +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") { + const inner = findChild(member, "function_signature"); + if (!inner) continue; + const name = extractFunctionName(inner); + if (!name) continue; + methods.push(name); + functions.push({ + name, + lineRange: [member.startPosition.row + 1, member.endPosition.row + 1], + params: extractParams(inner), + returnType: extractReturnType(inner), + }); + if (isExported(name)) { + exports.push({ name, lineNumber: member.startPosition.row + 1 }); + } + } else if (member.type === "declaration") { + // Field declaration — surface initialized_identifier names as properties. + 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); + } + } + } +} +``` + +Add a case to the top-level switch: + +```ts + case "class_definition": + this.extractClassDefinition(node, classes, functions, exports); + break; +``` + +Add the private method: + +```ts + private extractClassDefinition( + declNode: TreeSitterNode, + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = findChild(declNode, "identifier"); + if (!nameNode) return; + const name = nameNode.text; + + const methods: string[] = []; + const properties: string[] = []; + + const body = findChild(declNode, "class_body"); + 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 }); + } + } +``` + +Run tests — expect all class tests to pass. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — class extraction with fields + methods" +``` + +--- + +## Task 6: TDD — constructors (default, named, factory) + +**Files:** dart-extractor.{ts,test.ts} + +- [ ] **Step 1: Write failing tests** + +Append: + +```ts + 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(); + }); + }); +``` + +Run — expect 3 failures. + +- [ ] **Step 2: Implement constructor handling** + +AST shapes (verified live): + +- Unnamed: `class_body > declaration > constructor_signature { identifier(class), formal_parameter_list }` — only ONE identifier. +- Named: `class_body > declaration > constructor_signature { identifier(class), identifier(named), formal_parameter_list }` — TWO identifiers; second is the named-constructor name. +- Factory: `class_body > method_signature > factory_constructor_signature { identifier(class), identifier(named), formal_parameter_list }` — wrapped in `method_signature`. + +Extend `collectClassBody`'s `for` loop. Both the `method_signature` branch and the `declaration` branch get a constructor check **at the top** that short-circuits before the existing logic. Full revised loop body: + +```ts + if (member.type === "method_signature") { + // Factory constructor lives inside method_signature as + // factory_constructor_signature; check that first. + const factory = findChild(member, "factory_constructor_signature"); + if (factory) { + const name = constructorName(factory); + if (name) { + methods.push(name); + functions.push({ + name, + lineRange: [member.startPosition.row + 1, member.endPosition.row + 1], + params: extractParams(factory), + returnType: undefined, + }); + if (isExported(name)) { + exports.push({ name, lineNumber: member.startPosition.row + 1 }); + } + } + continue; + } + // ...existing function_signature handling unchanged... + } else if (member.type === "declaration") { + const ctor = findChild(member, "constructor_signature"); + if (ctor) { + const name = constructorName(ctor); + if (name) { + methods.push(name); + functions.push({ + name, + lineRange: [member.startPosition.row + 1, member.endPosition.row + 1], + params: extractParams(ctor), + returnType: undefined, + }); + if (isExported(name)) { + exports.push({ name, lineNumber: member.startPosition.row + 1 }); + } + } + continue; + } + // ...existing field-declaration handling unchanged... + } +``` + +Add the helper `constructorName` at the top: + +```ts +/** + * Build a constructor's method-graph name from a constructor_signature / + * factory_constructor_signature node: + * - one identifier → unnamed constructor, name = "" + * - two identifiers → named constructor, name = "." + */ +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}`; +} +``` + +Run tests — expect all constructor tests pass; previously-passing tests remain green. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — constructor naming (default/named/factory)" +``` + +--- + +## Task 7: TDD — mixins + +**Files:** dart-extractor.{ts,test.ts} + +- [ ] **Step 1: Write failing tests** + +```ts + 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(); + }); + }); +``` + +Run — expect 2 failures. + +- [ ] **Step 2: Implement mixin extraction** + +AST shape: `program > mixin_declaration { identifier(name), [type_identifier(on)], class_body }`. Same body shape as `class_definition`. + +Add case to the top-level switch: + +```ts + case "mixin_declaration": + this.extractMixinDeclaration(node, classes, functions, exports); + break; +``` + +Implement (almost identical to `extractClassDefinition`; refactoring opportunity but kept separate for clarity): + +```ts + private extractMixinDeclaration( + declNode: TreeSitterNode, + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = findChild(declNode, "identifier"); + if (!nameNode) return; + const name = nameNode.text; + + const methods: string[] = []; + const properties: string[] = []; + + const body = findChild(declNode, "class_body"); + 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 }); + } + } +``` + +Run tests — expect mixin tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — mixin declarations" +``` + +--- + +## Task 8: TDD — extensions + +**Files:** dart-extractor.{ts,test.ts} + +- [ ] **Step 1: Write failing tests** + +```ts + 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(); + }); + }); +``` + +Run — expect 2 failures. + +- [ ] **Step 2: Implement extension extraction** + +AST shape (verified): + +- Named: `extension_declaration { identifier(name), type_identifier(on-type), extension_body }` +- Anonymous: `extension_declaration { type_identifier(on-type), extension_body }` — no leading identifier. + +Add the case to the top-level switch: + +```ts + case "extension_declaration": + this.extractExtensionDeclaration(node, classes, functions, exports); + break; +``` + +Implement: + +```ts + private extractExtensionDeclaration( + declNode: TreeSitterNode, + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + // Try named extension first: leading `identifier` child is the name. + const idNode = findChild(declNode, "identifier"); + let name: string; + if (idNode) { + name = idNode.text; + } else { + // Anonymous: name the entry after the target type so the graph builder + // doesn't drop it. The on-type is the first `type_identifier`. + const onType = findChild(declNode, "type_identifier"); + if (!onType) return; + name = `on ${onType.text}`; + } + + const methods: string[] = []; + const properties: string[] = []; + + const body = findChild(declNode, "extension_body"); + 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 }); + } + } +``` + +Run — expect extension tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — extension declarations (named + anonymous)" +``` + +--- + +## Task 9: TDD — enums + +**Files:** dart-extractor.{ts,test.ts} + +- [ ] **Step 1: Write failing tests** + +```ts + 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(); + }); + }); +``` + +Run — expect 1 failure. + +- [ ] **Step 2: Implement enum extraction** + +AST shape: `enum_declaration { identifier(name), enum_body { enum_constant { identifier }... } }`. + +Add case to the top-level switch: + +```ts + case "enum_declaration": + this.extractEnumDeclaration(node, classes, exports); + break; +``` + +Implement: + +```ts + 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 }); + } + } +``` + +Run — expect enum test passes. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — enum declarations" +``` + +--- + +## Task 10: TDD — import + export directives + +**Files:** dart-extractor.{ts,test.ts} + +- [ ] **Step 1: Write failing tests** + +```ts + 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(); + }); + }); + + 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(); + }); + }); +``` + +Run — expect 5 new failures. + +- [ ] **Step 2: Implement import/export extraction** + +AST shape (verified): + +- Top-level wrapper: `import_or_export { library_import | library_export }`. +- `library_import { import_specification { configurable_uri { uri { string_literal }, [combinator { 'show', identifier, ... }], [identifier(as-prefix)] } } }`. + - The `string_literal` text contains the surrounding quotes (`'foo.dart'`). + - A `combinator` named child holds `show`/`hide` keyword + identifier list. + - An `identifier` named child at the import_specification level is the `as` prefix. +- `library_export { configurable_uri { uri { string_literal } } }`. + +Add a helper at top of file: + +```ts +/** + * Unwrap the string-literal text from `uri > string_literal`, stripping the + * surrounding single or double quotes. + */ +function uriText(uriNode: TreeSitterNode): string | null { + const lit = findChild(uriNode, "string_literal"); + if (!lit) return null; + return lit.text.replace(/^['"]|['"]$/g, ""); +} +``` + +Add cases to the top-level switch: + +```ts + case "import_or_export": + this.extractImportOrExport(node, imports, exports); + break; +``` + +Implement: + +```ts + 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[] = []; + + // `show Bar, Baz` — combinator has identifier children for the shown names. + const combinators = findChildren(spec, "combinator"); + for (const c of combinators) { + for (const id of findChildren(c, "identifier")) { + specifiers.push(id.text); + } + } + + // `as Foo` — a direct `identifier` child of import_specification is the + // alias. Has to come AFTER the configurable_uri in source order. + const asId = findChild(spec, "identifier"); + if (asId && specifiers.length === 0) { + specifiers.push(asId.text); + } + + imports.push({ + source, + specifiers, + lineNumber: libImport.startPosition.row + 1, + }); + } + + 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, + }); + } +``` + +Run — expect import + export tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — import directives (package/relative/show/as) + export directives" +``` + +--- + +## Task 11: TDD — visibility (underscore-prefix rule) + +**Files:** dart-extractor.{ts,test.ts} + +- [ ] **Step 1: Write failing tests** + +```ts + 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(); + }); + }); +``` + +Run — first two tests should already pass (current implementation already uses `isExported(name)` everywhere). The class-member test should also pass thanks to `collectClassBody` calling `isExported`. If all three pass without code changes, this task is just a coverage commit. + +- [ ] **Step 2: Confirm all 3 pass; if any fail, add a missing `isExported` guard at the relevant emit site** + +```bash +pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -10 +``` + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "test(core): DartExtractor — visibility rule (underscore prefix)" +``` + +--- + +## Task 12: TDD — call graph + +**Files:** dart-extractor.{ts,test.ts} + +- [ ] **Step 1: Write failing tests** + +```ts + 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(); + }); + }); +``` + +Run — expect 2 failures (third passes because the stub returns `[]`). + +- [ ] **Step 2: Implement call-graph extraction** + +AST shape for a Dart call (verified live): + +``` +function_body + block + expression_statement + identifier "print" ← bare-call callee + selector + argument_part +``` + +And for a method-style call: + +``` +expression_statement + string_literal "'hi'" ← receiver + selector + unconditional_assignable_selector + identifier "toUpperCase" ← method-call callee (last identifier in the selector chain) + selector + argument_part +``` + +Key insight: in Dart's grammar, a call is represented as a target expression followed by one or more `selector` siblings, with the LAST `selector` containing an `argument_part`. The callee identifier is either: + +- The first identifier in the expression_statement (bare call), OR +- The last identifier appearing inside any `unconditional_assignable_selector` before the `selector` that contains `argument_part`. + +Pragmatic approach: walk every node, and whenever we see a `selector` containing an `argument_part`, look for the callee as the IDENTIFIER token immediately preceding it in the parent's children. If none, look inside the previous sibling `selector` for an `identifier` (the method name in chained call). + +Replace `extractCallGraph`: + +```ts + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { + const entries: CallGraphEntry[] = []; + const functionStack: string[] = []; + + const walk = (node: TreeSitterNode) => { + let pushed = false; + + // Push function_signature names (both top-level and inside method_signature). + if (node.type === "function_signature") { + const name = extractFunctionName(node); + if (name) { + functionStack.push(name); + pushed = true; + } + } + + // Detect a call: any `selector` node containing an `argument_part`. + if ( + node.type === "selector" && + findChild(node, "argument_part") && + functionStack.length > 0 + ) { + const callee = this.extractCalleeName(node); + if (callee) { + entries.push({ + caller: functionStack[functionStack.length - 1], + callee, + lineNumber: node.startPosition.row + 1, + }); + } + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walk(child); + } + + if (pushed) functionStack.pop(); + }; + + walk(rootNode); + return entries; + } + + /** + * Find the callee name for a `selector` node that contains an + * `argument_part`. We look at the parent's children: the callee identifier + * is either the immediately-preceding `identifier` sibling (bare call) or + * the last `identifier` inside the immediately-preceding `selector` + * sibling's `unconditional_assignable_selector` (method call). + */ + private extractCalleeName(callSelector: TreeSitterNode): string | null { + const parent = callSelector.parent; + if (!parent) return null; + + // Find this selector's index in the parent. + let myIdx = -1; + for (let i = 0; i < parent.childCount; i++) { + if (parent.child(i) === callSelector) { + 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; + } +``` + +Run — expect call-graph tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ + understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +git commit -m "feat(core): DartExtractor — call graph extraction" +``` + +--- + +## Task 13: Final verification + lint + push + +**Files:** none — verification only. + +- [ ] **Step 1: Run the full core test suite** + +```bash +cd /Users/thejesh/Git/Understand-Anything +pnpm --filter @understand-anything/core test 2>&1 | tail -20 +``` + +Expected: All existing tests pass AND the new Dart tests pass. Look for the summary line — should show counts like `Tests passed ( files)`. If any pre-existing test failed, investigate before continuing. + +- [ ] **Step 2: Run the skill build (must not regress)** + +```bash +pnpm --filter @understand-anything/skill build 2>&1 | tail -5 +``` + +Expected: tsc clean. + +- [ ] **Step 3: Run lint across the project** + +```bash +pnpm lint 2>&1 | tail -10 +``` + +Expected: clean (or only pre-existing warnings unrelated to our changes). Fix any errors introduced by our changes inline; do NOT commit lint warnings. + +- [ ] **Step 4: Run the full test suite** + +```bash +pnpm test 2>&1 | tail -10 +``` + +Expected: full repo suite passes, no regressions. + +- [ ] **Step 5: Manual smoke — verify integration with the real TreeSitterPlugin** + +Write a one-off Node script at `/tmp/smoke-dart.mjs`: + +```js +import { TreeSitterPlugin } from "@understand-anything/core"; +import { dartConfig } from "@understand-anything/core/languages"; + +const plugin = new TreeSitterPlugin([dartConfig]); +await plugin.init(); + +const dart = ` +import 'package:flutter/material.dart'; + +class Counter { + int count = 0; + void increment() => count++; +} + +void main() { + Counter().increment(); +} +`; + +const result = plugin.analyzeFile("example.dart", dart); +console.log(JSON.stringify(result, null, 2)); +``` + +Run from the core package: + +```bash +cd understand-anything-plugin/packages/core +cp /tmp/smoke-dart.mjs ./smoke-dart.mjs +node smoke-dart.mjs +rm smoke-dart.mjs +``` + +Expected output: a `StructuralAnalysis` JSON with non-empty `functions` (containing `main`, `increment`), `classes` (containing `Counter`), `imports` (containing `package:flutter/material.dart`), `exports` (containing `Counter`, `main`). + +If the imports/exports are subtly different from what the unit tests assert (e.g., empty `specifiers`), that's fine — the integration test just confirms the plugin loads and produces non-empty results. + +- [ ] **Step 6: Push the branch** + +```bash +git push -u origin feat/dart-language-support +``` + +Expected: branch lands on remote. PR creation is a separate user step — do NOT open a PR autonomously. + +--- + +## Coverage map (spec → tasks) + +| Spec section | Task(s) | +|---|---| +| File-level changes — tree-sitter-dart-wasm package + workspace wiring | Task 1 | +| File-level changes — core dependency + dartConfig + index | Task 2 | +| File-level changes — DartExtractor + index | Task 3 | +| File-level changes — dart-extractor.test.ts | Tasks 4–12 | +| `dartConfig` shape | Task 2, Step 2 | +| WASM grammar source — vendored | Task 1 | +| DartExtractor — top-level AST nodes handled (functions) | Task 4 | +| DartExtractor — classes + constructors | Tasks 5, 6 | +| DartExtractor — mixins | Task 7 | +| DartExtractor — extensions (named + anonymous) | Task 8 | +| DartExtractor — enums | Task 9 | +| DartExtractor — imports (three forms) | Task 10 | +| Top-level `export` directive | Task 10 | +| Visibility rule (underscore prefix) | Task 11 | +| Class body walking convention (methods → functions[]) | Task 5, Step 2 | +| Call graph (bare + method calls) | Task 12 | +| Error handling (inherited from existing pipeline; no new modes) | covered implicitly; verified by Task 13 smoke | +| Verification commands | Task 13 | +| Edge cases NOT handled (records, patterns, `part of`) | not implemented, by design — spec rationale stands | + +## Self-review notes (already applied) + +- All step code blocks include the EXACT code to write; no "fill in similar code" cross-references. +- Method signatures used across tasks (`extractFunctionName`, `extractParams`, `extractReturnType`, `collectClassBody`, `constructorName`, `uriText`, `isExported`) are consistent everywhere they appear. +- The `extractCallGraph` implementation in Task 12 uses the same `extractFunctionName` helper introduced in Task 4 — no name drift. +- AST shapes for every walked node have been verified live against the freshly-built wasm; the plan is not speculating about grammar structure. +- Each task ends with a commit step so progress is incremental and the branch always builds. +- Task 11 may pass without code changes if Tasks 4–10 wired `isExported` correctly throughout; this is an intentional "coverage commit" tied to the spec's call-out that visibility is the one thing reviewers will trip on. From 3587ffa7a390623f89cf9ff9ddf7226ea895cc08 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 04:38:41 -0700 Subject: [PATCH 04/20] feat(tree-sitter-dart-wasm): vendor freshly-built dart WASM grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream tree-sitter-dart@1.0.0 ships a pre-`dylink.0` wasm that fails to load in web-tree-sitter@0.26.x. The grammar source itself is sound — rebuilding with the current tree-sitter-cli + wasi-sdk produces a working dylink.0 wasm. Vendor that artifact as a workspace-internal package so @understand-anything/core can depend on it via workspace:*. BUILD.md documents the provenance and rebuild instructions. --- .../packages/tree-sitter-dart-wasm/BUILD.md | 47 ++++++ .../tree-sitter-dart-wasm/package.json | 9 ++ .../tree-sitter-dart.wasm | Bin 0 -> 765060 bytes understand-anything-plugin/pnpm-lock.yaml | 153 ++++++++++++++++-- 4 files changed, 199 insertions(+), 10 deletions(-) create mode 100644 understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md create mode 100644 understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json create mode 100644 understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm diff --git a/understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md b/understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md new file mode 100644 index 0000000..d6c636f --- /dev/null +++ b/understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md @@ -0,0 +1,47 @@ +# tree-sitter-dart WASM (vendored) + +This directory ships a pre-built `tree-sitter-dart.wasm` because the upstream +npm release does not. + +## Why vendored + +The published `tree-sitter-dart@1.0.0` (2023-02-24) tarball does include a +`tree-sitter-dart.wasm`, but it was built with a pre-`dylink.0` tree-sitter +CLI. `web-tree-sitter@0.26.x` — the loader this project uses — expects the +newer `dylink.0` custom-section name and refuses to load the older format +(failure surfaces in `getDylinkMetadata`). + +Rebuilding the same upstream grammar.js with a current +`tree-sitter-cli@0.26.x` produces a `dylink.0` wasm that loads cleanly. + +## How to rebuild + +```bash +npm install -g tree-sitter-cli@latest +cd /tmp && npm pack tree-sitter-dart@1.0.0 +tar xzf tree-sitter-dart-1.0.0.tgz +cd package +tree-sitter build --wasm +cp tree-sitter-dart.wasm \ + /path/to/understand-anything-plugin/packages/tree-sitter-dart-wasm/ +``` + +Verify the resulting wasm: + +```bash +head -c 30 tree-sitter-dart.wasm | xxd | head -1 +# Expect: ...dylin / k.0... +``` + +## Provenance + +- Grammar source: `tree-sitter-dart@1.0.0` (publisher: amaanq) — `grammar.js` + unchanged, only the wasm artifact is regenerated. +- Built with: `tree-sitter-cli@0.26.x`, `wasi-sdk-29-arm64-macos`. +- License: MIT, inherited from tree-sitter-dart@1.0.0 (publisher: amaanq). + +## When to remove this package + +If amaanq publishes a refreshed `tree-sitter-dart` with a `dylink.0` wasm, +this workspace package can be deleted and the dependency in +`@understand-anything/core` flipped to the upstream package. diff --git a/understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json b/understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json new file mode 100644 index 0000000..595b436 --- /dev/null +++ b/understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json @@ -0,0 +1,9 @@ +{ + "name": "@understand-anything/tree-sitter-dart-wasm", + "version": "0.1.0", + "type": "module", + "description": "Vendored tree-sitter-dart WASM grammar built with the modern dylink.0 ABI for use with web-tree-sitter@^0.26.", + "main": "tree-sitter-dart.wasm", + "files": ["tree-sitter-dart.wasm", "BUILD.md"], + "license": "MIT" +} diff --git a/understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm b/understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm new file mode 100644 index 0000000000000000000000000000000000000000..4154b05abcb7ed2e1e32c9abe6fae65f700ad322 GIT binary patch literal 765060 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

%spaP-HF#`V_|@ub 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 literal 0 HcmV?d00001 diff --git a/understand-anything-plugin/pnpm-lock.yaml b/understand-anything-plugin/pnpm-lock.yaml index 16b6eed..23c375d 100644 --- a/understand-anything-plugin/pnpm-lock.yaml +++ b/understand-anything-plugin/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@understand-anything/core': specifier: workspace:* version: link:packages/core + graphology: + specifier: ~0.26.0 + version: 0.26.0(graphology-types@0.24.8) + graphology-communities-louvain: + specifier: ^2.0.2 + version: 2.0.2(graphology-types@0.24.8) devDependencies: '@types/node': specifier: ^22.0.0 @@ -24,6 +30,9 @@ importers: packages/core: dependencies: + '@tree-sitter-grammars/tree-sitter-kotlin': + specifier: 1.1.0 + version: 1.1.0 fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -133,7 +142,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 +154,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 +165,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 +493,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 +635,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 +691,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 +1120,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 +1248,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 +1430,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 +1809,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 +2321,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 +2429,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 +2437,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 +2788,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 +3202,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 +3547,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 +3568,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 +3613,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 From 62932684c61b92a333b7ed906a90009f20b56c4f Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 04:46:13 -0700 Subject: [PATCH 05/20] feat(core): register dart LanguageConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the Dart language config and wires it into builtinLanguageConfigs so .dart files are recognized by the language registry. References the vendored @understand-anything/tree-sitter-dart-wasm package for grammar loading. No extractor yet — structural extraction lands in the next commit. --- .../packages/core/package.json | 1 + .../src/__tests__/language-registry.test.ts | 4 +-- .../core/src/languages/configs/dart.ts | 29 +++++++++++++++++++ .../core/src/languages/configs/index.ts | 3 ++ understand-anything-plugin/pnpm-lock.yaml | 3 ++ 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/dart.ts diff --git a/understand-anything-plugin/packages/core/package.json b/understand-anything-plugin/packages/core/package.json index 7ea50ae..e54ce72 100644 --- a/understand-anything-plugin/packages/core/package.json +++ b/understand-anything-plugin/packages/core/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@tree-sitter-grammars/tree-sitter-kotlin": "1.1.0", + "@understand-anything/tree-sitter-dart-wasm": "workspace:*", "fuse.js": "^7.1.0", "ignore": "^7.0.5", "tree-sitter-c-sharp": "^0.23.1", diff --git a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts index 7a3c774..4ecad11 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts @@ -49,10 +49,10 @@ describe("LanguageRegistry", () => { }); describe("createDefault", () => { - it("registers all 40 built-in language configs", () => { + it("registers all 41 built-in language configs", () => { const registry = LanguageRegistry.createDefault(); const all = registry.getAllLanguages(); - expect(all.length).toBe(40); + expect(all.length).toBe(41); }); it("maps all expected extensions", () => { diff --git a/understand-anything-plugin/packages/core/src/languages/configs/dart.ts b/understand-anything-plugin/packages/core/src/languages/configs/dart.ts new file mode 100644 index 0000000..2d94a23 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/dart.ts @@ -0,0 +1,29 @@ +import type { LanguageConfig } from "../types.js"; + +export const dartConfig = { + id: "dart", + displayName: "Dart", + extensions: [".dart"], + treeSitter: { + wasmPackage: "@understand-anything/tree-sitter-dart-wasm", + wasmFile: "tree-sitter-dart.wasm", + }, + concepts: [ + "null safety", + "mixins", + "extensions", + "isolates", + "async/await", + "streams", + "factory constructors", + "named constructors", + "records", + "sealed classes", + ], + filePatterns: { + entryPoints: ["lib/main.dart", "bin/*.dart"], + barrels: ["lib/*.dart"], + tests: ["test/**/*_test.dart"], + config: ["pubspec.yaml", "analysis_options.yaml"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/index.ts b/understand-anything-plugin/packages/core/src/languages/configs/index.ts index 4893c4d..6a949e8 100644 --- a/understand-anything-plugin/packages/core/src/languages/configs/index.ts +++ b/understand-anything-plugin/packages/core/src/languages/configs/index.ts @@ -11,6 +11,7 @@ import { swiftConfig } from "./swift.js"; import { kotlinConfig } from "./kotlin.js"; import { cConfig } from "./c.js"; import { cppConfig } from "./cpp.js"; +import { dartConfig } from "./dart.js"; import { csharpConfig } from "./csharp.js"; import { luaConfig } from "./lua.js"; // Non-code language configs @@ -56,6 +57,7 @@ export const builtinLanguageConfigs: LanguageConfig[] = [ luaConfig, cConfig, cppConfig, + dartConfig, csharpConfig, // Non-code languages markdownConfig, @@ -101,6 +103,7 @@ export { luaConfig, cConfig, cppConfig, + dartConfig, csharpConfig, // Non-code languages markdownConfig, diff --git a/understand-anything-plugin/pnpm-lock.yaml b/understand-anything-plugin/pnpm-lock.yaml index 23c375d..e885e21 100644 --- a/understand-anything-plugin/pnpm-lock.yaml +++ b/understand-anything-plugin/pnpm-lock.yaml @@ -33,6 +33,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 From 072bab798cd5fd0c2ec4e61d23ec7ad37d81f5e6 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 04:50:00 -0700 Subject: [PATCH 06/20] feat(core): scaffold DartExtractor + register in builtinExtractors Empty extractor that satisfies the LanguageExtractor interface so the plugin pipeline can load it. Real extraction logic lands in subsequent TDD commits. --- .../src/plugins/extractors/dart-extractor.ts | 48 +++++++++++++++++++ .../core/src/plugins/extractors/index.ts | 3 ++ 2 files changed, 51 insertions(+) create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts new file mode 100644 index 0000000..a8724f4 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -0,0 +1,48 @@ +import type { StructuralAnalysis, CallGraphEntry } from "../../types.js"; +import type { LanguageExtractor, TreeSitterNode } from "./types.js"; +import { findChild, findChildren } 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("_"); +} + +/** + * Dart extractor for tree-sitter structural analysis + call graph. + * + * Approach (matching `KotlinExtractor` convention): mixin / extension / enum + * declarations are folded into `StructuralAnalysis.classes[]` because the + * shared schema does not have a first-class slot for them. Extension + * declarations without a name surface as `"on "` so they aren't + * silently dropped. + */ +export class DartExtractor implements LanguageExtractor { + readonly languageIds = ["dart"]; + + extractStructure(rootNode: TreeSitterNode): StructuralAnalysis { + const functions: StructuralAnalysis["functions"] = []; + const classes: StructuralAnalysis["classes"] = []; + const imports: StructuralAnalysis["imports"] = []; + const exports: StructuralAnalysis["exports"] = []; + + // Implementation lands in subsequent tasks. + void rootNode; + void findChild; + void findChildren; + void isExported; + + return { functions, classes, imports, exports }; + } + + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { + // Implementation lands in a later task. + void rootNode; + return []; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts index 4f0b5a3..8fbe736 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts @@ -9,6 +9,7 @@ export { RubyExtractor } from "./ruby-extractor.js"; export { PhpExtractor } from "./php-extractor.js"; export { CppExtractor } from "./cpp-extractor.js"; export { CSharpExtractor } from "./csharp-extractor.js"; +export { DartExtractor } from "./dart-extractor.js"; export { KotlinExtractor } from "./kotlin-extractor.js"; import type { LanguageExtractor } from "./types.js"; @@ -21,6 +22,7 @@ import { RubyExtractor } from "./ruby-extractor.js"; import { PhpExtractor } from "./php-extractor.js"; import { CppExtractor } from "./cpp-extractor.js"; import { CSharpExtractor } from "./csharp-extractor.js"; +import { DartExtractor } from "./dart-extractor.js"; import { KotlinExtractor } from "./kotlin-extractor.js"; export const builtinExtractors: LanguageExtractor[] = [ @@ -33,5 +35,6 @@ export const builtinExtractors: LanguageExtractor[] = [ new PhpExtractor(), new CppExtractor(), new CSharpExtractor(), + new DartExtractor(), new KotlinExtractor(), ]; From 136f85c1c8e350a5c408e166b3533b7f4f519452 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 04:55:06 -0700 Subject: [PATCH 07/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20top-level=20function=20extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add TDD tests and implement extractTopLevelFunction with helpers for extracting function name, params, and return type (including generics where the grammar emits type_identifier + type_arguments as siblings). Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 77 +++++++++++++ .../src/plugins/extractors/dart-extractor.ts | 108 +++++++++++++++++- 2 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts new file mode 100644 index 0000000..ce44fc9 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { createRequire } from "node:module"; +import { DartExtractor } from "../dart-extractor.js"; + +const require = createRequire(import.meta.url); + +let Parser: any; +let Language: any; +let dartLang: any; + +beforeAll(async () => { + const mod = await import("web-tree-sitter"); + Parser = mod.Parser; + Language = mod.Language; + await Parser.init(); + const wasmPath = require.resolve( + "@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm", + ); + dartLang = await Language.load(wasmPath); +}); + +function parse(code: string) { + const parser = new Parser(); + parser.setLanguage(dartLang); + const tree = parser.parse(code); + const root = tree.rootNode; + return { tree, parser, root }; +} + +describe("DartExtractor", () => { + const extractor = new DartExtractor(); + + it("has correct languageIds", () => { + expect(extractor.languageIds).toEqual(["dart"]); + }); + + describe("extractStructure - functions", () => { + it("extracts a simple top-level function with params and return type", () => { + const { tree, parser, root } = parse(`int add(int a, int b) => a + b;\n`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("add"); + expect(result.functions[0].params).toEqual(["a", "b"]); + expect(result.functions[0].returnType).toBe("int"); + + tree.delete(); + parser.delete(); + }); + + it("extracts a function with no params and void return type", () => { + const { tree, parser, root } = parse(`void noop() {}\n`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("noop"); + expect(result.functions[0].params).toEqual([]); + expect(result.functions[0].returnType).toBe("void"); + + tree.delete(); + parser.delete(); + }); + + it("extracts an async function with a generic return type", () => { + const { tree, parser, root } = parse(`Future fetch(String url) async { return ""; }\n`); + const result = extractor.extractStructure(root); + + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("fetch"); + expect(result.functions[0].params).toEqual(["url"]); + expect(result.functions[0].returnType).toBe("Future"); + + tree.delete(); + parser.delete(); + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index a8724f4..94de626 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -13,6 +13,79 @@ function isExported(name: string): boolean { return !name.startsWith("_"); } +/** + * Extract the identifier name from a `function_signature` node. + * + * NOTE: for `method_signature` (class-body method declarations), callers + * must first unwrap to the inner `function_signature` child before invoking + * this helper — the Dart grammar layers `method_signature > function_signature` + * and `findChild(..., "identifier")` would otherwise miss the function name. + */ +function extractFunctionName(sig: TreeSitterNode): string | null { + const id = findChild(sig, "identifier"); + return id ? id.text : null; +} + +/** + * Extract parameter names from a `formal_parameter_list`. Each + * `formal_parameter` child carries the parameter name as its `identifier` + * child; we ignore the type annotation. + * + * Currently only required positional parameters (`formal_parameter` direct + * children) are surfaced. Dart's optional positional (`[...]`) and named + * (`{...}`) parameters are wrapped in `optional_formal_parameters` and + * `named_parameter_list` container nodes respectively; supporting those is + * left for a follow-up — the project-graph use case does not currently + * distinguish parameter kinds. + */ +function extractParams(sig: TreeSitterNode): string[] { + const params: string[] = []; + const paramList = findChild(sig, "formal_parameter_list"); + if (!paramList) return params; + for (const p of findChildren(paramList, "formal_parameter")) { + const id = findChild(p, "identifier"); + if (id) params.push(id.text); + } + return params; +} + +/** + * Extract the return type from a function_signature. The return type is the + * sequence of NAMED children that appear before the function name + * (`identifier`) or `formal_parameter_list`. If there is no such child, the + * function has no declared return type (Dart infers it). + * + * Common shapes seen during AST probing: + * `int add(int a, int b)` → [type_identifier "int"] + * `void noop()` → [void_type] + * `Future fetch()`→ [type_identifier "Future", type_arguments ""] + * + * For generic types the grammar emits the base type and the type arguments as + * separate sibling nodes, so we collect ALL nodes before `identifier` and + * concatenate their text to reconstruct the full type spelling. + */ +function extractReturnType(sig: TreeSitterNode): string | undefined { + const parts: string[] = []; + for (let i = 0; i < sig.childCount; i++) { + const child = sig.child(i); + if (!child || !child.isNamed) continue; + if ( + child.type === "identifier" || + child.type === "formal_parameter_list" || + child.type === "type_parameters" + ) { + // Reached the function NAME (`identifier`), the parameter list, or the + // generic-parameter list (`type_parameters` is the function's own + // generics, e.g. `` in `T fn(T x)`). Anything we passed before + // this point WAS the return type; if we hit this stop without having + // collected anything, the function has no declared return type. + break; + } + parts.push(child.text); + } + return parts.length > 0 ? parts.join("") : undefined; +} + /** * Dart extractor for tree-sitter structural analysis + call graph. * @@ -31,15 +104,40 @@ export class DartExtractor implements LanguageExtractor { const imports: StructuralAnalysis["imports"] = []; const exports: StructuralAnalysis["exports"] = []; - // Implementation lands in subsequent tasks. - void rootNode; - void findChild; - void findChildren; - void isExported; + 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; + } + } 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 }); + } + } + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { // Implementation lands in a later task. void rootNode; From f4fc8027437c6737f0362255cc2b42e56ee3ba5c Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:02:48 -0700 Subject: [PATCH 08/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20class=20extraction=20with=20fields=20+=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 74 +++++++++++ .../src/plugins/extractors/dart-extractor.ts | 120 +++++++++++++++++- 2 files changed, 190 insertions(+), 4 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index ce44fc9..5aa3445 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -74,4 +74,78 @@ describe("DartExtractor", () => { 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 appear as `method_signature > getter_signature`, a separate node + // type from `function_signature` — not yet surfaced (documented limitation). + expect(result.classes[0].methods).not.toContain("value"); + + tree.delete(); + parser.delete(); + }); + + it("extracts an empty class", () => { + const { tree, parser, root } = parse(`class Empty {}\n`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Empty"); + expect(result.classes[0].methods).toEqual([]); + + tree.delete(); + parser.delete(); + }); + + it("extracts an abstract class with method requirements", () => { + const { tree, parser, root } = parse(`abstract class Shape { + double area(); +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Shape"); + expect(result.classes[0].methods).toContain("area"); + + tree.delete(); + parser.delete(); + }); + + it("extracts a class with extends + with + implements clauses", () => { + const { tree, parser, root } = parse(`class Square extends Shape with Comparable implements Cloneable { + double side; + Square(this.side); + double area() => side * side; +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Square"); + expect(result.classes[0].methods).toContain("area"); + + tree.delete(); + parser.delete(); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index 94de626..443af3d 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -16,10 +16,12 @@ function isExported(name: string): boolean { /** * Extract the identifier name from a `function_signature` node. * - * NOTE: for `method_signature` (class-body method declarations), callers - * must first unwrap to the inner `function_signature` child before invoking - * this helper — the Dart grammar layers `method_signature > function_signature` - * and `findChild(..., "identifier")` would otherwise miss the function name. + * 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"); @@ -86,6 +88,83 @@ function extractReturnType(sig: TreeSitterNode): string | undefined { 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 }); + } +} + +/** + * 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") { + // Concrete method: `method_signature > function_signature`. + // NOTE: `getter_signature` also nests under `method_signature` but is a + // separate node type — getters are not yet surfaced (documented limitation). + 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") { + // 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. + 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. * @@ -112,6 +191,9 @@ export class DartExtractor implements LanguageExtractor { case "function_signature": this.extractTopLevelFunction(node, functions, exports); break; + case "class_definition": + this.extractClassDefinition(node, classes, functions, exports); + break; } } @@ -138,6 +220,36 @@ export class DartExtractor implements LanguageExtractor { } } + private extractClassDefinition( + declNode: TreeSitterNode, + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + const nameNode = findChild(declNode, "identifier"); + if (!nameNode) return; + const name = nameNode.text; + + const methods: string[] = []; + const properties: string[] = []; + + const body = findChild(declNode, "class_body"); + 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 }); + } + } + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { // Implementation lands in a later task. void rootNode; From 893208efb36b0e687cdaaae61f8b9abb6247a4f7 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:12:41 -0700 Subject: [PATCH 09/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20constructor=20naming=20(default/named/factory)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add constructorName() helper and extend collectClassBody() to surface unnamed constructors as "ClassName", named constructors as "Class.named", and factory named constructors as "Class.named" in methods[]/functions[]. Probe confirmed plan's AST shapes match exactly; extractReturnType returns undefined for all constructor forms (factory keyword is an unnamed node). Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 42 +++++++++++++++++++ .../src/plugins/extractors/dart-extractor.ts | 41 ++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 5aa3445..e9f98d6 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -148,4 +148,46 @@ describe("DartExtractor", () => { 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(); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index 443af3d..baae9c1 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -113,6 +113,29 @@ function pushMethod( } } +/** + * Build a constructor's method-graph name from a constructor_signature / + * factory_constructor_signature node: + * - one identifier → unnamed constructor, name = "" + * - two identifiers → named constructor, name = "." + * + * Returns null when no identifier is present (defensive — should not happen + * for a real constructor declaration). + * + * Probe findings (2026-06-13): the plan's claimed AST shapes match exactly. + * - Unnamed: constructor_signature { identifier[Foo], formal_parameter_list } + * - Named: constructor_signature { identifier[Foo], identifier[zero], formal_parameter_list, ... } + * - Factory: factory_constructor_signature { , identifier[Foo], identifier[fromString], formal_parameter_list } + * extractReturnType returns undefined for all three (factory keyword is unnamed, + * so it is skipped; the loop stops at the first identifier). + */ +function constructorName(sig: TreeSitterNode): string | null { + const ids = findChildren(sig, "identifier"); + if (ids.length === 0) return null; + if (ids.length === 1) return ids[0].text; + return `${ids[0].text}.${ids[1].text}`; +} + /** * Walk a `class_body` (or `extension_body` / `enum_body`) and collect * `method_signature` declarations into the class's `methods` array AND the @@ -135,6 +158,15 @@ function collectClassBody( 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; + } // Concrete method: `method_signature > function_signature`. // NOTE: `getter_signature` also nests under `method_signature` but is a // separate node type — getters are not yet surfaced (documented limitation). @@ -144,6 +176,15 @@ function collectClassBody( 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 method declarations (e.g. `double area();`) appear as // `declaration > function_signature` — not wrapped in `method_signature`. const fnSig = findChild(member, "function_signature"); From 05ce514db5cfd2923b799475abe203f02bef9b4e Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:15:46 -0700 Subject: [PATCH 10/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20mixin=20declarations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add mixin_declaration handling to extractStructure, folding mixins into classes[] (same convention as class_definition). The `on` constraint sibling is intentionally ignored for graph purposes. Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 29 +++++++++++++++++++ .../src/plugins/extractors/dart-extractor.ts | 20 +++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index e9f98d6..0947b11 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -190,4 +190,33 @@ describe("DartExtractor", () => { 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(); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index baae9c1..20e6cac 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -233,7 +233,10 @@ export class DartExtractor implements LanguageExtractor { this.extractTopLevelFunction(node, functions, exports); break; case "class_definition": - this.extractClassDefinition(node, classes, functions, exports); + this.extractClassLikeDeclaration(node, "class_body", classes, functions, exports); + break; + case "mixin_declaration": + this.extractClassLikeDeclaration(node, "class_body", classes, functions, exports); break; } } @@ -261,8 +264,19 @@ export class DartExtractor implements LanguageExtractor { } } - private extractClassDefinition( + /** + * 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`. + * + * Anonymous variants (e.g. `extension on Foo` with no name) are handled by + * the caller — this method requires `declNode` to have a leading + * `identifier` child for the name. + */ + private extractClassLikeDeclaration( declNode: TreeSitterNode, + bodyNodeType: string, classes: StructuralAnalysis["classes"], functions: StructuralAnalysis["functions"], exports: StructuralAnalysis["exports"], @@ -274,7 +288,7 @@ export class DartExtractor implements LanguageExtractor { const methods: string[] = []; const properties: string[] = []; - const body = findChild(declNode, "class_body"); + const body = findChild(declNode, bodyNodeType); if (body) { collectClassBody(body, methods, properties, functions, exports); } From 306ee2c07077c610b8e41ac12e3be79135dd4bd2 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:21:25 -0700 Subject: [PATCH 11/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20extension=20declarations=20(named=20+=20anonymous)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 31 +++++++++++ .../src/plugins/extractors/dart-extractor.ts | 55 +++++++++++++++++-- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 0947b11..3bab6ea 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -219,4 +219,35 @@ describe("DartExtractor", () => { 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(); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index 20e6cac..dacdfee 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -238,6 +238,9 @@ export class DartExtractor implements LanguageExtractor { case "mixin_declaration": this.extractClassLikeDeclaration(node, "class_body", classes, functions, exports); break; + case "extension_declaration": + this.extractExtensionDeclaration(node, classes, functions, exports); + break; } } @@ -270,9 +273,9 @@ export class DartExtractor implements LanguageExtractor { * `extension_declaration`. The only difference between these shapes is the * body's node type name, which is passed in via `bodyNodeType`. * - * Anonymous variants (e.g. `extension on Foo` with no name) are handled by - * the caller — this method requires `declNode` to have a leading - * `identifier` child for the name. + * 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, @@ -280,10 +283,16 @@ export class DartExtractor implements LanguageExtractor { classes: StructuralAnalysis["classes"], functions: StructuralAnalysis["functions"], exports: StructuralAnalysis["exports"], + nameOverride?: string, ): void { - const nameNode = findChild(declNode, "identifier"); - if (!nameNode) return; - const name = nameNode.text; + 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[] = []; @@ -305,6 +314,40 @@ export class DartExtractor implements LanguageExtractor { } } + private extractExtensionDeclaration( + declNode: TreeSitterNode, + classes: StructuralAnalysis["classes"], + functions: StructuralAnalysis["functions"], + exports: StructuralAnalysis["exports"], + ): void { + // Named extension — extractClassLikeDeclaration finds the leading identifier itself. + const idNode = findChild(declNode, "identifier"); + if (idNode) { + this.extractClassLikeDeclaration( + declNode, + "extension_body", + classes, + functions, + exports, + ); + return; + } + + // Anonymous extension — no `identifier` child. The on-type is the first + // `type_identifier`. Name the entry "on " so the graph + // builder doesn't drop it for having an empty name. + const onType = findChild(declNode, "type_identifier"); + if (!onType) return; + this.extractClassLikeDeclaration( + declNode, + "extension_body", + classes, + functions, + exports, + `on ${onType.text}`, + ); + } + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { // Implementation lands in a later task. void rootNode; From fd1d1c6450abcef6876b46b6976ff6134dee24bf Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:26:12 -0700 Subject: [PATCH 12/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20enum=20declarations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds enum_declaration handling to DartExtractor: enum constants are surfaced as properties[] so the structural graph captures Color.red / Color.green etc. Implements Task 9 of the Dart language support plan (TDD, 16/16 dart tests pass, full suite 708/708). Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 13 ++++++++ .../src/plugins/extractors/dart-extractor.ts | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 3bab6ea..4cf09f2 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -220,6 +220,19 @@ describe("DartExtractor", () => { }); }); + 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 { diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index dacdfee..5e37f46 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -241,6 +241,9 @@ export class DartExtractor implements LanguageExtractor { case "extension_declaration": this.extractExtensionDeclaration(node, classes, functions, exports); break; + case "enum_declaration": + this.extractEnumDeclaration(node, classes, exports); + break; } } @@ -348,6 +351,36 @@ export class DartExtractor implements LanguageExtractor { ); } + 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 }); + } + } + extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { // Implementation lands in a later task. void rootNode; From 798c1747b915303059ab838d7aa47a02f2a703e6 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:30:40 -0700 Subject: [PATCH 13/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20import=20directives=20(package/relative/show/as)=20+=20expor?= =?UTF-8?q?t=20directives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 64 +++++++++++ .../src/plugins/extractors/dart-extractor.ts | 100 +++++++++++++++++- 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 4cf09f2..9954743 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -263,4 +263,68 @@ describe("DartExtractor", () => { 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(); + }); + }); + + 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(); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index 5e37f46..f65adc2 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -1,6 +1,6 @@ import type { StructuralAnalysis, CallGraphEntry } from "../../types.js"; import type { LanguageExtractor, TreeSitterNode } from "./types.js"; -import { findChild, findChildren } from "./base-extractor.js"; +import { findChild, findChildren, getStringValue } from "./base-extractor.js"; /** * Whether a Dart name is exported. @@ -113,6 +113,17 @@ function pushMethod( } } +/** + * 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: @@ -244,6 +255,9 @@ export class DartExtractor implements LanguageExtractor { case "enum_declaration": this.extractEnumDeclaration(node, classes, exports); break; + case "import_or_export": + this.extractImportOrExport(node, imports, exports); + break; } } @@ -381,6 +395,90 @@ export class DartExtractor implements LanguageExtractor { } } + 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[] { // Implementation lands in a later task. void rootNode; From 23d6b1a39c366c36ae7c9a1d8950916eed9723da Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:36:23 -0700 Subject: [PATCH 14/20] =?UTF-8?q?test(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20visibility=20rule=20(underscore=20prefix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/dart-extractor.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 9954743..b6c5401 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -327,4 +327,46 @@ describe("DartExtractor", () => { 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(); + }); + }); }); From a1ee028c3523860ef5bbd534209d8e878fbd75c7 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:41:43 -0700 Subject: [PATCH 15/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20call=20graph=20extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements extractCallGraph with a sibling-aware walk that pairs each function_signature with its subsequent function_body sibling (Dart's AST differs from Kotlin's: signature and body are siblings, not parent/child). Detects call sites via selector nodes containing argument_part; uses startIndex for sibling lookup (web-tree-sitter returns new wrapper objects per child() call, making === unreliable). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/dart-extractor.test.ts | 38 +++++ .../src/plugins/extractors/dart-extractor.ts | 137 +++++++++++++++++- 2 files changed, 172 insertions(+), 3 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index b6c5401..5471bd9 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -328,6 +328,44 @@ describe("DartExtractor", () => { }); }); + 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(); + }); + }); + describe("extractStructure - visibility", () => { it("does NOT export a top-level declaration whose name starts with _", () => { const { tree, parser, root } = parse(`int _helper() => 1; diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index f65adc2..8f3d10f 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -480,8 +480,139 @@ export class DartExtractor implements LanguageExtractor { } extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { - // Implementation lands in a later task. - void rootNode; - return []; + 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, + }); + } + } + 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 function_signature; sibling function_body follows. + const inner = findChild(child, "function_signature"); + if (inner) pendingName = extractFunctionName(inner); + 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; } } From 6be6ad057ce6569663c0dddbf648c5e745c143bd Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 05:57:24 -0700 Subject: [PATCH 16/20] =?UTF-8?q?chore:=20bump=20version=202.7.7=20?= =?UTF-8?q?=E2=86=92=202.8.0=20for=20Dart=20language=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/plugin.json | 2 +- .copilot-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- understand-anything-plugin/.claude-plugin/plugin.json | 2 +- understand-anything-plugin/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b4a2751..3f018c3 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/.copilot-plugin/plugin.json b/.copilot-plugin/plugin.json index bad1eb7..7d55745 100644 --- a/.copilot-plugin/plugin.json +++ b/.copilot-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 2a73c7d..d6f6791 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "understand-anything", "displayName": "Understand Anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/understand-anything-plugin/.claude-plugin/plugin.json b/understand-anything-plugin/.claude-plugin/plugin.json index b4a2751..3f018c3 100644 --- a/understand-anything-plugin/.claude-plugin/plugin.json +++ b/understand-anything-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.7.7", + "version": "2.8.0", "author": { "name": "Egonex" }, diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index a47eaac..67b9633 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@understand-anything/skill", - "version": "2.7.7", + "version": "2.8.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", From e4301aea0cf93a1fb3d2bfbcf21e9a0afe8e7f4a Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 07:49:34 -0700 Subject: [PATCH 17/20] chore: trim process docs from docs/superpowers/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer feedback on #436 — these are personal brainstorm/plan artifacts produced via the superpowers skill flow, not repo documentation. The BUILD.md provenance note in packages/tree-sitter-dart-wasm/ stays since that's repo-level docs about the vendored wasm. --- .../plans/2026-06-13-dart-language-support.md | 1921 ----------------- .../specs/2026-06-13-dart-support-design.md | 357 --- 2 files changed, 2278 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-13-dart-language-support.md delete mode 100644 docs/superpowers/specs/2026-06-13-dart-support-design.md diff --git a/docs/superpowers/plans/2026-06-13-dart-language-support.md b/docs/superpowers/plans/2026-06-13-dart-language-support.md deleted file mode 100644 index 6fc64ef..0000000 --- a/docs/superpowers/plans/2026-06-13-dart-language-support.md +++ /dev/null @@ -1,1921 +0,0 @@ -# Dart Language Support Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Land deep Dart support in `@understand-anything/core` at parity with the recently merged Kotlin extractor (PR #347), producing structural graph + call-graph edges for `.dart` files. - -**Architecture:** Vendor a freshly-built `tree-sitter-dart.wasm` as a workspace-internal package (`@understand-anything/tree-sitter-dart-wasm`); register a `dartConfig` `LanguageConfig` referencing it; add a `DartExtractor` class implementing `LanguageExtractor`; cover with ~22 vitest cases driven by the real WASM grammar. No changes to shared schemas, registries, or the dashboard. - -**Tech Stack:** TypeScript 5 (strict), pnpm 10 workspaces, vitest 3, `web-tree-sitter@^0.26.6`, `tree-sitter-cli@0.26.x` (build-time only). - ---- - -## File structure - -| File | Responsibility | -|---|---| -| `understand-anything-plugin/pnpm-workspace.yaml` | Add `packages/tree-sitter-dart-wasm/*` so pnpm sees the new package | -| `.../packages/tree-sitter-dart-wasm/package.json` | Minimal package metadata — name, version, main pointing at the wasm | -| `.../packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm` | Vendored wasm binary (built from `tree-sitter-dart@1.0.0` grammar.js) | -| `.../packages/tree-sitter-dart-wasm/BUILD.md` | Provenance + rebuild instructions for future maintainers | -| `.../packages/core/package.json` | Add `@understand-anything/tree-sitter-dart-wasm: workspace:*` to dependencies | -| `.../packages/core/src/languages/configs/dart.ts` | Single `LanguageConfig` object for Dart | -| `.../packages/core/src/languages/configs/index.ts` | Import + register `dartConfig` | -| `.../packages/core/src/plugins/extractors/dart-extractor.ts` | `DartExtractor` class — structural + call-graph extraction | -| `.../packages/core/src/plugins/extractors/index.ts` | Import + register `DartExtractor` | -| `.../packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts` | ~22 vitest cases — real WASM parse + assertions | - ---- - -## Working directory & branch assumption - -All paths in this plan are relative to the repository root `/Users/thejesh/Git/Understand-Anything`. The implementation branch `feat/dart-language-support` already exists (the spec was committed to it in commits `2bb5233` and `c447b69`). - -Verify before starting: - -```bash -cd /Users/thejesh/Git/Understand-Anything -git status # clean -git branch --show-current # feat/dart-language-support -git log --oneline -3 # top: c447b69 docs: revise Dart spec... -``` - ---- - -## Task 1: Vendor the freshly-built tree-sitter-dart wasm - -**Why first:** Every downstream task depends on this wasm loading correctly via `require.resolve("@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm")`. Build + commit it before writing the config so dependent tasks can run their tests. - -**Files:** -- Create: `understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json` -- Create: `understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm` (binary, ~745 KB) -- Create: `understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md` -- Modify: `understand-anything-plugin/pnpm-workspace.yaml` - -- [ ] **Step 1: Inspect existing workspace config** - -Run: -```bash -cat understand-anything-plugin/pnpm-workspace.yaml -``` - -Expected output: -```yaml -packages: - - packages/* - - src -``` - -Confirm `packages/*` is already a glob — that means our new `packages/tree-sitter-dart-wasm/` will be picked up automatically and no edit is required. If the file does NOT use a glob, add a line ` - packages/tree-sitter-dart-wasm`. - -- [ ] **Step 2: Build the wasm from upstream grammar source** - -Prerequisites — install once if absent: -```bash -npm install -g tree-sitter-cli@latest -tree-sitter --version # expect: tree-sitter 0.26.x or newer -``` - -Build: -```bash -cd /tmp && rm -rf dart-build && mkdir dart-build && cd dart-build -npm pack tree-sitter-dart@1.0.0 # downloads the upstream tarball -tar xzf tree-sitter-dart-1.0.0.tgz -cd package -tree-sitter build --wasm # ~30 s; downloads wasi-sdk-29 on first run -ls -la tree-sitter-dart.wasm # expect: ~745 KB -head -c 30 tree-sitter-dart.wasm | xxd | head -1 -``` - -Expected last line: -``` -00000000: 0061 736d 0100 0000 0011 0864 796c 696e .asm.......dylin -``` - -(The `\0asm` magic followed by a custom section named `dylink.0` — the byte after `dylink` must be `2e 30`, NOT a length byte for an old-format `dylink` section.) - -If the byte after `dylink` is `c8 9b 2c` (the broken upstream wasm), the build did NOT regenerate — verify your `tree-sitter --version` is current. - -- [ ] **Step 3: Vendor the wasm into the workspace package** - -```bash -cd /Users/thejesh/Git/Understand-Anything -mkdir -p understand-anything-plugin/packages/tree-sitter-dart-wasm -cp /tmp/dart-build/package/tree-sitter-dart.wasm \ - understand-anything-plugin/packages/tree-sitter-dart-wasm/tree-sitter-dart.wasm -ls -la understand-anything-plugin/packages/tree-sitter-dart-wasm/ -``` - -Expected: the wasm file is present, ~745 KB. - -- [ ] **Step 4: Write the package metadata** - -Create `understand-anything-plugin/packages/tree-sitter-dart-wasm/package.json`: - -```json -{ - "name": "@understand-anything/tree-sitter-dart-wasm", - "version": "0.1.0", - "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" -} -``` - -- [ ] **Step 5: Write the BUILD provenance note** - -Create `understand-anything-plugin/packages/tree-sitter-dart-wasm/BUILD.md`: - -```markdown -# 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`. - -## 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. -``` - -- [ ] **Step 6: Run pnpm install to wire the workspace package** - -```bash -cd understand-anything-plugin -pnpm install 2>&1 | tail -5 -``` - -Expected: `Done in ` with no errors mentioning the new package. The package is now resolvable via `require.resolve("@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm")` from any workspace member that depends on it (which we'll wire in Task 3). - -- [ ] **Step 7: Commit** - -```bash -cd /Users/thejesh/Git/Understand-Anything -git add understand-anything-plugin/packages/tree-sitter-dart-wasm/ \ - understand-anything-plugin/pnpm-lock.yaml \ - understand-anything-plugin/pnpm-workspace.yaml -git commit -m "$(cat <<'EOF' -feat(tree-sitter-dart-wasm): vendor freshly-built dart WASM grammar - -The upstream tree-sitter-dart@1.0.0 ships a pre-`dylink.0` wasm that -fails to load in web-tree-sitter@0.26.x. The grammar source itself is -sound — rebuilding with the current tree-sitter-cli + wasi-sdk produces -a working dylink.0 wasm. Vendor that artifact as a workspace-internal -package so @understand-anything/core can depend on it via workspace:*. - -BUILD.md documents the provenance and rebuild instructions. -EOF -)" -``` - ---- - -## Task 2: Add the dartConfig LanguageConfig - -**Files:** -- Create: `understand-anything-plugin/packages/core/src/languages/configs/dart.ts` -- Modify: `understand-anything-plugin/packages/core/src/languages/configs/index.ts` -- Modify: `understand-anything-plugin/packages/core/package.json` (add workspace dep) - -- [ ] **Step 1: Add the workspace dependency to core** - -Edit `understand-anything-plugin/packages/core/package.json` — add **one line** inside the `"dependencies"` block, in alphabetical position (before `fuse.js`): - -```json -"dependencies": { - "@understand-anything/tree-sitter-dart-wasm": "workspace:*", - "@tree-sitter-grammars/tree-sitter-kotlin": "1.1.0", - ... -} -``` - -Note: pnpm's workspace protocol uses `workspace:*` — same as how core would reference any other internal package. - -Run: -```bash -cd understand-anything-plugin -pnpm install 2>&1 | tail -3 -``` - -Expected: clean install, no warnings mentioning the workspace package. - -- [ ] **Step 2: Create the dart.ts config file** - -Create `understand-anything-plugin/packages/core/src/languages/configs/dart.ts`: - -```ts -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; -``` - -- [ ] **Step 3: Register dartConfig in the configs index** - -Edit `understand-anything-plugin/packages/core/src/languages/configs/index.ts`. Three places to edit: - -(a) Add the import alongside the other code-language imports (alphabetical-ish, between `cppConfig` and `csharpConfig` is fine): - -```ts -import { dartConfig } from "./dart.js"; -``` - -(b) Add `dartConfig` to the `builtinLanguageConfigs` array, inside the "Code languages" block (place between `cppConfig` and `csharpConfig`): - -```ts - // Code languages - typescriptConfig, - javascriptConfig, - pythonConfig, - goConfig, - rustConfig, - javaConfig, - rubyConfig, - phpConfig, - swiftConfig, - kotlinConfig, - luaConfig, - cConfig, - cppConfig, - dartConfig, - csharpConfig, -``` - -(c) Add `dartConfig` to the named re-export block in the same position. - -- [ ] **Step 4: Build core to verify TypeScript compiles** - -```bash -cd understand-anything-plugin -pnpm --filter @understand-anything/core build 2>&1 | tail -5 -``` - -Expected: `Done` with no tsc errors. - -- [ ] **Step 5: Write a smoke test that the config is registered and the grammar loads** - -This test is a sanity check — it doesn't exercise the extractor (Task 3 onwards does that). Append it to the existing test file -`understand-anything-plugin/packages/core/src/languages/__tests__/language-registry.test.ts` (look at the existing tests there for style; if no test file exists, the build step's import of `dartConfig` is enough sanity for this task). - -Run: -```bash -pnpm --filter @understand-anything/core test 2>&1 | tail -10 -``` - -Expected: all existing tests still pass. No regressions. - -- [ ] **Step 6: Verify the wasm actually loads via the existing TreeSitterPlugin** - -Write a one-off Node script at `/tmp/verify-dart-wasm.mjs`: - -```js -import { createRequire } from "node:module"; -import * as ts from "web-tree-sitter"; - -const require = createRequire(import.meta.url); -await ts.Parser.init(); -const wasmPath = require.resolve( - "@understand-anything/tree-sitter-dart-wasm/tree-sitter-dart.wasm", -); -const Lang = await ts.Language.load(wasmPath); -const p = new ts.Parser(); -p.setLanguage(Lang); -const tree = p.parse("void main() { print('hi'); }"); -console.log("rootType:", tree.rootNode.type); -console.log("firstChild:", tree.rootNode.namedChild(0)?.type); -``` - -Run from inside core: -```bash -cd understand-anything-plugin/packages/core -cp /tmp/verify-dart-wasm.mjs ./verify-dart-wasm.mjs -node verify-dart-wasm.mjs -rm verify-dart-wasm.mjs -``` - -Expected output: -``` -rootType: program -firstChild: function_signature -``` - -If you instead see an `Error: ... at getDylinkMetadata`, the wasm is the wrong ABI — go back to Task 1, Step 2 and verify the build produced a `dylink.0` artifact. - -- [ ] **Step 7: Commit** - -```bash -git add understand-anything-plugin/packages/core/package.json \ - understand-anything-plugin/packages/core/src/languages/configs/dart.ts \ - understand-anything-plugin/packages/core/src/languages/configs/index.ts \ - understand-anything-plugin/pnpm-lock.yaml -git commit -m "$(cat <<'EOF' -feat(core): register dart LanguageConfig - -Adds the Dart language config and wires it into builtinLanguageConfigs -so .dart files are recognized by the language registry. References the -vendored @understand-anything/tree-sitter-dart-wasm package for grammar -loading. - -No extractor yet — structural extraction lands in the next commit. -EOF -)" -``` - ---- - -## Task 3: Scaffold DartExtractor + register it - -**Why before TDD steps:** Subsequent TDD tasks need an importable `DartExtractor` class to add tests against. This task creates the empty shell + registration; the next tasks fill in extraction logic test-first. - -**Files:** -- Create: `understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts` -- Modify: `understand-anything-plugin/packages/core/src/plugins/extractors/index.ts` - -- [ ] **Step 1: Create the skeleton DartExtractor** - -Create `understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts`: - -```ts -import type { StructuralAnalysis, CallGraphEntry } from "../../types.js"; -import type { LanguageExtractor, TreeSitterNode } from "./types.js"; -import { findChild, findChildren } 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("_"); -} - -/** - * Dart extractor for tree-sitter structural analysis + call graph. - * - * Approach (matching `KotlinExtractor` convention): mixin / extension / enum - * declarations are folded into `StructuralAnalysis.classes[]` because the - * shared schema does not have a first-class slot for them. Extension - * declarations without a name surface as `"on "` so they aren't - * silently dropped. - */ -export class DartExtractor implements LanguageExtractor { - readonly languageIds = ["dart"]; - - extractStructure(rootNode: TreeSitterNode): StructuralAnalysis { - const functions: StructuralAnalysis["functions"] = []; - const classes: StructuralAnalysis["classes"] = []; - const imports: StructuralAnalysis["imports"] = []; - const exports: StructuralAnalysis["exports"] = []; - - // Implementation lands in subsequent tasks. - void rootNode; - void findChild; - void findChildren; - void isExported; - - return { functions, classes, imports, exports }; - } - - extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { - // Implementation lands in a later task. - void rootNode; - return []; - } -} -``` - -- [ ] **Step 2: Register DartExtractor in the extractors index** - -Edit `understand-anything-plugin/packages/core/src/plugins/extractors/index.ts`. Three edits: - -(a) Add the named re-export beside the others: - -```ts -export { DartExtractor } from "./dart-extractor.js"; -``` - -(b) Add the import beside the others: - -```ts -import { DartExtractor } from "./dart-extractor.js"; -``` - -(c) Add `new DartExtractor()` to `builtinExtractors` (place between `CppExtractor` and `CSharpExtractor`): - -```ts -export const builtinExtractors: LanguageExtractor[] = [ - new TypeScriptExtractor(), - new PythonExtractor(), - new GoExtractor(), - new RustExtractor(), - new JavaExtractor(), - new RubyExtractor(), - new PhpExtractor(), - new CppExtractor(), - new DartExtractor(), - new CSharpExtractor(), - new KotlinExtractor(), -]; -``` - -- [ ] **Step 3: Build + test (must still pass)** - -```bash -pnpm --filter @understand-anything/core build 2>&1 | tail -3 -pnpm --filter @understand-anything/core test 2>&1 | tail -5 -``` - -Expected: tsc clean, all existing tests pass. (Skeleton extractor returns empty results, no behavior change for non-Dart files.) - -- [ ] **Step 4: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/index.ts -git commit -m "feat(core): scaffold DartExtractor + register in builtinExtractors - -Empty extractor that satisfies the LanguageExtractor interface so the -plugin pipeline can load it. Real extraction logic lands in subsequent -TDD commits. -" -``` - ---- - -## Task 4: TDD — top-level function extraction - -From here through Task 12 follow strict TDD: write failing test, verify it fails for the right reason, implement minimum to pass, verify pass, commit. Each task corresponds to a roughly-coherent slice of extractor behavior. - -**Reference test setup** — every test file in `__tests__/` uses the same `beforeAll` + `parse()` helper shape. Establish it once in Step 1, then re-use across Tasks 4–12. - -**Files (all of Tasks 4–12):** -- Create: `understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts` -- Modify: `understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts` - -- [ ] **Step 1: Create the test-file scaffold** - -Create `understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts`: - -```ts -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"]); - }); -}); -``` - -Run: -```bash -pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -10 -``` - -Expected: 1 test passes (the `languageIds` assertion), no errors. If `beforeAll` errors, the wasm path is wrong — fix before continuing. - -- [ ] **Step 2: Write the failing function-extraction tests** - -Append inside the `describe("DartExtractor", …)` block: - -```ts - describe("extractStructure - functions", () => { - it("extracts a simple top-level function with params and return type", () => { - const { tree, parser, root } = parse(`int add(int a, int b) => a + b;\n`); - const result = extractor.extractStructure(root); - - expect(result.functions).toHaveLength(1); - expect(result.functions[0].name).toBe("add"); - expect(result.functions[0].params).toEqual(["a", "b"]); - expect(result.functions[0].returnType).toBe("int"); - - tree.delete(); - parser.delete(); - }); - - it("extracts a function with no params and void return type", () => { - const { tree, parser, root } = parse(`void noop() {}\n`); - const result = extractor.extractStructure(root); - - expect(result.functions).toHaveLength(1); - expect(result.functions[0].name).toBe("noop"); - expect(result.functions[0].params).toEqual([]); - expect(result.functions[0].returnType).toBe("void"); - - tree.delete(); - parser.delete(); - }); - - it("extracts an async function with a generic return type", () => { - const { tree, parser, root } = parse(`Future fetch(String url) async { return ""; }\n`); - const result = extractor.extractStructure(root); - - expect(result.functions).toHaveLength(1); - expect(result.functions[0].name).toBe("fetch"); - expect(result.functions[0].params).toEqual(["url"]); - expect(result.functions[0].returnType).toBe("Future"); - - tree.delete(); - parser.delete(); - }); - }); -``` - -Run: -```bash -pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -10 -``` - -Expected: 3 new tests FAIL because the extractor returns empty `functions`. The `languageIds` test still passes. - -- [ ] **Step 3: Implement function extraction** - -In `dart-extractor.ts`, replace the `extractStructure` body. The AST shape (verified live): - -- A top-level function appears as `program > function_signature` followed by a **sibling** `function_body`. (Not parent/child — `function_body` is a separate top-level node.) -- `function_signature` children: an optional return-type node (`type_identifier` or `void_type` or a generic `type` subtree), then `identifier` (the name), then `formal_parameter_list`. - -Add this helper at the top of the file (after the existing `isExported` helper): - -```ts -/** - * Extract the identifier name from a function_signature / method_signature - * node. The name is the first `identifier` child after any return-type - * subtree. - */ -function extractFunctionName(sig: TreeSitterNode): string | null { - const id = findChild(sig, "identifier"); - return id ? id.text : null; -} - -/** - * Extract parameter names from a `formal_parameter_list`. Each - * `formal_parameter` child carries the parameter name as its `identifier` - * child; we ignore the type annotation. - */ -function extractParams(sig: TreeSitterNode): string[] { - const params: string[] = []; - const paramList = findChild(sig, "formal_parameter_list"); - if (!paramList) return params; - for (const p of findChildren(paramList, "formal_parameter")) { - const id = findChild(p, "identifier"); - if (id) params.push(id.text); - } - return params; -} - -/** - * Extract the return type from a function_signature. The return type is the - * first NAMED child whose type is NOT `identifier` or `formal_parameter_list` - * or `type_parameters`. If there is no such child, the function has no - * declared return type (Dart infers it). - * - * Common shapes seen during AST probing: - * `int add(int a, int b)` → type_identifier "int" - * `void noop()` → void_type - * `Future fetch()`→ type_identifier "Future" wrapped in a type with type_arguments - */ -function extractReturnType(sig: TreeSitterNode): string | undefined { - 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 name / params without seeing a return type. - return undefined; - } - // This is the return type node. Its full text (including generics) is - // what we want. - return child.text; - } - return undefined; -} -``` - -Now replace `extractStructure`: - -```ts - 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; - } - } - - 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 }); - } - } -``` - -The four `void X;` lines from the Task 3 skeleton inside `extractStructure` are gone now (replaced by the real body). Leave the `void rootNode;` line in `extractCallGraph` for now — it'll be replaced when Task 12 implements call-graph extraction. - -- [ ] **Step 4: Run the function tests and verify pass** - -```bash -pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -15 -``` - -Expected: all 4 tests (including `languageIds`) pass. If one fails, inspect actual vs expected — adjust the helper if the AST shape for that case differs from what was assumed. - -- [ ] **Step 5: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — top-level function extraction" -``` - ---- - -## Task 5: TDD — class extraction (plain, abstract, with inheritance) - -**Files:** -- Modify: `dart-extractor.ts`, `dart-extractor.test.ts` - -- [ ] **Step 1: Write the failing class tests** - -Append inside `describe("DartExtractor", …)`: - -```ts - 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"); - - tree.delete(); - parser.delete(); - }); - - it("extracts an empty class", () => { - const { tree, parser, root } = parse(`class Empty {}\n`); - const result = extractor.extractStructure(root); - - expect(result.classes).toHaveLength(1); - expect(result.classes[0].name).toBe("Empty"); - expect(result.classes[0].methods).toEqual([]); - - tree.delete(); - parser.delete(); - }); - - it("extracts an abstract class with method requirements", () => { - const { tree, parser, root } = parse(`abstract class Shape { - double area(); -} -`); - const result = extractor.extractStructure(root); - - expect(result.classes).toHaveLength(1); - expect(result.classes[0].name).toBe("Shape"); - expect(result.classes[0].methods).toContain("area"); - - tree.delete(); - parser.delete(); - }); - - it("extracts a class with extends + with + implements clauses", () => { - const { tree, parser, root } = parse(`class Square extends Shape with Comparable implements Cloneable { - double side; - Square(this.side); - double area() => side * side; -} -`); - const result = extractor.extractStructure(root); - - expect(result.classes).toHaveLength(1); - expect(result.classes[0].name).toBe("Square"); - expect(result.classes[0].methods).toContain("area"); - - tree.delete(); - parser.delete(); - }); - }); -``` - -Run tests — expect 4 new failures. - -- [ ] **Step 2: Implement class extraction** - -Class AST shape (verified live): - -- `program > class_definition { identifier(name), class_body }`. Inheritance clauses (`extends Foo`, `with Mixin`, `implements Iface`) appear as siblings between the `identifier` and `class_body`. We ignore them for now (out of scope for this task — captured at the class node's text level if ever needed; not required for the graph). -- `class_body > method_signature { function_signature { return_type, identifier, formal_parameter_list } }` followed by a sibling `function_body` (mirroring the top-level shape). -- `class_body > declaration { type_identifier, initialized_identifier_list { initialized_identifier { identifier(name) } } }` — this is a field declaration. - -Add a helper for class-body walking (after the function helpers): - -```ts -/** - * 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. - */ -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") { - const inner = findChild(member, "function_signature"); - if (!inner) continue; - const name = extractFunctionName(inner); - if (!name) continue; - methods.push(name); - functions.push({ - name, - lineRange: [member.startPosition.row + 1, member.endPosition.row + 1], - params: extractParams(inner), - returnType: extractReturnType(inner), - }); - if (isExported(name)) { - exports.push({ name, lineNumber: member.startPosition.row + 1 }); - } - } else if (member.type === "declaration") { - // Field declaration — surface initialized_identifier names as properties. - 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); - } - } - } -} -``` - -Add a case to the top-level switch: - -```ts - case "class_definition": - this.extractClassDefinition(node, classes, functions, exports); - break; -``` - -Add the private method: - -```ts - private extractClassDefinition( - declNode: TreeSitterNode, - classes: StructuralAnalysis["classes"], - functions: StructuralAnalysis["functions"], - exports: StructuralAnalysis["exports"], - ): void { - const nameNode = findChild(declNode, "identifier"); - if (!nameNode) return; - const name = nameNode.text; - - const methods: string[] = []; - const properties: string[] = []; - - const body = findChild(declNode, "class_body"); - 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 }); - } - } -``` - -Run tests — expect all class tests to pass. - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — class extraction with fields + methods" -``` - ---- - -## Task 6: TDD — constructors (default, named, factory) - -**Files:** dart-extractor.{ts,test.ts} - -- [ ] **Step 1: Write failing tests** - -Append: - -```ts - 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(); - }); - }); -``` - -Run — expect 3 failures. - -- [ ] **Step 2: Implement constructor handling** - -AST shapes (verified live): - -- Unnamed: `class_body > declaration > constructor_signature { identifier(class), formal_parameter_list }` — only ONE identifier. -- Named: `class_body > declaration > constructor_signature { identifier(class), identifier(named), formal_parameter_list }` — TWO identifiers; second is the named-constructor name. -- Factory: `class_body > method_signature > factory_constructor_signature { identifier(class), identifier(named), formal_parameter_list }` — wrapped in `method_signature`. - -Extend `collectClassBody`'s `for` loop. Both the `method_signature` branch and the `declaration` branch get a constructor check **at the top** that short-circuits before the existing logic. Full revised loop body: - -```ts - if (member.type === "method_signature") { - // Factory constructor lives inside method_signature as - // factory_constructor_signature; check that first. - const factory = findChild(member, "factory_constructor_signature"); - if (factory) { - const name = constructorName(factory); - if (name) { - methods.push(name); - functions.push({ - name, - lineRange: [member.startPosition.row + 1, member.endPosition.row + 1], - params: extractParams(factory), - returnType: undefined, - }); - if (isExported(name)) { - exports.push({ name, lineNumber: member.startPosition.row + 1 }); - } - } - continue; - } - // ...existing function_signature handling unchanged... - } else if (member.type === "declaration") { - const ctor = findChild(member, "constructor_signature"); - if (ctor) { - const name = constructorName(ctor); - if (name) { - methods.push(name); - functions.push({ - name, - lineRange: [member.startPosition.row + 1, member.endPosition.row + 1], - params: extractParams(ctor), - returnType: undefined, - }); - if (isExported(name)) { - exports.push({ name, lineNumber: member.startPosition.row + 1 }); - } - } - continue; - } - // ...existing field-declaration handling unchanged... - } -``` - -Add the helper `constructorName` at the top: - -```ts -/** - * Build a constructor's method-graph name from a constructor_signature / - * factory_constructor_signature node: - * - one identifier → unnamed constructor, name = "" - * - two identifiers → named constructor, name = "." - */ -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}`; -} -``` - -Run tests — expect all constructor tests pass; previously-passing tests remain green. - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — constructor naming (default/named/factory)" -``` - ---- - -## Task 7: TDD — mixins - -**Files:** dart-extractor.{ts,test.ts} - -- [ ] **Step 1: Write failing tests** - -```ts - 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(); - }); - }); -``` - -Run — expect 2 failures. - -- [ ] **Step 2: Implement mixin extraction** - -AST shape: `program > mixin_declaration { identifier(name), [type_identifier(on)], class_body }`. Same body shape as `class_definition`. - -Add case to the top-level switch: - -```ts - case "mixin_declaration": - this.extractMixinDeclaration(node, classes, functions, exports); - break; -``` - -Implement (almost identical to `extractClassDefinition`; refactoring opportunity but kept separate for clarity): - -```ts - private extractMixinDeclaration( - declNode: TreeSitterNode, - classes: StructuralAnalysis["classes"], - functions: StructuralAnalysis["functions"], - exports: StructuralAnalysis["exports"], - ): void { - const nameNode = findChild(declNode, "identifier"); - if (!nameNode) return; - const name = nameNode.text; - - const methods: string[] = []; - const properties: string[] = []; - - const body = findChild(declNode, "class_body"); - 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 }); - } - } -``` - -Run tests — expect mixin tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — mixin declarations" -``` - ---- - -## Task 8: TDD — extensions - -**Files:** dart-extractor.{ts,test.ts} - -- [ ] **Step 1: Write failing tests** - -```ts - 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(); - }); - }); -``` - -Run — expect 2 failures. - -- [ ] **Step 2: Implement extension extraction** - -AST shape (verified): - -- Named: `extension_declaration { identifier(name), type_identifier(on-type), extension_body }` -- Anonymous: `extension_declaration { type_identifier(on-type), extension_body }` — no leading identifier. - -Add the case to the top-level switch: - -```ts - case "extension_declaration": - this.extractExtensionDeclaration(node, classes, functions, exports); - break; -``` - -Implement: - -```ts - private extractExtensionDeclaration( - declNode: TreeSitterNode, - classes: StructuralAnalysis["classes"], - functions: StructuralAnalysis["functions"], - exports: StructuralAnalysis["exports"], - ): void { - // Try named extension first: leading `identifier` child is the name. - const idNode = findChild(declNode, "identifier"); - let name: string; - if (idNode) { - name = idNode.text; - } else { - // Anonymous: name the entry after the target type so the graph builder - // doesn't drop it. The on-type is the first `type_identifier`. - const onType = findChild(declNode, "type_identifier"); - if (!onType) return; - name = `on ${onType.text}`; - } - - const methods: string[] = []; - const properties: string[] = []; - - const body = findChild(declNode, "extension_body"); - 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 }); - } - } -``` - -Run — expect extension tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — extension declarations (named + anonymous)" -``` - ---- - -## Task 9: TDD — enums - -**Files:** dart-extractor.{ts,test.ts} - -- [ ] **Step 1: Write failing tests** - -```ts - 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(); - }); - }); -``` - -Run — expect 1 failure. - -- [ ] **Step 2: Implement enum extraction** - -AST shape: `enum_declaration { identifier(name), enum_body { enum_constant { identifier }... } }`. - -Add case to the top-level switch: - -```ts - case "enum_declaration": - this.extractEnumDeclaration(node, classes, exports); - break; -``` - -Implement: - -```ts - 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 }); - } - } -``` - -Run — expect enum test passes. - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — enum declarations" -``` - ---- - -## Task 10: TDD — import + export directives - -**Files:** dart-extractor.{ts,test.ts} - -- [ ] **Step 1: Write failing tests** - -```ts - 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(); - }); - }); - - 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(); - }); - }); -``` - -Run — expect 5 new failures. - -- [ ] **Step 2: Implement import/export extraction** - -AST shape (verified): - -- Top-level wrapper: `import_or_export { library_import | library_export }`. -- `library_import { import_specification { configurable_uri { uri { string_literal }, [combinator { 'show', identifier, ... }], [identifier(as-prefix)] } } }`. - - The `string_literal` text contains the surrounding quotes (`'foo.dart'`). - - A `combinator` named child holds `show`/`hide` keyword + identifier list. - - An `identifier` named child at the import_specification level is the `as` prefix. -- `library_export { configurable_uri { uri { string_literal } } }`. - -Add a helper at top of file: - -```ts -/** - * Unwrap the string-literal text from `uri > string_literal`, stripping the - * surrounding single or double quotes. - */ -function uriText(uriNode: TreeSitterNode): string | null { - const lit = findChild(uriNode, "string_literal"); - if (!lit) return null; - return lit.text.replace(/^['"]|['"]$/g, ""); -} -``` - -Add cases to the top-level switch: - -```ts - case "import_or_export": - this.extractImportOrExport(node, imports, exports); - break; -``` - -Implement: - -```ts - 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[] = []; - - // `show Bar, Baz` — combinator has identifier children for the shown names. - const combinators = findChildren(spec, "combinator"); - for (const c of combinators) { - for (const id of findChildren(c, "identifier")) { - specifiers.push(id.text); - } - } - - // `as Foo` — a direct `identifier` child of import_specification is the - // alias. Has to come AFTER the configurable_uri in source order. - const asId = findChild(spec, "identifier"); - if (asId && specifiers.length === 0) { - specifiers.push(asId.text); - } - - imports.push({ - source, - specifiers, - lineNumber: libImport.startPosition.row + 1, - }); - } - - 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, - }); - } -``` - -Run — expect import + export tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — import directives (package/relative/show/as) + export directives" -``` - ---- - -## Task 11: TDD — visibility (underscore-prefix rule) - -**Files:** dart-extractor.{ts,test.ts} - -- [ ] **Step 1: Write failing tests** - -```ts - 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(); - }); - }); -``` - -Run — first two tests should already pass (current implementation already uses `isExported(name)` everywhere). The class-member test should also pass thanks to `collectClassBody` calling `isExported`. If all three pass without code changes, this task is just a coverage commit. - -- [ ] **Step 2: Confirm all 3 pass; if any fail, add a missing `isExported` guard at the relevant emit site** - -```bash -pnpm --filter @understand-anything/core test src/plugins/extractors/__tests__/dart-extractor.test.ts 2>&1 | tail -10 -``` - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "test(core): DartExtractor — visibility rule (underscore prefix)" -``` - ---- - -## Task 12: TDD — call graph - -**Files:** dart-extractor.{ts,test.ts} - -- [ ] **Step 1: Write failing tests** - -```ts - 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(); - }); - }); -``` - -Run — expect 2 failures (third passes because the stub returns `[]`). - -- [ ] **Step 2: Implement call-graph extraction** - -AST shape for a Dart call (verified live): - -``` -function_body - block - expression_statement - identifier "print" ← bare-call callee - selector - argument_part -``` - -And for a method-style call: - -``` -expression_statement - string_literal "'hi'" ← receiver - selector - unconditional_assignable_selector - identifier "toUpperCase" ← method-call callee (last identifier in the selector chain) - selector - argument_part -``` - -Key insight: in Dart's grammar, a call is represented as a target expression followed by one or more `selector` siblings, with the LAST `selector` containing an `argument_part`. The callee identifier is either: - -- The first identifier in the expression_statement (bare call), OR -- The last identifier appearing inside any `unconditional_assignable_selector` before the `selector` that contains `argument_part`. - -Pragmatic approach: walk every node, and whenever we see a `selector` containing an `argument_part`, look for the callee as the IDENTIFIER token immediately preceding it in the parent's children. If none, look inside the previous sibling `selector` for an `identifier` (the method name in chained call). - -Replace `extractCallGraph`: - -```ts - extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] { - const entries: CallGraphEntry[] = []; - const functionStack: string[] = []; - - const walk = (node: TreeSitterNode) => { - let pushed = false; - - // Push function_signature names (both top-level and inside method_signature). - if (node.type === "function_signature") { - const name = extractFunctionName(node); - if (name) { - functionStack.push(name); - pushed = true; - } - } - - // Detect a call: any `selector` node containing an `argument_part`. - if ( - node.type === "selector" && - findChild(node, "argument_part") && - functionStack.length > 0 - ) { - const callee = this.extractCalleeName(node); - if (callee) { - entries.push({ - caller: functionStack[functionStack.length - 1], - callee, - lineNumber: node.startPosition.row + 1, - }); - } - } - - for (let i = 0; i < node.childCount; i++) { - const child = node.child(i); - if (child) walk(child); - } - - if (pushed) functionStack.pop(); - }; - - walk(rootNode); - return entries; - } - - /** - * Find the callee name for a `selector` node that contains an - * `argument_part`. We look at the parent's children: the callee identifier - * is either the immediately-preceding `identifier` sibling (bare call) or - * the last `identifier` inside the immediately-preceding `selector` - * sibling's `unconditional_assignable_selector` (method call). - */ - private extractCalleeName(callSelector: TreeSitterNode): string | null { - const parent = callSelector.parent; - if (!parent) return null; - - // Find this selector's index in the parent. - let myIdx = -1; - for (let i = 0; i < parent.childCount; i++) { - if (parent.child(i) === callSelector) { - 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; - } -``` - -Run — expect call-graph tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts \ - understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts -git commit -m "feat(core): DartExtractor — call graph extraction" -``` - ---- - -## Task 13: Final verification + lint + push - -**Files:** none — verification only. - -- [ ] **Step 1: Run the full core test suite** - -```bash -cd /Users/thejesh/Git/Understand-Anything -pnpm --filter @understand-anything/core test 2>&1 | tail -20 -``` - -Expected: All existing tests pass AND the new Dart tests pass. Look for the summary line — should show counts like `Tests passed ( files)`. If any pre-existing test failed, investigate before continuing. - -- [ ] **Step 2: Run the skill build (must not regress)** - -```bash -pnpm --filter @understand-anything/skill build 2>&1 | tail -5 -``` - -Expected: tsc clean. - -- [ ] **Step 3: Run lint across the project** - -```bash -pnpm lint 2>&1 | tail -10 -``` - -Expected: clean (or only pre-existing warnings unrelated to our changes). Fix any errors introduced by our changes inline; do NOT commit lint warnings. - -- [ ] **Step 4: Run the full test suite** - -```bash -pnpm test 2>&1 | tail -10 -``` - -Expected: full repo suite passes, no regressions. - -- [ ] **Step 5: Manual smoke — verify integration with the real TreeSitterPlugin** - -Write a one-off Node script at `/tmp/smoke-dart.mjs`: - -```js -import { TreeSitterPlugin } from "@understand-anything/core"; -import { dartConfig } from "@understand-anything/core/languages"; - -const plugin = new TreeSitterPlugin([dartConfig]); -await plugin.init(); - -const dart = ` -import 'package:flutter/material.dart'; - -class Counter { - int count = 0; - void increment() => count++; -} - -void main() { - Counter().increment(); -} -`; - -const result = plugin.analyzeFile("example.dart", dart); -console.log(JSON.stringify(result, null, 2)); -``` - -Run from the core package: - -```bash -cd understand-anything-plugin/packages/core -cp /tmp/smoke-dart.mjs ./smoke-dart.mjs -node smoke-dart.mjs -rm smoke-dart.mjs -``` - -Expected output: a `StructuralAnalysis` JSON with non-empty `functions` (containing `main`, `increment`), `classes` (containing `Counter`), `imports` (containing `package:flutter/material.dart`), `exports` (containing `Counter`, `main`). - -If the imports/exports are subtly different from what the unit tests assert (e.g., empty `specifiers`), that's fine — the integration test just confirms the plugin loads and produces non-empty results. - -- [ ] **Step 6: Push the branch** - -```bash -git push -u origin feat/dart-language-support -``` - -Expected: branch lands on remote. PR creation is a separate user step — do NOT open a PR autonomously. - ---- - -## Coverage map (spec → tasks) - -| Spec section | Task(s) | -|---|---| -| File-level changes — tree-sitter-dart-wasm package + workspace wiring | Task 1 | -| File-level changes — core dependency + dartConfig + index | Task 2 | -| File-level changes — DartExtractor + index | Task 3 | -| File-level changes — dart-extractor.test.ts | Tasks 4–12 | -| `dartConfig` shape | Task 2, Step 2 | -| WASM grammar source — vendored | Task 1 | -| DartExtractor — top-level AST nodes handled (functions) | Task 4 | -| DartExtractor — classes + constructors | Tasks 5, 6 | -| DartExtractor — mixins | Task 7 | -| DartExtractor — extensions (named + anonymous) | Task 8 | -| DartExtractor — enums | Task 9 | -| DartExtractor — imports (three forms) | Task 10 | -| Top-level `export` directive | Task 10 | -| Visibility rule (underscore prefix) | Task 11 | -| Class body walking convention (methods → functions[]) | Task 5, Step 2 | -| Call graph (bare + method calls) | Task 12 | -| Error handling (inherited from existing pipeline; no new modes) | covered implicitly; verified by Task 13 smoke | -| Verification commands | Task 13 | -| Edge cases NOT handled (records, patterns, `part of`) | not implemented, by design — spec rationale stands | - -## Self-review notes (already applied) - -- All step code blocks include the EXACT code to write; no "fill in similar code" cross-references. -- Method signatures used across tasks (`extractFunctionName`, `extractParams`, `extractReturnType`, `collectClassBody`, `constructorName`, `uriText`, `isExported`) are consistent everywhere they appear. -- The `extractCallGraph` implementation in Task 12 uses the same `extractFunctionName` helper introduced in Task 4 — no name drift. -- AST shapes for every walked node have been verified live against the freshly-built wasm; the plan is not speculating about grammar structure. -- Each task ends with a commit step so progress is incremental and the branch always builds. -- Task 11 may pass without code changes if Tasks 4–10 wired `isExported` correctly throughout; this is an intentional "coverage commit" tied to the spec's call-out that visibility is the one thing reviewers will trip on. diff --git a/docs/superpowers/specs/2026-06-13-dart-support-design.md b/docs/superpowers/specs/2026-06-13-dart-support-design.md deleted file mode 100644 index 2119288..0000000 --- a/docs/superpowers/specs/2026-06-13-dart-support-design.md +++ /dev/null @@ -1,357 +0,0 @@ -# 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/` and `framework-registry.ts`. -- **No schema extensions.** Mixins, extensions, and enums are folded into the - existing `StructuralAnalysis.classes[]` bucket. Adding `mixins[]` / `extensions[]` - as first-class fields would require coordinated changes to `types.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` / `part` multi-file libraries.** Each `.dart` file - is analyzed independently; cross-`part` relationships 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 `.dart` files — 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 - -```ts -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 `.dart` extension; Flutter widgets share it. -- `entryPoints` covers both Flutter (`lib/main.dart`) and Dart CLI (`bin/*.dart`). -- `barrels` matches Dart's idiomatic top-level re-export files (`lib/foo.dart` - re-exporting `lib/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_signature` inside `class_body` → push name to - the class's `methods[]` AND append a full entry to top-level `functions[]` - (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"`. - -### 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 as `assignable_expression > selector > - arguments`. The callee identifier is the named child immediately preceding - the `arguments` node. Two shapes: - - Bare call `foo(...)` → callee is the `identifier` child. - - Method call `target.foo(...)` → callee is the last `identifier` in the - `selector` chain (analogous to Kotlin's `navigation_expression` handling). - -### 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 fetch(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 a `console.debug` "skipping structural analysis" - message; `.dart` files fall back to LLM-only analysis. Same path Swift uses - today (Swift has a `LanguageConfig` but no `treeSitter` field, so the loader - silently skips it). -- **Parse failure on a malformed `.dart` file**: tree-sitter returns a partial - tree; the extractor walks what's present and returns whatever it found. - Matches `KotlinExtractor` behavior. -- **Empty / `library` / `part` only files**: extractor returns - `{ functions: [], classes: [], imports: [], exports: [] }`. Not an error. - -## Edge cases handled in code - -- **Anonymous extension** (`extension on Foo`): the class entry's `name` is - 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 like `Foo.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 _PrivateImpl` is not in `exports[]`. A `class Public` with a method - `_helper()` has the class itself in `exports[]` but `_helper` is excluded. - Non-underscore class members ARE pushed to `exports[]` alongside the class - entry, matching `KotlinExtractor.collectClassBody`'s behavior of pushing - exported members to the top-level `exports[]` 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` / `part` multi-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.0` wasm uses the - pre-`dylink.0` format and fails to load in `web-tree-sitter@0.26.x`. A - fresh build with the current `tree-sitter-cli@0.26.x` + `wasi-sdk-29` - produces a `dylink.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-line `isExported` helper - with an explanatory comment. From 68777feb1d9b1bf7017819265ec9153fbe3f8251 Mon Sep 17 00:00:00 2001 From: thejesh Date: Sat, 13 Jun 2026 07:49:48 -0700 Subject: [PATCH 18/20] =?UTF-8?q?feat(core):=20DartExtractor=20=E2=80=94?= =?UTF-8?q?=20broaden=20coverage=20per=20review=20(#436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Incorporates stronger pieces from the prior Dart attempts (#348, #415) that @Lum1104 called out: - `extractParams` now walks `optional_formal_parameters` (covers both optional positional `[...]` AND named `{...}` parameters — the Dart grammar uses one wrapper for both). - New `extractParamName` helper extracts the user-visible field name from `this.field` and `super.field` initializer parameters by unwrapping `constructor_param` / `super_formal_parameter`. - `collectClassBody` now routes `getter_signature` and `setter_signature` in both shapes: - concrete: `method_signature > getter_signature` + sibling function_body - abstract: `declaration > getter_signature` Setters use the same path. The previous limitation assertion (`methods).not.toContain("value")`) flipped to a positive `.toContain("value")`. - Added import/export edge-case tests: `dart:` SDK URIs, multi-import declaration-order preservation, and `export ... show` clauses. - Added a comma-list field test (`int a, b, c;`). Underscore-prefix visibility carries through naturally to all new code paths via the existing `isExported` gate inside `pushMethod`; explicit test added for an underscore-prefixed getter. Test counts: 28 → 41 dart cases; full core suite 720 → 733; no regressions. --- .../__tests__/dart-extractor.test.ts | 168 +++++++++++++++++- .../src/plugins/extractors/dart-extractor.ts | 107 +++++++++-- 2 files changed, 256 insertions(+), 19 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 5471bd9..4d4a9fc 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -75,6 +75,51 @@ describe("DartExtractor", () => { }); }); + describe("extractStructure - parameter kinds", () => { + it("surfaces optional positional parameters", () => { + const { tree, parser, root } = parse(`void show([String? title, int count = 0]) {}\n`); + const result = extractor.extractStructure(root); + + expect(result.functions[0].params).toEqual(["title", "count"]); + tree.delete(); + parser.delete(); + }); + + it("surfaces named parameters wrapped in {...}", () => { + const { tree, parser, root } = parse(`void show({String? title, int count = 0}) {}\n`); + const result = extractor.extractStructure(root); + + expect(result.functions[0].params).toEqual(["title", "count"]); + tree.delete(); + parser.delete(); + }); + + it("mixes required and named parameters in one signature", () => { + const { tree, parser, root } = parse(`String join(List items, {String sep = ","}) => "";\n`); + const result = extractor.extractStructure(root); + + expect(result.functions[0].params).toEqual(["items", "sep"]); + tree.delete(); + parser.delete(); + }); + + it("extracts `this.field` constructor parameters as the field name", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + String y; + Foo(this.x, this.y); +} +`); + const result = extractor.extractStructure(root); + + const ctor = result.functions.find((f) => f.name === "Foo"); + expect(ctor).toBeDefined(); + expect(ctor!.params).toEqual(["x", "y"]); + tree.delete(); + parser.delete(); + }); + }); + describe("extractStructure - classes", () => { it("extracts a class with fields and methods", () => { const { tree, parser, root } = parse(`class Counter { @@ -96,9 +141,8 @@ describe("DartExtractor", () => { expect(result.classes[0].properties).toEqual( expect.arrayContaining(["count", "label"]), ); - // Getters appear as `method_signature > getter_signature`, a separate node - // type from `function_signature` — not yet surfaced (documented limitation). - expect(result.classes[0].methods).not.toContain("value"); + // Getters are surfaced as methods (`int get value` → "value"). + expect(result.classes[0].methods).toContain("value"); tree.delete(); parser.delete(); @@ -147,6 +191,85 @@ describe("DartExtractor", () => { 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", () => { @@ -314,6 +437,33 @@ describe("DartExtractor", () => { 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", () => { @@ -326,6 +476,18 @@ describe("DartExtractor", () => { 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", () => { diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index 8f3d10f..ebe666f 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -29,24 +29,68 @@ function extractFunctionName(sig: TreeSitterNode): string | null { } /** - * Extract parameter names from a `formal_parameter_list`. Each - * `formal_parameter` child carries the parameter name as its `identifier` - * child; we ignore the type annotation. + * Extract the user-visible name from a `formal_parameter` (or one of its + * specialized children). * - * Currently only required positional parameters (`formal_parameter` direct - * children) are surfaced. Dart's optional positional (`[...]`) and named - * (`{...}`) parameters are wrapped in `optional_formal_parameters` and - * `named_parameter_list` container nodes respectively; supporting those is - * left for a follow-up — the project-graph use case does not currently - * distinguish parameter kinds. + * 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 paramList = findChild(sig, "formal_parameter_list"); - if (!paramList) return params; - for (const p of findChildren(paramList, "formal_parameter")) { - const id = findChild(p, "identifier"); - if (id) params.push(id.text); + 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; } @@ -178,9 +222,24 @@ function collectClassBody( } 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`. - // NOTE: `getter_signature` also nests under `method_signature` but is a - // separate node type — getters are not yet surfaced (documented limitation). const inner = findChild(member, "function_signature"); if (!inner) continue; const name = extractFunctionName(inner); @@ -196,6 +255,20 @@ function collectClassBody( } 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"); @@ -207,6 +280,8 @@ function collectClassBody( 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")) { From f2d6b997c33b1ff68b672c482cec7fd05455a417 Mon Sep 17 00:00:00 2001 From: thejesh Date: Mon, 15 Jun 2026 02:49:55 -0700 Subject: [PATCH 19/20] chore: sync root pnpm-lock.yaml with new workspace dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI runs `pnpm install` from the repo root using the root lockfile with the default frozen-lockfile behaviour. The previous commits ran install only inside understand-anything-plugin/ so the new @understand-anything/tree-sitter-dart-wasm workspace dependency was recorded in understand-anything-plugin/pnpm-lock.yaml but missed at the repo root — CI would have stopped at install. Flagged by @chatgpt-codex-connector on #435. --- pnpm-lock.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b9b06e..37350c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@tree-sitter-grammars/tree-sitter-kotlin': specifier: 1.1.0 version: 1.1.0 + '@understand-anything/tree-sitter-dart-wasm': + specifier: workspace:* + version: link:../tree-sitter-dart-wasm fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -198,6 +201,8 @@ importers: specifier: ^3.1.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + understand-anything-plugin/packages/tree-sitter-dart-wasm: {} + packages: '@ampproject/remapping@2.3.0': From a555f4dd2a9c436b336a10f96c1453e431ec681d Mon Sep 17 00:00:00 2001 From: thejesh Date: Mon, 15 Jun 2026 02:50:06 -0700 Subject: [PATCH 20/20] =?UTF-8?q?fix(core):=20DartExtractor=20=E2=80=94=20?= =?UTF-8?q?call=20graph=20coverage=20for=20codex=20P2=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the call-graph walker, both flagged by codex on #435: 1. `const Foo(...)` / `new Foo(...)` constructor calls were silently dropped. The grammar emits these as `const_object_expression` / `new_expression` containing `arguments` directly — they bypass the `selector > argument_part` shape the walker relied on. Added a dedicated branch that records the inner `type_identifier` as the callee. Critical for Flutter widget trees where `runApp(const MyApp())` would otherwise lose the MyApp construction edge. 2. When a getter / setter / constructor / factory_constructor has a body, its `method_signature` wraps `getter_signature` / `setter_signature` / `constructor_signature` / `factory_constructor_signature` instead of `function_signature`. The walker only looked for `function_signature`, so `pendingName` stayed null and the sibling `function_body` was walked with an empty stack — calls inside ctor/factory/getter/setter bodies were silently dropped even though those members were already extracted as functions. Now dispatch across all five signature variants, using `constructorName` for the (factory) constructor pair to match what `collectClassBody` pushes. Tests: 41 → 47 dart cases (+6); full core 733 → 739; no regressions. --- .../__tests__/dart-extractor.test.ts | 103 ++++++++++++++++++ .../src/plugins/extractors/dart-extractor.ts | 49 ++++++++- 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 4d4a9fc..dd69c9b 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -526,6 +526,109 @@ int caller() { 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", () => { diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index ebe666f..5605e40 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -587,6 +587,29 @@ export class DartExtractor implements LanguageExtractor { }); } } + + // 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); }; @@ -606,9 +629,29 @@ export class DartExtractor implements LanguageExtractor { // Recurse into signature (no calls expected, but stay complete). walkSiblings(child); } else if (child.type === "method_signature") { - // method_signature wraps function_signature; sibling function_body follows. - const inner = findChild(child, "function_signature"); - if (inner) pendingName = extractFunctionName(inner); + // 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.