Files
codegraph/docs/design/dart-kernel-port-checklist.md
T
d1b75a1a27 feat(kernel): R7b Dart walker — dart module, vendored-grammar-C d4d8f3e + wasm byte-copy vendor, dart default-routed (#1386)
R7b batch 4 #4 — the FINAL R7b language (docs/design/dart-kernel-port-checklist.md
is the authoritative quirk list). The fourth vendored-grammar-C language,
with a twist: production dart resolved its wasm from tree-sitter-wasms,
whose dart dependency is an UNPINNED github:UserNobody14/tree-sitter-dart —
a routine dependency update would have silently changed dart's grammar.
This PR byte-copies the shipping 0.1.13 artifact into src/extraction/wasm/
(VENDORED_WASM_LANGS += dart) and compiles the same-commit (d4d8f3e337d8)
parser.c/scanner.c in the kernel — table identity proven by the
kernel-grammar-parity row. crates.io tree-sitter-dart is the nielsenko
fork (different lineage) — rejected.

The center of gravity is THE SIBLING-BODY DOUBLE-WALK, reproduced
bug-for-bug: dart attaches every function/method body as a NEXT SIBLING of
its signature, and the TS walkers consume each body TWICE — once via
resolveBody (attributed to the function/method) and once via the enclosing
generic walk (attributed to the file/class). Duplicate local-function
nodes with the SAME id under different parents, duplicated
calls/instantiates refs, and file/class-attributed fn-ref twins all emit
in the exact observed interleave (a dedicated fixture pins the
duplicate-id rows; the bloc kind-census spot-check pins the counts).

Also preserved (probe-pinned): the extractBareCall selector matrix (the
first callTypes=[] language — cascades completely invisible, `?.` encodes
like `.`, the `ConfigT.load()` calls+references double emission with no
callee-of-call skip, capitalized-chain `Foo.create().run` re-encode,
const-object callee names); the constructor hooks (unnamed ctor skipped,
named ctors/factories renamed to the CTOR name with the class as
returnType, `@override (T) m()` record-misparse rescued by class-name
validation); operator methods minting `method "<anonymous>"`;
static_final_declaration constants via the visitNode hook while instance
fields mint NOTHING; the prefixed-return-type prefix bug (`other.OtherClass
f()` → returnType `other`); enum `with` mixins silent vs `implements`
working; anonymous extensions named after the ON type; deferred imports
invisible; named-argument callbacks NOT fn-ref-captured (the Flutter
`onPressed:` idiom — future accuracy PR, TS-side first); `async*`/`sync*`
NOT async; value-refs with the LIVE dart sibling-body pull and the
`$X`-vs-`${X}` interpolation asymmetry; dartdoc kept in all three comment
forms with the annotation-broken chain.

Gates: parity sweeps first-run 0-diff on shelf/bloc/flutter — 5,815 clean
files byte-parity, deferrals 10/21/1341 ≈ the survey's 10/21/~1340
(both-arm grammar reality: empty object patterns — the sealed-class
idiom — and unnamed `library;` dominate; --max-deferral 0.3); full-init
dumps byte-identical ×3 (shelf 7,959 / bloc 40,026 / flutter 1,855,319
dump lines); bloc per-kind node census identical across arms (the
double-walk duplicate rows survive the store identically);
kernel-dart-parity suite (7 fixtures + in-memory CRLF variants +
double-walk duplicate-id pin + generated-file skip pin + two defer pins);
full suite 2,688 green ×2 with CODEGRAPH_KERNEL_EXPECT=1.
DEFAULT_ROUTED += dart (20 langs — R7b COMPLETE).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:55:48 -05:00

1083 lines
72 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dart kernel port (R7b batch 4) — the bug-for-bug checklist
**Status: SURVEY COMPLETE (2026-07-20), port not started.** Survey basis: every
TS-side branch a `.dart` file exercises, with file:line anchors as of
**`45a53eb`** (HEAD at survey time, clean main). Every extraction-behavior
claim below was **pinned against the real `dist/` extractor** (the
`extract-*.txt` ground-truth dumps in the session scratchpad `svy-dart/`
§Probe artifacts), and every grammar-shape claim was **probed against the
production wasm** (tree-sitter-wasms 0.1.13's dart build) via full CST dumps —
not inferred from reading code. Field-presence claims come from
`fieldNameForChild` dumps corroborated by extractor behavior (the swift
lesson: dump labels alone lie — here every field claim is backed by an
extractor-output pin). Read WITH `docs/design/rust-kernel-migration-plan.md`
(§0a recipe, §2 boundary, §4 tracker, §5 gates) and the format precedents
(`kotlin-kernel-port-checklist.md` — the closest structural sibling:
visitNode-hook constants, vendored-grammar-C build; `swift-kernel-port-checklist.md`
— the shared-branch liveness style and high-deferral posture).
**Blocking findings: none — but one mechanism is NOVEL and load-bearing, and
one number is big.** (1) **The sibling-body double-walk (§The double-walk) is
the center of gravity of this port.** Dart's grammar attaches every
function/method BODY as a *next sibling* of its signature node, and the TS
walkers consume the body TWICE — once via `resolveBody` (attributed to the
function/method) and once via the enclosing generic walk (attributed to the
file/class). The result is a deterministic pattern of duplicate nodes
(local functions minted twice under different parents with the SAME id),
duplicate `instantiates`/`calls` refs, and file/class-attributed
`function_ref` twins — all of which the walker must reproduce byte-for-byte,
in exactly the observed interleave. NO previously ported walker has this
surface. (2) **Both-arm parse-error incidence is 3.420.7% on the gate
repos** — flutter/flutter sits at 20.73% because the grammar (2025-10) cannot
parse *empty* object patterns (`TimerInitial() =>`, the dominant sealed-class
state-matching idiom) or the unnamed `library;` directive. Run parity sweeps
with **`--max-deferral 0.3`** (swift-style exemption). 0 phantom errors were
found anywhere (3 repos + a 46-snippet battery) — unlike kotlin — but the
defer rule stays trust-the-flag. (3) The "grammar bump" for dart is a
**byte-copy of the ALREADY-SHIPPING wasm** (unpinned-dep de-risking, not a
version change), so the bump gate is trivial — expect byte-identical
everything; the only real grammar gate is kernel-grammar-parity (vendored-C
build ↔ vendored wasm table identity).
## Grammar prep (wasm byte-copy vendor + vendored-C kernel build)
dart is NOT in `VENDORED_WASM_LANGS` (grammars.ts:291-333) — production
resolves `require.resolve('tree-sitter-wasms/out/tree-sitter-dart.wasm')`
(mapping `dart: 'tree-sitter-dart.wasm'` grammars.ts:36; `.dart` → dart at
grammars.ts:108, no content sniffing, no dialects). tree-sitter-wasms 0.1.13's
dart dependency is **UNPINNED `github:UserNobody14/tree-sitter-dart`**, built
at the 0.1.13 publish (2025-10-07) — a tree-sitter-wasms update would silently
change dart's grammar, which is exactly the hazard this vendor kills.
- **Provenance (verified by the batch-4 grammar probe, re-verified here):**
UserNobody14/tree-sitter-dart **master@`d4d8f3e337d8be23be27ffc35a0aef972343cd54`**
(2025-10-04, "Fix `set` and `get` contextual keywords…" #89) — parser.c
tables positionally identical to the shipped wasm (ABI **15**, STATE_COUNT
3640, SYMBOL_COUNT 524 + ALIAS_COUNT 1, TOKEN_COUNT 161, 22 fields,
EXTERNAL_TOKEN_COUNT 7).
- **Production wasm** sha256
`7f5364e4256cf7e55efd01dd52421ef2663caa8061b82659b7e4bf61064545ec`
(984,666 bytes, `node_modules/tree-sitter-wasms/out/tree-sitter-dart.wasm`).
**Vendor plan: BYTE-COPY this file** to
`src/extraction/wasm/tree-sitter-dart.wasm` + `VENDORED_WASM_LANGS +=
'dart'` (R7b comment: commit sha + "wasm is the byte-copied
tree-sitter-wasms 0.1.13 artifact; kernel compiles the same-commit vendored
C — codegraph-kernel/grammars/dart"). No rebuild, no `tree-sitter
generate` — the shipped bytes ARE the reference. `copy-assets` already
globs `src/extraction/wasm/*.wasm`. MIT license.
- **External scanner: YES** — `src/scanner.c` PRESENT at the pinned commit.
It owns 7 external tokens: the 5 string-template char classes
(`_template_chars_*`), `_block_comment`, and
`_documentation_block_comment` — i.e. **`/** */` doc comments are
scanner-produced**; the vendored C must include scanner.c or every
string/comment shape breaks.
- **Vendor file list + sha256 (recorded at survey time from the commit clone):**
- `src/parser.c` `5a42b47abb4d494f125dbdee9138979248041689b1aa36355550fa3e28dcb8b8`
- `src/scanner.c` `07a7b7818b175e9460523e705dd88d20f7b5141bac95c593d4426e6d52284996`
- `src/tree_sitter/parser.h` `180b893c8734778fd32f372dfbc27bd6ad1cd2221f26150b31256ff6716320d2`
- `src/tree_sitter/alloc.h` `b29c1c9fb7cc82f58c84b376df1297d6e2737a1d655fd356db0859e3c29c2fea`
- `src/tree_sitter/array.h` `5bdf6ed1a78e3409fd443e085ca967a64c188a5d082aaf7f819bccd53a471c94`
- **crates.io `tree-sitter-dart` 0.2.0 is the nielsenko FORK — REJECTED**
(different lineage, 480 positional table mismatches per the batch-4 probe).
This supersedes the plan §4 long-tail row's "crates.io" route for dart. Do
not revisit unless the TS side migrates grammars.
- **Kernel side — vendored-grammar-C (the kotlin #1382 mechanism, second
use):** copy the three-file set above (parser.c, scanner.c, tree_sitter/*.h)
to `codegraph-kernel/grammars/dart/`; extend `codegraph-kernel/build.rs`'s
cc::Build to compile both C files with the grammar's own flags (its
checked-in `bindings/rust/build.rs` uses `-Wno-unused-parameter`,
`-Wno-unused-but-set-variable`, `-Wno-trigraphs`, msvc `-utf-8` — same set
kotlin vendored). langs.rs:
```rust
extern "C" { fn tree_sitter_dart() -> *const (); }
// …
"dart" => Some(unsafe { tree_sitter_language::LanguageFn::from_raw(tree_sitter_dart) }.into()),
```
plus `LANGUAGES` (langs.rs:24; `[&str; 15]` at survey time — sibling
batch-4 legs are landing concurrently, take whatever count is current)
+= `"dart"`.
`__tests__/kernel-grammar-parity.test.ts:39` `GRAMMAR_LANGUAGES += 'dart'`
— the id-by-id ABI/kind/field-table compare against the vendored wasm is
the whole grammar gate here (both artifacts are same-commit by
construction; the parity test proves it stayed that way).
- **Staging:** wasm byte-copy + VENDORED_WASM_LANGS can land standalone
before the walker (full suite green; the "old-vs-new" dump gate degenerates
to cmp-identical since the bytes are identical — run it once on one repo as
a smoke test, expect zero). The kernel C vendor + build.rs + langs.rs +
grammar-parity row land with it or with the walker — same-commit from day
one.
- **Error incidence (both-arm reality — same grammar bytes on both arms by
construction; `error-sweep.cjs`, all `.dart` ≤1 MiB, `.git`/`node_modules`/
`.dart_tool` skipped):**
| Repo | files | hasError | % | phantom |
|---|---|---|---|---|
| dart-lang/shelf (small, server dart) | 99 | 10 | 10.10% | 0 |
| felangel/bloc (medium, Flutter + packages) | 616 | 21 | 3.41% | 0 |
| flutter/flutter (large, the framework) | 6,465 | 1,340 | 20.73% | 0 |
Error classes (sampled + minimized to repros, `dart3-errors.txt` +
`error-diag.cjs`): **(a) empty object patterns** — `Init() =>` /
`case Init():` / `if (x case Init())` ALWAYS error (MISSING identifier in
`constant_pattern`); patterns WITH fields (`Point(x: var a)`) parse clean.
This is the modern sealed-class state-match idiom → it alone drives
flutter's 20.73% and bloc's flutter examples. **(b) unnamed `library;`**
(the dangling-doc idiom, dart-lang style guide) → MISSING identifier;
`library foo.bar;` is clean. **(c) null-aware collection elements**
(`[?x]`, `key: ?value`, Dart 3.8) → ERROR, often blowing the whole file
root. **(d)** mustache-template `.dart` files (bloc's `bricks/`
`{{name.snakeCase()}}`) — not real dart. **(e)** `augment class` (rare).
Everything else probed CLEAN, including records, non-empty patterns, switch
expressions, sealed/base/final/interface/mixin class modifiers, extension
types, super-params, one-line class bodies (NO kotlin-style phantom),
shebang `#!`, digit separators, and CRLF. **A non-ASCII IDENTIFIER errors
(`String séance`) — but that is invalid Dart anyway** (identifiers are
ASCII-only by spec); keep fixture identifiers ASCII and non-ASCII text in
comments/strings.
- **Deferral policy:** per-file `has_error()` → `defer:` (trust the flag);
sweeps with **`--max-deferral 0.3`** (covers flutter's 20.7% with headroom;
shelf/bloc sit far under). A deferral-rate JUMP vs the table is a walker
bug; the rate itself is grammar reality.
## Architecture decisions
1. **No preParse, no POST_PASS.** `dartExtractor` has no `preParse` hook
(languages/dart.ts — whole file) → `preParsedSource` (kernel/index.ts:108)
is a no-op; both arms parse raw bytes. `POST_PASSES` (kernel/index.ts:94)
is empty → `tryKernelExtractRaw` stays eligible.
2. **No dart framework resolver exists** (grep src/resolution/frameworks/ —
nothing lists dart; no Flutter resolver). BUT the **universal** `vue` and
`astro` resolvers (no `languages` list ⇒ applicable to every language,
frameworks/index.ts:110-118) carry `extract()` hooks → in a repo where
vue/astro is DETECTED, parse-worker.ts:92-99 forces `.dart` files onto the
decoded `extractFromSource` path (their extract() hooks no-op on `.dart`
content — transport differs, output must not). Pure dart/Flutter repos
ride the raw buffers path. All three gate repos are expected raw-path —
verify at sweep time; a mixed Vue+dart repo is the decoded-path smoke
check.
3. **One walker module** (suggest `codegraph-kernel/src/dart.rs`), registered
in langs.rs; per-file `has_error()` → `defer:`. Cribs: **kotlin.rs** for
the visitNode-hook-consumed constants + hook/scan interplay; **java.rs**
for the class-like scope stack, static-member refs, decorators, and
type-annotation walks. **Three surfaces have NO precedent in any ported
walker** and are transcription work from this doc: (a) `extractBareCall`
selector-walking (dart is the first callTypes=[] language), (b) the
sibling-body double-walk reproduction (§The double-walk), (c) the
constructor naming/skip hooks (`resolveName`/`isMisparsedFunction`).
4. **Wire notes (KERNEL_ABI_VERSION 2, layout.ts:20):** no dart extraction
path emits refs carrying `filePath` (verified across every ground-truth
dump — zero refs printed one) → **REF_FLAG_FILE_PATH (layout.ts:98) stays
unused, flag 0 on every ref** like swift/rust. `function_ref` = wire code
200 (layout.ts:89). The node `decorators` wire field (layout.ts:56) is
**never populated for dart** (no `extractModifiers` hook — annotations are
`decorates` REFS only). The node `signature` field (layout.ts:55) is
**heavily used** (functions/methods AND hook-minted constants).
5. **MAX_FILE_SIZE (1 MiB) and skip dirs** (`.dart_tool`, `.pub-cache` —
extraction/index.ts:173) are orchestrator/TS-side and shared.
`isGeneratedFile` (generated-detection.ts:63-68) matches `.g.dart`,
`.freezed.dart`, `.pb.dart`, `.pbgrpc.dart`, `.chopper.dart` — those
files still EXTRACT normally but **skip the function_ref flush and the
value-ref pass** (tree-sitter.ts:647/:785); build_runner output is a huge
population in Flutter repos, so port the skip.
6. **Kind inventory** (what dart can emit): file, import, function, method,
class, enum, enum_member, type_alias, constant. **NEVER: field, property,
variable, struct, interface, trait, namespace, module.** Instance fields
mint NO nodes at all (§Constants); there is no namespace/package node
(no `packageTypes` hook — `library foo.bar;` is invisible); interfaces
don't exist as a dart concept (abstract classes are kind `class`).
## Extractor config (languages/dart.ts — 380 lines, read it whole)
Types: functionTypes=[`function_signature`] (:119); classTypes=
[`class_definition`] (:120); methodTypes=[`method_signature`,
`constructor_signature`] (:126); interfaceTypes=[] ; structTypes=[];
enumTypes=[`enum_declaration`] (:129); enumMemberTypes=[`enum_constant`]
(:130); typeAliasTypes=[`type_alias`] (:131); importTypes=[`import_or_export`]
(:132); **callTypes=[] (:133 — extractCall NEVER runs for dart; all call refs
ride `extractBareCall` in the body walker)**; variableTypes=[] (:134 —
extractVariable never runs); extraClassNodeTypes=[`mixin_declaration`,
`extension_declaration`] (:135). nameField=`name` (:172), bodyField=`body`
(:173), paramsField=`formal_parameter_list` (:174 — DEAD, see
§Type-annotation refs), returnField=`type` (:175 — DEAD likewise).
Grammar-shape facts the config leans on (all probed, `mini-cst.txt` /
`torture-cst.txt`):
- **Signature/body split:** every function and method is a SIGNATURE node
(`function_signature` / `method_signature` / bare `constructor_signature`)
whose body is the **next named sibling** `function_body`. `function_body`
wraps either a `block` or a bare `=>` expression (arrow bodies have no
block child). The `async`/`async*`/`sync*` keyword is an anon child of
function_body.
- **Field truth table** (fieldNameForChild + behavior-pinned):
`function_signature.name` → identifier ✓ (formal_parameter_list is an
UNFIELDED child; return type is an unfielded `type_identifier`/`void_type`
BEFORE the name). `method_signature` has NO fields (its single child is
the inner signature). `getter_signature.name` / `setter_signature.name` ✓.
`constructor_signature` has **`name` fields on BOTH identifiers** (`name:
Widget`, `name: named` — childForFieldName('name') returns the FIRST =
the class) + `parameters:` field. `factory_constructor_signature` children
are UNFIELDED identifiers. `class_definition`: `name:` ✓, `superclass:`,
`interfaces:`, `body:` ✓. `extension_declaration`: `name:` (absent for
anonymous extensions), `class:` (the on-type), `body:` (extension_body).
`mixin_declaration`: **NO fields at all** (children: anon-ish named `mixin`
keyword node, identifier, on-types as bare type_identifiers, class_body).
`enum_declaration`: `name:` + `body:` (enum_body); `enum_constant.name` ✓.
`type_alias` has NO fields (both modern `typedef X = T` and legacy
`typedef void X(int)` are node `type_alias`).
`initialized_variable_definition`: `name:` + one-or-more `value:` fields
(the RHS postfix chain is FLATTENED into multiple value-fielded siblings).
- **In-class member shapes:** methods/getters/setters/operators/factories
and BODIED constructors are `method_signature > (function_signature |
getter_signature | setter_signature | operator_signature |
constructor_signature | factory_constructor_signature)` + sibling
`function_body`, both direct children of class_body. **BODILESS
constructors** (`Widget(this.size);`, `Widget._() : x = 0;`) wrap in a
`declaration` node instead (`declaration > constructor_signature [+
initializers]`) — reached by the walker via plain recursion into
`declaration`. `const` constructors are `declaration >
constant_constructor_signature` and redirecting factories are
`declaration > redirecting_factory_constructor_signature` — **NEITHER is
in methodTypes → const ctors and `const factory X.r() = Impl;` are
INVISIBLE (no node, no refs)**. Fields are `declaration >
(static)? (final|const|type|var) > initialized_identifier_list |
static_final_declaration_list` (§Constants).
- **Statements/expressions:** calls have NO call node — a postfix chain is
FLAT siblings: `identifier selector selector …` where each `selector`
wraps `unconditional_assignable_selector` (`.name`) /
`conditional_assignable_selector` (`?.name`) / `argument_part >
arguments > argument*`. `new Foo(1)` IS a single `new_expression`
(children: unfielded type_identifier + arguments). `const Foo.bar(1)` in
EXPRESSION position is a `const_object_expression`; in a `const x = …`
declaration the const rides the declaration and the value is a plain flat
chain. Cascades are `cascade_section > cascade_selector + argument_part`
(NO selector node → invisible to everything, §Calls). Assignments are
`assignment_expression` with `left:` (an `assignable_expression`) and
`right:` fields. Locals are `local_variable_declaration >
initialized_variable_definition` (even without initializer). Lambdas are
`function_expression` (children incl. `body: function_expression_body`).
String templates: `string_literal > template_substitution` wrapping either
a full expression (`${…}`) or an `identifier_dollar_escaped` (`$name`).
Hooks PRESENT (port each exactly — anchors into languages/dart.ts):
- **visitNode (:144-157) — the constants branch.** `node.type ===
'static_final_declaration'` → nameNode = first namedChild of type
`identifier`; if present: valueNode = `nameNode.nextNamedSibling` (the
FIRST value child only — a flattened chain like `WidgetT(0)` captures just
`WidgetT`; a wrapped expression like `SHARED_MAX + 1` captures the whole
expression node); initValue = its text `.slice(0,100)` (UTF-16);
`ctx.createNode('constant', name, node, { signature: initValue ? \`=
${initValue}${initValue.length >= 100 ? '...' : ''}\` : undefined })`.
Return **true** (consumed — even when nameNode missing? NO: name found is
required for createNode but the hook returns true for EVERY
static_final_declaration reached, node minted or not — transcribe the
early-return shape exactly: `if (nameNode) {…}; return true`). The
dispatcher then runs `scanFnRefSubtree(node, 0)` and never descends →
§Function-as-value for what still gets captured. Everything else → false.
**Reality of the node type (probed, `probe2-cst.txt`):**
`static_final_declaration` = a `final`/`const` declaration WITH an
initializer that is **top-level** (`const SHARED_MAX = 10;`, `final
typedTop = compute();`, typed or untyped, incl. multi-declarations `final
multiA = 1, multiB = 2;` → one node per list entry) **or class-level WITH
`static`** (`static const int K_MAX = 9;`, `static final sharedInst =
WidgetT(0);`). Instance `final untyped = 5;` / `final int size;` / any
`var`/typed-var member → `initialized_identifier` → **NO NODE** (dart
emits zero `field` nodes, ever). Top-level `var topVar = 5;` / `int
topTyped = 6;` → initialized_identifier → invisible too. Constants get
**no docstring, no visibility, no isExported** — signature only (pinned:
`/// Doc on num const?` is dropped).
- **resolveBody (:158-171)** — `function_signature`/`method_signature` →
`node.nextNamedSibling` if it's a `function_body`, else null (NOTE: a bare
`constructor_signature` — the declaration-wrapped bodiless ctor — takes the
OTHER branch: childForFieldName('body') → null → find
class_body/extension_body among children → null ⇒ named bodiless ctors
never walk a body, and their `initializers` sibling is left to plain
visitNode recursion). For class/mixin/extension nodes: standard `body`
field first (class_definition ✓, extension_declaration ✓), else the first
namedChild of type `class_body` | `extension_body` (mixin_declaration has
no body field → found by type). Used by extractClass/extractEnum body
resolution AND createNode's endLine extension (tree-sitter.ts:1322-1334 —
**LIVE and load-bearing for dart**: the comment there names Dart; a
method/function node's endLine extends to the sibling body's end, e.g.
`named` L123-125 spans its body).
- **getReturnType = extractDartReturnType (:80-92)** — ctor = dartCtorInfo
(§below): a validated ctor returns the CLASS name (named ctors, factories
→ ret = enclosing class). Else sig = dartInnerSignature (:9 —
method_signature unwraps to function/getter/setter signature; **NOT
constructor/factory/operator** — those keep sig = the method_signature,
where the type search then finds nothing) → the FIRST namedChild of type
`type_identifier` → text, `.replace(/<[^>]*>/g,'')`, `.trim()`, last
`.`-segment, must match `/^[A-Za-z_]\w*$/`. Pinned: `Future<void>` →
`Future` (type_arguments is a SIBLING node, so the strip is a no-op);
`List<WidgetT>` → `List`; `WidgetT?` → `WidgetT` (nullable_type unwraps
positionally — the type_identifier is still first); `void` → undefined
(void_type ≠ type_identifier); `num`/`dynamic`/`Object` → themselves;
`T` (generic param) → `T` (leaks); **prefixed `other.OtherClass` → `other`**
(the prefix and the name are SEPARATE type_identifier leaves and the FIRST
is the prefix — the `.pop()` never sees a dot. BUG, PRESERVE); getters →
their type (`int get area` → `int`); setters → undefined; operators →
undefined (inner unwrap misses operator_signature); ctors → class name.
- **isMisparsedFunction (:177-188)** — dartCtorInfo != null AND ctorName ===
className → **the UNNAMED constructor `Widget(this.size)` is skipped** (no
node; extractFunction/extractMethod still resolve+walk the body — which is
null for the declaration-wrapped form, so in practice nothing emits).
Named ctors/factories are kept. The `@override (T) m()` misparse (an
annotation swallowing a record return type leaves `m()` shaped like a
single-identifier constructor_signature) is NOT skipped — dartCtorInfo
validates ids[0] against the enclosing type name via dartEnclosingTypeName
(:38 — walks parents for class_definition/mixin_declaration/
extension_declaration/enum_declaration and reads ITS name field) and
`reduce` ≠ `Action` → treated as the method it is (pinned:
`extract-probe4.txt` — method `reduce`, position starting AT `reduce`,
sig/ret undefined, decorates override intact).
- **getSignature (:189-208)** — method_signature unwraps to
function/getter/setter signature (NOT ctor/factory/operator); params =
find namedChild `formal_parameter_list`; retType = find namedChild
`type_identifier` | `void_type`; neither → undefined; result = `[retType
text + ' '] + [params text]`, trimmed. Pinned shapes: `void (int a,
String b)`; getter → `int` (retType only); setter → `(int v)` (params
only); generic return `Future<void> load()` → **`Future ()`** (the
type_arguments are NOT included — retType is just the identifier);
full param text is a RAW source slice — named/optional brackets, defaults,
nested function-typed params all verbatim (`void ({int? named, required
WidgetT child, String note = 'x'})`); bodied named ctor (method_signature >
constructor_signature) → **undefined** (unwrap misses ctor; no direct
formal_parameter_list on method_signature); **bodiless named ctor
(declaration > constructor_signature) → `"()"`-style params-only sig**
(node IS the ctor signature; its formal_parameter_list is found by type) —
pin BOTH ctor sig shapes; operators → undefined.
- **getVisibility (:209-222)** — method_signature → unwrap to
function/getter/setter signature → its first `identifier` child; other
nodes → childForFieldName('name'). Name starts `_` → 'private', else
'public'. QUIRKS, PRESERVE: **every constructor is 'public'** — for
method_signature-wrapped ctors the unwrap misses constructor_signature
(nameNode null → public); for bare constructor_signature the `name` FIELD
is the CLASS identifier (`Widget`, not `_`) → `Widget._()` is 'public'
(pinned). Operators → public. `_check`/`_privateMethod` → private ✓.
Classes/enums get visibility via the same hook at their extractors:
class_definition name field → `_Private` class would be 'private'.
- **isAsync (:223-233)** — nextNamedSibling is function_body → scan ALL its
children (anon included) for child.type === `'async'` → true. Pinned:
`async` → TRUE; **`async*` and `sync*` → FALSE** (different token text →
different anon type — generators are not "async"); bodiless (external/
abstract) → false. Note the anon child's TYPE is exactly `async` (probed
via behavior).
- **isStatic (:234-243)** — node.type === 'method_signature' → scan ALL
children for type `'static'` → true; else false. `static WidgetT make()`
→ true ✓ (the `static` keyword is an anon child of method_signature);
bare constructor_signature → false. Top-level functions → false.
- **resolveName (:244-260)** — dartCtorInfo: named ctor/factory → the ctor
name (`named`, `create`, `_`); unnamed ctor or non-ctor → undefined (falls
to extractName §below).
- **extractImport (:261-304)** — importText = trimmed full node slice;
moduleName = the URI string content: `library_import >
import_specification > configurable_uri > uri > string_literal` (else the
same chain under `library_export`), quotes stripped via
`.replace(/['"]/g,'')`. Returns {moduleName, signature} (no handledRefs)
→ import NODE (name = the URI, e.g. `package:torture/other.dart`,
`dart:async`, `src/reexported.dart`) + the generic `imports` ref
(tree-sitter.ts:3183-3194) {from: file node (no namespace exists), name =
URI, line/col of the import_or_export node}. `as alias`, `show`/`hide`
combinators: IGNORED (not read). **`import 'x.dart' deferred as y;` is
INVISIBLE** — the deferred form's import_specification holds a bare `uri`
(NO configurable_uri wrapper) → hook returns null → falls through the
multi-import inline handlers (none match import_or_export) → no node, no
ref (pinned, `extract-mini.txt`). `part`/`part of`/`library` directives
are different node types (part_directive, part_of_directive, library_name)
in NO type list → invisible. None of the TS/py/rust/php/ruby binding
emitters (:3197-3234) fire for dart.
- **extractBareCall (:305-379)** — §Calls, the full matrix.
Hooks ABSENT (the walker must NOT do these): `preParse`, `recoverMangledName`,
`classifyClassNode` (class_definition is always kind `class` — abstract/
sealed/base/final/interface-modified classes included), `classifyMethodNode`,
`extractPropertyName`, `propertyTypes`, `fieldTypes`, `getReceiverType`
(never diverts extractFunction:1522; no receiver QNs; no owner-contains
:1799 — extension members ride the extension's own class node),
`isConst`, `isExported` (**undefined everywhere except the file node's
literal `false`**), `interfaceKind`, `extractPackage`/`packageTypes` (no
namespace node — top-level QNs are bare), `extractModifiers` (node
`decorators` never set), `synthesizeMembers`, `skipBodilessClass` (bodiless
`class Base {}` still mints — dart classes always have `{}` anyway),
`methodsAreTopLevel`, `resolveTypeAliasKind` (type_alias is always kind
`type_alias`).
Registration: `EXTRACTORS.dart` (languages/index.ts:56), `FN_REF_SPECS.dart`
(function-ref.ts:394).
## tree-sitter.ts branches (anchors as of `45a53eb`)
### visitNode dispatch — what each dart node hits (ladder at 936-1303)
| Node | Branch | Behavior |
|---|---|---|
| every node | visitNode hook first (:943) | `static_final_declaration` consumed (§Constants); handled → scanFnRefSubtree + STOP |
| every node | maybeCaptureFnRefs (:990) | fires for `arguments`/`assignment_expression`/`pair`/`list_literal`/`static_final_declaration` in visitNode context — the source of the file/class-attributed fn-ref twins (§double-walk) |
| `function_signature` | functionTypes:994 | methodTypes does NOT include it → **always extractFunction:1517**, even inside a class (isInsideClassLike && !methodTypes.includes → else-arm). In-class function_signatures occur only for ABSTRACT/bodiless methods, reached via their `declaration` wrapper (`declaration > function_signature`, probed) → **kind `function` contained by the class** (pinned: `AbstractT::mustImpl` is a function; the sibling `declaration > getter_signature` abstract getter stays invisible). skipChildren |
| `class_definition` | classTypes:1005 | no classifyClassNode → always extractClass:1679 (abstract/sealed/base/etc. included) |
| `mixin_declaration`, `extension_declaration` | extraClassNodeTypes:1022 | extractClass(node) → kind **`class`** |
| `method_signature`, `constructor_signature` | methodTypes:1027 | **NOT class-gated at the ladder** — extractMethod:1737 runs anywhere; its own gate :1747 (not class-like, no methodsAreTopLevel, no receiver) falls back: parent `object`/`object_expression`? (never in dart) else **extractFunction** — this is how extension-TYPE members and any stray non-class method_signature become plain `function` nodes (pinned: `extension type MetersT` → function `km`, bare QN) |
| `enum_declaration` | enumTypes:1064 → extractEnum:1914 | §Class family |
| `type_alias` | typeAliasTypes:1071 → extractTypeAlias:2890 | plain type_alias node; `getChildByField(node,'value')` → **null** (no fields) → **NO refs from the aliased type** (`typedef MapAlias = Map<String, WidgetT>` emits nothing); returns false → children re-visited (function_type/formal_parameter_list children match nothing) |
| `import_or_export` | importTypes:1209 → extractImport:3170 | §Extractor config |
| `new_expression` | INSTANTIATION_KINDS:1255 (`new_expression` ∈ :354-361) | extractInstantiation:4610 → ctor field lookups null → namedChild(0) = type_identifier → `instantiates` ref from stack top; `<`-strip + last-`.`-segment apply (`new p.Foo<T>()` → `Foo`). findAnonymousClassBody → always null for dart. Children still recursed |
| `function_body` (sibling of a consumed signature) | **no branch** | recursed → THE DOUBLE-WALK (§below) |
| `declaration` (fields, bodiless ctors) | no branch | recursed → constructor_signature hits methodTypes; initialized_identifier/list, constant_constructor_signature, redirecting_factory_constructor_signature, initializers, annotations-in-place: nothing |
| `getter_signature` / `setter_signature` BARE (top level) | no branch | **top-level getters/setters are INVISIBLE** (no node; their sibling function_body is visitNode-recursed where calls don't extract) — in classes they're method_signature-wrapped → methods |
| `const_object_expression`, `selector`, `cascade_section`, `assignment_expression`, `local_variable_declaration`, patterns, `extension_type_declaration`, `part_directive`, `library_name`, lambdas | no branch | recursed; calls only extract in the BODY walker (`extractBareCall` is not consulted by visitNode!) — §Calls for the consequences |
| `property_signature`/`method_signature` TS branch (:1282) | **shadowed** | method_signature is consumed at :1027 first; property_signature isn't a dart kind — branch unreachable |
### THE DOUBLE-WALK (sibling bodies) — reproduce it exactly
Because bodies are SIBLINGS, every signature-consuming extractor walks its
body via resolveBody, and then the ENCLOSING loop (program level, class-body
loop, or an outer visitFunctionBody) visits the same `function_body` node
again as an ordinary child. What fires on each pass:
- **Pass 1 (visitFunctionBody, attributed to the function/method):** the
full body matrix — extractBareCall calls, instantiates, static-member
refs, nested named functions (:5245), fn-ref capture (:5137).
- **Pass 2a (visitNode recursion — program/class-body sibling visit):** ONLY
the visitNode-dispatched branches: `new_expression` → **instantiates from
the FILE (top level) or CLASS (class body)**; nested `function_signature`
→ **a SECOND extractFunction** (functionTypes:994) minting a DUPLICATE
node — same (kind, name, line) ⇒ **the SAME node id** — whose parent is
the file/class (contains edge from file/class; QN `localFn` at top level,
`Holder::methodLocal` in a class) and whose own body walk re-emits its
refs attributed to the duplicate; maybeCaptureFnRefs:990 → **fn-ref
candidate twins from the file/class**; `static_final_declaration` never
occurs inside bodies (locals are initialized_variable_definition) — no
constant dupes. Bare calls, static reads, cascades: NOTHING (no
extractBareCall/extractStaticMemberRef in visitNode).
- **Pass 2b (outer visitFunctionBody sibling visit — a LOCAL function's body
seen by ITS enclosing body walker):** the full body matrix again,
attributed to the ENCLOSING function — bare calls inside a local fn hence
emit from the local fn (pass 1) AND from the enclosing fn (pass 2b) AND
from the duplicate local fn (pass 2a's own body walk).
Pinned interleaves (`extract-probe2.txt`, transcribe as parity expectations):
- Top-level `hostFn` with local `localFn { inner(n); }` and local
`localWithNew { new Holder(1); }`:
nodes `hostFn::localFn` (contained by hostFn) … then `localFn` (bare QN,
contained by file, SAME id) after the pass-2a revisit; refs in order:
`calls inner from=localFn` (pass 1 of nested extract), `calls inner
from=hostFn` (pass 2b), `calls localFn from=hostFn`, `instantiates Holder
from=localWithNew`, `instantiates Holder from=hostFn`, `calls localWithNew
from=hostFn`, — then the program-level revisit: `calls inner from=localFn`
(2nd node's walk), `instantiates Holder from=localWithNew` (2nd),
`instantiates Holder from=file`.
- In-class `useNew` with a local `methodLocal { h.touch(); }` and `new
Holder(2)`: `instantiates Holder from=useNew`, `calls h.touch
from=methodLocal`, `calls h.touch from=useNew`, `calls methodLocal
from=useNew`, then the class-body revisit: `instantiates Holder
from=class:Holder`, `calls h.touch from=methodLocal` (2nd) — and node
`Holder::useNew::methodLocal` + duplicate `Holder::methodLocal` (kind
function both, same id).
- fn-ref twins: `register(seedValue)` in a top-level fn → `function_ref
seedValue from=refTaker` AND `from=file:…` (flush order: all in-scope
candidates first, then the revisit's file/class twins — capture order);
in a class method → `from=method:wire` AND `from=class:H` twins
(`fnref2.dart` pin).
- Per-declaration order: ALL of a function's pass-1 refs come before its
pass-2a refs, which come before the NEXT declaration's refs (the program
loop visits signature then body then the next signature). Same per class
member inside class_body.
**Dedupe reality:** node dupes share an ID (`generateNodeId` has no
uniquifier) — two node rows, two contains edges; downstream the store
last-writer-wins by id, but the WIRE/parity dump carries both rows —
byte-parity requires emitting both, in order. The kernel `node_ids` vec
must therefore NOT self-dedupe: emit exactly what TS emits.
### Node creation, IDs, qualified names
- createNode (:1308): id = `generateNodeId(filePath, kind, name,
startRow+1)` = `` `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}` ``
(tree-sitter-helpers.ts:18-30). FILE node id = literal `file:${filePath}`
(:509), name = basename, qualifiedName = filePath, endLine =
`source.split('\n').length`, isExported false.
- **endLine extension (:1322-1334) is LIVE** for function/method kinds:
resolveBody's sibling function_body extends endLine past the signature
(`named` L123→125; `bodyShapes` L60→96). Class/enum/etc. spans are their
own node extents.
- contains edge from nodeStack top for every created node (:1363);
extractModifiers merge (:1355) is a no-op (hook absent);
captureValueRefScope (:1374).
- qualifiedName = nodeStack names joined `::` (buildQualifiedName:1447; file
excluded; namespacePrefix always empty outside C/C++). No package node →
top-level QNs are bare; members `WidgetT::render`; nested
`WidgetT::useNew::methodLocal` (and the duplicate's shallower
`WidgetT::methodLocal` — §double-walk).
- isInsideClassLikeNode (:1486): stack-top kind ∈ {class, struct, interface,
trait, enum, module} — for dart that means class (incl. mixins/extensions)
and enum.
- Node position = the SIGNATURE node (functions/methods): `externalFn` starts
col9 (the `external` keyword sits OUTSIDE function_signature); the
`@override (T) m()` misparse starts at `m` (col16 pin); constants sit at
the static_final_declaration (name start), enum members at the
enum_constant, imports at import_or_export.
### extractFunction / extractMethod (:1517 / :1737)
- extractFunction: no receiver hook → never diverts. Name via extractName
(:90): resolveName (ctor names) → nameField `name` (function_signature ✓)
→ the method_signature inner-unwrap fallback (:148-167 — the dart-specific
branch: find inner function/getter/setter/constructor/factory_constructor
signature, take its first `identifier` child) → first
identifier/type_identifier child (:178-189) → `<anonymous>`.
- **Operator methods: name = `<anonymous>`** — operator_signature is NOT in
the :148 inner list and method_signature has no identifier children → the
fallback finds nothing → createNode('method', '<anonymous>', …) — the
name is truthy so the node IS minted (extractMethod has no `<anonymous>`
skip, unlike extractFunction:1549 which only guards the extractFunction
path — and even there `<anonymous>` only occurs for arrow/function_
expression types, not dart signatures). Pinned: `WidgetT operator +(…)` →
method `<anonymous>` vis=public sig/ret undefined, its param/return
type_identifiers still emitted as references FROM it, body calls
attributed to it. Multiple operators in one class → same name, different
lines → distinct ids.
- extractMethod extras {docstring, signature, visibility, isAsync, isStatic,
returnType}; extractFunction adds isExported (undefined). Then
extractTypeAnnotations (:1594/:1816 — §Type-annotation refs), then
extractDecoratorsFor (:1599/:1819 — §Decorators), then push + body walk
via resolveBody + pop.
- Bodiless-in-class function_signature (abstract members) → extractFunction
→ **kind `function`** node with class-prefixed QN (pinned `AbstractT::
mustImpl`); resolveBody → nextNamedSibling is NOT a function_body (next
member) → no body walk. Abstract GETTERS (`int get abstractGetter;`) are
bare getter_signatures → invisible entirely.
- extractMethod's fallback for non-class method_signatures (extension types,
hypothetical top-level method_signature) → extractFunction (:1760) after
the object-literal parent check (:1751 — never true in dart).
### The class family — extractClass / extractEnum
- **extractClass (:1679)** for class_definition + mixin_declaration +
extension_declaration: resolvedBody via hook (class_body/extension_body);
no skipBodilessClass. extras {docstring, visibility (name-underscore
rule), isExported undefined}. extractInheritance (§below) BEFORE the body
walk; extractCsharpPrimaryCtorParamRefs — **WARNING, NOT a no-op by
language gate**: :5938 checks `this.language !== 'csharp'` → returns
early ✓ (cheap early-out — port as a no-op). extractDecoratorsFor (§Decorators:
`@immutable class` → decorates from the class). Push, visit body
namedChildren via visitNode, pop.
- Extension names: `extension WidgetTExt on WidgetT` → name field →
`WidgetTExt`; **anonymous `extension on String` → the extractName
fallback finds the FIRST type_identifier = the ON type → a class node
named `String`** (pinned — members become `String::anonExt`). The
extended type is otherwise INVISIBLE (no ref — extractInheritance matches
nothing on extension/mixin nodes).
- Mixins: `mixin MixA on BaseT` → class `MixA`; the `on` types are bare
type_identifier children matching NO inheritance clause → **no refs**.
`mixin MixB implements DrawT` → the `interfaces` child DOES match →
implements DrawT ✓.
- **extractEnum (:1914)**: body REQUIRED (enum_body via `body` field —
present). extras {docstring, visibility, isExported undefined}.
extractInheritance on enum_declaration: `interfaces` child → implements ✓;
**the `mixins` child is a DIRECT child of enum_declaration (no superclass
wrapper) and matches NO clause → `enum StatusT with MixB` emits NOTHING
for the mixin** (pinned — the class-side mixin handling lives inside the
`superclass` branch only). Body loop (:1941-1950): `enum_constant` ∈
enumMemberTypes → extractEnumMembers; everything else (declaration-wrapped
fields/const ctors → nothing; method_signature getters/statics →
extractMethod with the enum pushed) → visitNode.
- **extractEnumMembers (:1958)**: `getChildByField(node,'name')` — the
`name` field EXISTS on enum_constant → **ONE enum_member node per
constant, positioned at the ENUM_CONSTANT node** (extent covers `ok(200)`
— id line = the constant's line), then RETURN. Ctor arguments
(`argument_part`) are NEVER walked → no refs from enum-constant
arguments. The identifier-scan fallback (:1967) is dead for dart.
- Inheritance — extractInheritance (:5291), the dart rows (probed):
- `superclass` child (class_definition only) → **the dart branch
(:5368-5393)**: for each namedChild of the superclass node — direct
`type_identifier` → **`extends`** ref; a `mixins` child → one
**`implements`** ref per `type_identifier` inside. `class WidgetT
extends BaseT with MixA, MixB` → extends BaseT + implements MixA +
implements MixB; `class OnlyMix with MixA {}` → superclass holds ONLY
mixins → implements MixA, NO extends (pinned). Generic supertypes:
the type_identifier is the base, `type_arguments` a sibling →
`extends Base` clean. Position = each type_identifier.
- `interfaces` child (:5437-5459, class + enum): targets = its
namedChildren (no type_list in dart) → one **`implements`** per child,
name = FULL child text (children are type_identifiers; a generic
`implements Comparable<T>` would ride the type_identifier + sibling
type_arguments shape → name `Comparable`).
- Order: extends+mixins first (superclass child precedes interfaces in
source), then interfaces — all BEFORE decorates and body members
(pinned ref order: extends BaseT, implements MixA, implements MixB,
implements DrawT, decorates immutable, then member refs).
- The python `argument_list`-under-`class_definition` branch (:5463)
shares dart's class node TYPE — but a dart class_definition never has
an argument_list child → dead; port nothing but know why it's safe.
- Extensions/mixins: extractInheritance runs but nothing matches (`on`
types are bare type_identifiers) — zero refs.
### Constants (the visitNode hook) — and what NEVER minted
Covered in §Extractor config. Emission pins (`extract-torture.txt`):
top-level `const SHARED_MAX = 10` → constant, QN bare, sig `= 10`;
`final DERIVED_VAL = SHARED_MAX + 1` → sig `= SHARED_MAX + 1` (whole
expression node); `final typedTop = compute()` → sig `= compute` (flattened
chain → FIRST value child only); `static final sharedInst = WidgetT(0)` →
constant under the class, sig `= WidgetT`; multi-declarations → one node
each with own columns. **NO nodes ever**: instance fields (typed/untyped/
late/var), `static var`, top-level var/typed vars, top-level getters/
setters, const constructors, redirecting factories, extension_type
containers, `part`/`part of`/`library`/deferred imports. **Initializer
side-effects:** hook-consumed constants' initializers are NOT walked → no
calls/instantiates from them (only scanFnRefSubtree capture — §fn-refs);
initialized_identifier fields' initializers ARE recursed by visitNode but
only INSTANTIATION_KINDS fires there → `int counter = 0;` emits nothing,
but a field `final w = new Widget();` would emit `instantiates Widget` from
the CLASS (and a top-level `var w = new Widget();` from the FILE). No
static-member refs from any of these contexts (body-walker only).
### Calls — extractBareCall (dart.ts:305-379) in the body walker (:5159-5173)
Only visitFunctionBody consults extractBareCall; the ref is {from: nodeStack
top, name, line/col of the MATCHED node (the selector/const-object node —
NOT the chain head)}. The dart matrix (all pinned in extract-torture.txt /
extract-mini.txt):
| Source shape | Matched node | Emitted `calls` ref |
|---|---|---|
| `helper(count)` | selector>argument_part; prev = identifier | `helper` at the selector position |
| `WidgetT(1)` | same | `WidgetT` (constructor = plain capitalized call; the resolution side's CONSTRUCTS_VIA_BARE_CALL — name-matcher.ts:918 — depends on exactly this shape) |
| `obj.method(x)` | args selector; prev = `.method` selector; accessorPrev = identifier | `obj.method` (receiver kept, lowercase or not) |
| `ConfigT.load()` | same | `ConfigT.load` **+ a `references ConfigT` static-member ref from the `.load` selector (§Static-member) — the double emission** |
| `WidgetT.named(3)` | same | `WidgetT.named` + references WidgetT |
| `other.OtherClass()` (prefixed ctor) | same | `other.OtherClass` |
| `this.own()` / `super.parent()` | args selector; prev = `.own` selector; accessorPrev = `this`/`super` node (not identifier) | bare `own` / `parent` |
| `a.b.call3(x)` (2+ hops) | accessorPrev = another non-argpart selector | bare `call3` |
| `FactoryT.create().run()` | outer args selector; accessorPrev = inner argument_part selector | **`FactoryT.create().run`** — the #750 re-encode via dartCalleeOfArgPart (:100-116): innerCallee `FactoryT.create` is `/^[A-Z]/` → `` `${innerCallee}().${method}` ``; plus the inner `FactoryT.create` ref from its own selector; plus references FactoryT |
| `WidgetT.named(3).chainTail()` | same | `WidgetT.named().chainTail` + `WidgetT.named` + references WidgetT |
| `lower().chain()` | innerCallee `lower` lowercase | bare `chain` + `lower` |
| `xs.map((e) => …).toList()` | innerCallee `xs.map` lowercase | bare `toList` + `xs.map` |
| `w?.render()` | conditional_assignable_selector | `w.render` — **`?.` is encoded exactly like `.`** |
| `y2..add(1)..add(2)` (cascades) | cascade_section (argument_part NOT inside a selector) | **NOTHING — cascade calls are completely invisible** |
| `new WidgetT(2)` | new_expression → INSTANTIATION branch :5145 FIRST | `instantiates WidgetT` (extractBareCall's new_expression arm :363-367 is DEAD — the else-if never reaches it); args still recursed |
| `pad(const EdgeInsetsT.all(8.0))` | const_object_expression :369-376 | `EdgeInsetsT.all` at the CONST node position (typeId + '.' + nameId; type-only form → `EdgeInsetsT`); children recursed after |
| `generic<int>(5)` | args selector (type args ride argument_part) | bare `generic` |
| `await fetch()` | recursion through unary/await_expression | `fetch` at the selector |
| `throw StateError('bad')` | recursion | `StateError` |
| `'sum ${a + compute()}'` | template_substitution recursion | `compute` (interpolation calls EMIT); `$name` → identifier_dollar_escaped → nothing |
| local-lambda body `final lam = (int a) { helper(a); }` | function_expression recursed transparently | `helper` attributed to the ENCLOSING function; `lam(5)` → `lam` |
| ctor initializers (`: size = seed()`), enum-constant args (`ok(200)`), default param values, hook-consumed constant initializers | never body-walked | NOTHING |
extractCall (:3684), LITERAL_RECEIVER_TYPES (:373-388), SKIP_RECEIVERS, the
parenthesized-conversion regex (:4530), template-strip — ALL UNREACHABLE for
dart (callTypes empty). Do not port them.
### Static-member / value-read refs — dart branch (:4759-4767), STATIC_MEMBER_LANGS:346
Called from the body walker only (:5218). The DART-SPECIFIC branch (the
shared MEMBER_ACCESS_TYPES path is never reached — it returns first):
node.type === `selector` AND it has NO `argument_part` child AND
previousNamedSibling is an `identifier` matching `/^[A-Z][A-Za-z0-9_]*$/` →
`references <identifier text>` from the enclosing symbol at the
**IDENTIFIER's (receiver's) position** (pushStaticMemberRef :4800). Pins:
- `ConfigT.setting;` → references ConfigT (value read).
- **`ConfigT.load()` → references ConfigT TOO** — the `.load` selector has
no argument_part (the args are the NEXT selector) and the dart branch has
NO callee-of-call skip → every capitalized-receiver method call
double-emits (references + calls). PRESERVE — this is the single biggest
ref-volume quirk on real repos.
- `util.Config.load()` → nothing (first selector's prev = lowercase `util`;
later selectors' prev = selectors).
- `this.x` → prev is a `this` node → nothing. Case patterns (`case
ColorT.blue:`) → `constant_pattern > qualified` shape, no selector →
NOTHING (pinned gap). Cascade sections → no selector → nothing.
Class-field/constant initializers and visitNode contexts → never called.
### Type-annotation references — dart ∈ TYPE_ANNOTATION_LANGUAGES (:5753), the dart branch (:5819-5833) is LIVE
For every function/method node, extractTypeAnnotations takes the DART path:
sig = node; if node.type === 'method_signature' → sig = first inner
function/getter/setter/constructor/factory_constructor signature (**?? node
— operators fall back to the method_signature itself**). Then
`extractTypeRefsFromSubtree(sig)` (:6090) — one `references` ref per
`type_identifier` LEAF in the whole signature subtree, skipping
BUILTIN_TYPES (:5768-5782), at each leaf's position. Consequences (pinned):
- Return types AND param types in one sweep: `void render(CanvasT c)` →
references CanvasT; `List<WidgetT> listRet(Map<String, WidgetT> m)` →
**references List** (List is NOT builtin!), WidgetT, **Map** (not
builtin), WidgetT.
- BUILTIN suppressions that matter for dart: `int`, `double`, `String`
(capital-S — the Scala row), `Boolean`, `bool`, `float`, `long`, `char`.
**NOT suppressed (noise refs, PRESERVE): `num`, `dynamic`, `Object`,
`List`, `Map`, `Set`, `Future`, `Stream`, `Iterable`, `T`-style generic
params.** `void` is a `void_type` node → structurally silent.
- Generic declarations self-reference: `T generic<T>(T v)` → **references T
×3** (return + the `<T>` type_parameters + the param — all
type_identifier leaves in the signature subtree).
- Prefixed types: `other.OtherClass` → references `other` AND `OtherClass`
(two leaves).
- Ctors: bodied named ctor (method_signature > constructor_signature) → sig
= constructor_signature → param types emit (`Widget.named(WidgetT w)` →
references WidgetT); `this.`-params (constructor_param) hold no
type_identifier → nothing. Bodiless declaration-wrapped ctors: extractMethod
runs on the bare constructor_signature → sig = node → same.
- Getters: references from the getter's TYPE (suppressed if builtin —
`int get area` → nothing; `WidgetT get w` → references WidgetT).
- extractVariableTypeAnnotation (:6074) needs a `type_annotation` child —
**no such node type in this grammar → dead**; the body-walker
`variable_declarator` branch (:5230) — no such node type → dead. Local
`WidgetT w = …;`, field types, `is`/`as` types, collection type args in
bodies → **NO refs** (pinned).
### Decorators — dart annotations DO emit `decorates` via the SIBLING scan
Dart annotations are `annotation` (with `name:` field; args form has an
`arguments` child) or `marker_annotation`-free — probed: both `@override`
(bare) and `@Deprecated('x')` (args) are node type `annotation`, PRECEDING
SIBLINGS of the declaration they decorate (inside program / class_body).
extractDecoratorsFor (:4897) is called for classes (:1710), functions
(:1599), methods (:1819) — NOT for hook-minted constants, enums(!), or
type aliases (extractEnum/extractTypeAlias never call it — an annotated
enum emits nothing). Mechanics for dart:
- Scan #1 (direct children :4976-4988): annotations are never children of
the signature → inert (no `modifiers` node either).
- Scan #2 (preceding siblings :5002-5023): walk BACKWARD from the
declaration; `annotation` is in the accepted set (:5017); stop at the
first non-annotation sibling. consider(): target = first namedChild of
accepted types → the `identifier` (`override`, `deprecated`, `pragma`,
`immutable`, `Deprecated`) → `<`-strip + last-`.`-segment (`@ui.Widget`
style would strip to `Widget`) → **`decorates` ref {from the decorated
node, name, line/col of the ANNOTATION node}**. With-args annotations
emit their NAME; the argument expressions are never visited (no refs from
`@Deprecated('use other')`'s string).
- **Stacked annotations emit in REVERSE source order** (the backward walk):
`@Deprecated('x')\n@pragma('vm:entry-point')\nvoid f()` → decorates
`pragma` FIRST, then `Deprecated` (pinned).
- For a method: the previous member's function_body (or any declaration)
breaks the chain correctly. The annotation-BETWEEN-doc-and-decl also
breaks the DOCSTRING chain (§Docstrings).
- Bodiless ctors (declaration-wrapped): extractMethod runs on
constructor_signature whose PARENT is the `declaration` node — the
backward scan runs over declaration's children (constructor_signature is
namedChild(0) → declIdx 0 → no siblings scanned) → an annotation before
the declaration attaches to NOTHING. Annotated fields likewise emit
nothing (no extractor runs).
### Docstrings (tree-sitter-helpers.ts:95-127) — dartdoc is KEPT, both forms
Dart comment node kinds: `comment` (`//` and `/* */`) and
`documentation_comment` (**`///` AND `/** */`** — the block form is
scanner-produced, external `_documentation_block_comment`). BOTH kinds are
in getPrecedingDocstring's accepted set → **`///` runs, `/** */` blocks, and
plain `//` comments all become docstrings and accumulate together** (pinned:
`/// Line doc kept.` + `// Plain comment also kept?` → joined two-line doc;
`/** Block dartdoc kept. */` → kept). cleanCommentMarkers: the `/*`-open
strip + `^\/\/[/!]?\s?` + `^\s*\*\s?` gm strips fire — **all `gm` strips
ride `js_multiline_strip` in docstring.rs (#1329 CRLF semantics) — call the
shared code, port nothing**. DOCSTRING_WRAPPER_TYPES contains no dart kinds
→ no anchor climbing. **An `annotation` between the comment run and the
declaration BREAKS the chain** (pinned: `/// Broken by annotation.`
`@deprecated` `void annotated()` → doc undefined — the dominant real-world
loss since `@override` is ubiquitous). Docstrings attach to: functions,
methods (incl. `<anonymous>` operators), classes/mixins/extensions, enums,
type aliases. NOT to: hook-minted constants (extra carries only signature —
pinned drop), enum members, imports, the file node. No comment-gluing into
import extents (probed — import_or_export ends at the `;`).
### Value-reference edges (:398-931) — dart IS in VALUE_REF_LANGS (:401)
Port the full machinery (crib java.rs/kotlin.rs): `CODEGRAPH_VALUE_REFS=0`
kill; MAX_VALUE_REF_NODES = 20,000 caps both DFS passes; isGeneratedFile
skip (`.g.dart` and friends!).
- **Targets** (captureValueRefScope:735): kind constant|variable, name ≥3
chars AND `/[A-Z_]/`, parent id prefix ∈ {file:, class:, module:, struct:,
enum:}. Dart mints ONLY `constant` (kind variable never occurs) → targets
= hook constants under file: or class: (an enum-scoped `static const`
would ride enum:). `lowercase_const`/`plain`/`low` (no capital, no `_`) →
not targets; `kLimit`/`typedTop` (embedded capitals) → targets.
- **Reader scopes** (:764): every function/method/constant node (dart has no
variable nodes). **The Dart sibling-body pull (:883-892) is LIVE and
load-bearing**: a function/method reader scope's node is the SIGNATURE —
its `nextNamedSibling`, when of type `function_body` (or `block`), is
pushed into the reader DFS; without it every method/function body read
would be invisible. Constants' reader subtree is the
static_final_declaration itself (initializer reads: pinned
`DERIVED_VAL → SHARED_MAX` edge); their nextNamedSibling is another
static_final_declaration (multi-lists) → not pulled ✓. Duplicate local-fn
nodes (§double-walk) are ALSO reader scopes — same reads, both scopes
(dedupe is per (scope,target), scopes differ → **duplicate value-ref
edges from the twin scopes when a local fn reads a target** — include the
shape in a fixture if a local fn reads a constant).
- **Shadow prune** (:803-878): the dart declarator cases (:844-850) —
`static_final_declaration` (the target itself), `initialized_identifier`
(fields/top-level vars), `initialized_variable_definition` (locals) — each
bumps its first `identifier`-typed namedChild. **Uninitialized locals
(`int DERIVED;`) still bump** (pinned prune); a method-local `final
SHARED_MAX = 1;` prunes the file-wide target (pinned). **`assignment_
expression` is NOT a prune case** (the :829 `assignment` case is
python's — the swift-lesson check comes out DEAD for dart): an
assignment-only rebind never bumps, and `low = 5;` style writes are
invisible to the prune. fileScopeValueCounts: conditional double-defs
don't occur in dart (const redefinition is illegal) — counts are 1 each.
- **Emission** (:880-930): per reader scope, stack-DFS (namedChildren pushed
in order, POPPED — reverse-source-order visitation; edge order follows);
match node type `identifier` (:907 — `constant`/`name`/`simple_identifier`
never occur in dart trees). Any identifier text mapping to a live target
emits — including the member half of `Table.COL_LIMIT` navigation
(pinned) and `${SHARED_MAX}` interpolations (template_substitution >
identifier). **`$SHARED_MAX` (no braces) is `identifier_dollar_escaped`
NOT accepted → no read** (pinned asymmetry). Skip self-id + same-name,
dedupe per (scope,target) → EDGE {kind:'references',
metadata:{valueRef:true}}, appended AFTER all other edges (last rows of
every dump).
### Function-as-value capture (#756) — DART_SPEC (function-ref.ts:310-320)
idTypes = {`identifier`} (bare identifiers ARE candidates). dispatch:
`arguments` → args; `assignment_expression` → rhs (field `right`); `pair`
value (field `value`); `list_literal` → list; `static_final_declaration`
varinit (NO field → last-named-child rule :471-487: requires ≥2 named
children; the name-field guard is inert — no `name` field on
static_final_declaration). layers: `argument` → null (fan out). NO special,
NO unwrap, NO ungatedModes, NO addressOfOnly. Pins:
- `register(topLevel)` → candidate via arguments→argument→identifier ✓.
- **Named arguments are NOT captured**: `reg(cb: onlyNamed)` → the child is
`named_argument` — not in layers, not an idType → NOTHING (pinned,
`namedarg.dart`). Flutter's `onPressed: handler` idiom is therefore
invisible — bug-for-bug, do NOT "fix" in the port (candidate future
accuracy PR: add `named_argument`/`label` handling TS-side first).
- `obj.cb = assigned` → rhs capture ✓; param-storage skip (:425-443)
compares the LHS's trailing identifier to the rhs text — `this.cb =
assigned` kept (cb ≠ assigned), a hypothetical `this.cb = cb` skipped.
`cb = selfStore` where selfStore is a PARAMETER shadowing a same-named
function → captured + gate-passed (false positive, PRESERVE — pinned).
- `[topLevel, blockDoc]` list ✓ (locals' list_literals capture too — the
dispatch fires wherever the node is walked); `{'k': topLevel}` pair ✓;
`final aliasTop = aliased;` top-level/static → varinit bare-identifier ✓
(pinned from=file / from=class); a LOCAL `final alias = topLevel;`
initialized_variable_definition NOT in dispatch → not captured.
- obj.method member values (`final g = obj.method`) → the last child is a
selector → normalizeValue [] → nothing (no member special for dart).
- Capture points: visitFunctionBody:5137 (bodies), visitNode:990 (the
§double-walk twins from file/class scope), scanFnRefSubtree (hook-consumed
constant initializers — halts at `function_expression` :609, so lambdas
inside a constant's initializer don't leak candidates).
- **Flush gate (:639-728): effectively "defined in this file" ONLY** — dart
import refs are URIs (`package:foo/util.dart`, `dart:async`) which match
neither SIMPLE_NAME nor QUALIFIED_IMPORT (`:` and `/` excluded) →
importedNames is always EMPTY for dart. definedHere = same-file
function/method NAMES — which includes single-letter method names (a
method `a` gates any bare arg `a` — pinned false positive, PRESERVE) and
`<anonymous>`. No `this.`/`::` forms are ever produced (no special) →
every candidate takes the definedHere gate. Dedupe `${fromNodeId}|${name}`
→ survivors as {referenceKind:'function_ref'} (wire 200) appended after
all walk refs — in-scope candidates first, then the double-walk
file/class twins (capture order).
### visitFunctionBody (:5129-5286) — dart rows
- maybeCaptureFnRefs (:5137) per node.
- callTypes (:5143) — empty, dead. INSTANTIATION_KINDS (:5145) →
new_expression → extractInstantiation (children still recursed — a nested
`new Foo(bar())` emits both instantiates and the inner call).
- **extractBareCall (:5159-5173)** — §Calls. No skip/return → children
always recursed after (inner chain selectors each get their own shot).
- extractStaticMemberRef (:5218) — §Static-member.
- variable_declarator branch (:5230) — dead (no such kind).
- Nested `function_signature` (:5245) → named → extractFunction → local
functions as `function` nodes (QN nested via stack), THEN the enclosing
walker also re-walks the local's sibling body (§double-walk pass 2b) and
the outer visitNode revisit re-extracts it (pass 2a).
- classTypes/enumTypes in bodies (:5255/:5268) — grammatically impossible in
dart (no local classes/enums) → dead.
- Recursion is transparent through if/for/while/switch statements (but case
PATTERN internals use pattern node kinds — `constant_pattern > qualified`
— where neither extractBareCall nor static refs match → enum reads in
case labels are invisible, pinned), try/catch, await/unary, throw,
string templates, function_expression lambdas (calls attribute to the
enclosing symbol), collection literals (`<Widget>[…]` type args silent).
### Misc shared paths
- Positions: `line = startPosition.row + 1`, `column =
startPosition.column` — **UTF-16 code units** (textutil::col16), as are
startIndex/endIndex substrings (getNodeText everywhere: signatures,
constant initializers `.slice(0,100)`, import signature `.trim()`).
- extract() wrap (:454-568): file node → (no package hook → no namespace) →
root visitNode walk → flushFnRefCandidates → flushValueRefs → pop. Table
order: nodes in creation order (INCLUDING double-walk duplicates); contains
edges interleaved with creation; value-ref EDGES appended last;
walk-order refs then function_ref refs at flush. Store/harness are
rowid-order-sensitive — reproduce exactly.
- **CRLF: probed clean end-to-end** — the CRLF-converted torture fixture
extracts BYTE-IDENTICALLY to LF (`extract-crlf.txt` diff-0 after filename
normalization). dart.ts has NO multi-line regexes (`/<[^>]*>/g`,
`/^[A-Za-z_]\w*$/`, `/['"]/g`, `/^[A-Z]/` are all single-token); the only
CRLF-sensitive shared path is cleanCommentMarkers' `gm` strips →
`js_multiline_strip` in docstring.rs. Multi-line signature slices and
≥100-char initializer captures include raw `\r` bytes on CRLF sources —
identical on both arms by construction (same source slice).
- Defer policy: per-file `has_error()` → `defer:`; expected incidence per
§Grammar prep (3.420.7%); `--max-deferral 0.3`. No phantom class found —
but defer on the FLAG regardless (never on ERROR-node presence).
- Language display name `Dart` (grammars.ts:626) — cosmetic only.
## Frameworks & synthesis consumers (stay TS-side — pin the walker's output contract)
- **No dart framework resolver, no dart callback-synthesizer pass, dart ∉
CC_LANGUAGES** — grep-verified. Nothing consumes dart node EXTENTS beyond
the generic paths; the endLine extension still matters for context slices
and explore output.
- **Resolution contracts the walker's output feeds** (behavior to hold
steady, verified at 45a53eb):
- `CONSTRUCTS_VIA_BARE_CALL` (name-matcher.ts:918) — bare capitalized
`calls WidgetT` refs resolve as constructions; depends on ctor calls
staying PLAIN calls refs (dart emits `instantiates` ONLY for
`new_expression`).
- `matchDottedCallChain` (name-matcher.ts:2147-2166, dart listed) —
consumes the `Foo.create().run` re-encode + the method nodes'
`returnType` (extractDartReturnType's class-name-for-ctors rule is what
makes `WidgetT.named(3).chainTail()` resolve — #750/#645).
- Local-variable receiver inference (name-matcher.ts:1202-1209) — regexes
over raw source (`var lg = Logger(`, `Logger lg,`) — no extraction
dependency beyond node names.
- CHAIN_LANGUAGES (resolution/index.ts:42) includes dart — same inputs.
- `matchMethodCall`'s bare-fn-only carve-out EXCLUDES dart (:226-235) —
bare fn-ref names may resolve to METHODS (implicit-self) — walker just
supplies names.
- rnCrossPlatformEdges / expo / fabric: dart NOT in their language sets.
## Parity mechanics (all have bitten before)
- **Emission order** per §Misc + §double-walk: file → source-order walk
(per construct: node + contains → for classes: extends/implements →
decorates → members; per function/method: node + contains → type-refs →
decorates → body refs → then the sibling-revisit twins) → function_ref
refs (in-scope then twins) → value-ref EDGES last.
- **generateNodeId inputs**: (filePath, kind, name, startRow+1). Names:
operators are `<anonymous>`; ctors are the ctor NAME (`named`, `_`,
`create`); anonymous extensions are the ON-type text; imports are the
URI; enum members the constant name at the enum_constant's line.
**Duplicate local-fn nodes share an id — emit both rows.**
- **UTF-16 columns/slices** everywhere; non-ASCII lives ONLY in
comments/strings (identifiers are ASCII by spec — a non-ASCII identifier
ERRORS and defers). The torture fixture's unicode comment + string pin
the column math.
- **CRLF**: fixture variants derived in-memory (kernel-tsjs-parity
pattern); expected: byte-identical output modulo the source bytes
themselves.
- **Defer policy**: has_error → defer, `--max-deferral 0.3`, expected
counts shelf 10/99, bloc 21/616, flutter ~1340/6465 (flutter HEAD drifts
— re-sweep at port time; the RATE is the signal).
- **node_ids self-check**: compare ID STRINGS; do NOT dedupe (double-walk).
## Gates (per plan §5, adapted to the byte-copy vendor)
- **Vendor gate (replaces the usual bump gate):** wasm byte-copy +
`VENDORED_WASM_LANGS += 'dart'` + kernel C vendor + langs.rs +
`GRAMMAR_LANGUAGES += 'dart'` — full suite green; one smoke dump-diff
(old resolution path vs vendored path) on shelf, expected byte-identical
(same bytes); kernel-grammar-parity proves C-build ↔ wasm table identity.
- **Torture fixtures** per `## Fixtures to build`, exercised by a new
`__tests__/kernel-dart-parity.test.ts` (+ CRLF variants in-memory).
- **Parity sweeps** (`scripts/kernel-parity.mjs <dir>`, order-sensitive
full-object, **`--max-deferral 0.3`**):
- `svy-dart/gate-repos/shelf` (small, 99 files — server dart; re-clone
dart-lang/shelf if gone)
- `…/bloc` (medium, 616 files — Flutter widgets + pure-dart packages +
generated-file population)
- `…/flutter` (large, 6,465 files — the framework; the deferral-heavy
arm)
(all three cloned fresh at survey; re-clone public OSS — agent-eval
policy). Expect 0-diff on every non-deferred file and ~the §Grammar-prep
deferral counts. Then **full-init dump-diffs byte-identical** (kernel arm
vs `CODEGRAPH_KERNEL=0`, `scripts/dump-graph.mjs`, cmp) on the same
three.
- **Census spot-check** (double-walk belt-and-suspenders): after a bloc
kernel-arm index, `select kind, count(*) from nodes where language='dart'
group by kind` must equal the wasm arm EXACTLY — the duplicate-node rows
are where a "helpful" dedupe would silently diverge before the dump gate
runs.
- **Suite**: torture + CRLF + a defer fixture (empty-object-pattern file —
kernel defers, wasm output served) + the `library;` defer shape; full
suite ×2 green with `CODEGRAPH_KERNEL_EXPECT=1`.
- **`DEFAULT_ROUTED += 'dart'`** (kernel/index.ts:37) only after ALL of the
above; changelog rides the existing kernel entry.
- Post-route perf sanity: all three gate repos ride the raw path (§arch-2);
a mixed Vue+dart repo is the decoded-path smoke check. The dart speedup
lands on ~7997% of files (deferral costs).
## Fixtures to build
1. `__tests__/fixtures/kernel-parity/torture.dart` — seed from the survey's
`svy-dart/torture.dart` (231 lines, parses clean; its
`extract-torture.txt` is the expected-output pin). Inventory by branch:
imports (dart:, package: with `as`+`show`, export with `hide`,
**deferred → invisible**, part → invisible); doc shapes (`///` run,
`/** */` kept, `//` kept, annotation-broken chain); annotations (bare,
with-args, stacked → reverse order, on class); top-level constants
(CAPS/lowercase/multi/typed/derived, sig truncation ≥100 chars);
top-level var/typed-var/getter/setter (all invisible); async vs
async*/sync* (isAsync true/false/false); returnType matrix (builtin,
generic container, nullable, prefixed → `other` bug, `T` leak, void);
signature matrix (params verbatim incl. named/optional/defaults/
function-typed, getter type-only, setter params-only, generic-return
`Future ()`); external fn (position after keyword); the full ctor set
(unnamed skipped; named bodied — no sig; named bodiless — `()` sig +
initializer-call invisibility; factory; **const factory + const ctor
invisible**); operator → `<anonymous>` method; private `_` names →
visibility; class with extends+with+implements (ref kinds/order) +
with-only class; mixin `on` (nothing) + mixin implements; named +
**anonymous extension (class named after the ON type)**; abstract class
(bodiless members → kind `function`; abstract getter invisible);
sealed/base modifiers (plain classes); enum (simple + enhanced: ctor'd
constants at constant positions, args unwalked, `with` GAP vs
`implements` ✓, members after `;`); typedef ×3 (modern/legacy/generic —
zero refs); extension type (container invisible, **members leak as bare
`function`s**); the FULL call matrix (§Calls rows verbatim — incl.
cascade invisibility, `?.`, const-object args, chain re-encodes,
interpolation calls, `$x` vs `${x}`); static reads (`X.member`,
`X.method()` double-emission, 3-segment nothing, case-pattern GAP);
local shapes (locals/uninit/lambdas/local fns — pinning the
§double-walk interleave exactly); fn-refs (positional arg, **named-arg
NOT captured**, rhs + param-storage-skip + shadow false positive, list,
pair, top-level/static bare-identifier varinit, local alias not
captured, undefined-name gated, file/class twins); value refs (targets
CAPS + kName; readers via sibling pull; constant→constant; `${X}` read
+ `$X` non-read; `X.member` member-half read; local shadow prune;
uninitialized-local prune; assignment non-prune; lowercase non-target);
a unicode comment + string before a symbol (UTF-16 columns).
2. **CRLF variants** derived in-memory (kernel-tsjs-parity pattern).
3. **Defer fixture #1**: `switch (x) { Init() => 1, _ => 0 }` (empty object
pattern) — kernel defers (`defer:`), wasm serves the recovery output.
4. **Defer fixture #2**: `library;` header file — same contract.
5. **Generated-file fixture** (`x.g.dart` naming) — extraction normal but
NO function_refs / value-refs (the isGeneratedFile skips).
## Probe artifacts (session scratchpad `svy-dart/`)
`cst-dump.cjs` (full CST + field labels via fieldNameForChild) +
`mini-cst.txt`/`probe2-cst.txt`/`probe3-cst.txt`/`probe4-cst.txt`/
`torture-cst.txt`; `extract-probe.cjs` (the REAL dist extractor —
nodes/edges/refs dumps) + `extract-mini.txt` (imports/deferred-import,
ctor set, operator `<anonymous>`, enum shapes, mixin/extension, call
matrix), `extract-probe2.txt` (THE double-walk interleave pins, local fns,
async*/sync*, annotations, value-ref prune, top-level shapes),
`extract-probe3.txt` (signatures verbatim, num/dynamic/Object refs,
external position, redirecting/const factories, lambdas, decorates order),
`extract-probe4.txt` (block dartdoc, extension-type leak, record-misparse
rescue, this/super calls, case-pattern gap), `extract-probe5.txt`
(value-ref matrix, prefixed calls, docstring drops, unicode),
`extract-torture.txt` + `extract-crlf.txt` (the master ground truth, LF +
CRLF byte-equal); fixtures `mini.dart`, `probe2-5.dart`, `namedarg.dart`,
`fnref2.dart`, `fnref3.dart`, `torture.dart` (+ `torture-crlf.dart`);
`probe-dart3.cjs` + `dart3-errors.txt` (46-construct error battery);
`error-sweep.cjs` + `errors-{shelf,bloc,flutter}.txt` +
`flutter-err-files.txt` (incidence + per-file lists); `error-diag.cjs`
(first-error context — the class minimizations); `tree-sitter-dart/` (the
d4d8f3e commit clone — vendor-file shas recorded in §Grammar prep).
Scratch dirs are throwaway — re-derive from this doc if gone.