Files
codegraph/docs/design/dart-kernel-port-checklist.md
Colby MchenryandClaude Opus 5.5 bf6f1fa67e fix(dart): a typedef references the types it aliases (#2763)
In

  class Report { final int score; const Report(this.score); }
  typedef ReportCallback = void Function(Report r);

`codegraph callers Report` did not list ReportCallback. Both extractors
minted the typedef's node and nothing else. tree-sitter-dart gives
`type_alias` no fields, so extractTypeAlias found no `value` to read and
had no Dart branch, and the kernel's extract_type_alias mirrored that.
Impact on a model type missed every callback or collection type built
on it. The legacy form with a return type, `typedef Report
LegacyMaker(int n);`, was also named after that return type: with no
`name` field the name fell back to the first type name in the
declaration, so a `type_alias Report` (or `Widget`, `Future`, `bool`)
stood beside the real type and took references meant for it.

Both extractors now name a typedef by the last type name before its
`=`, its type parameters or its parameter list, and add a `references`
ref from the typedef, on each type's name, for every type its aliased
type names: a function type's return and parameter types, generic
arguments, record fields, a type parameter's bound, and the legacy
form's return and parameter types. Annotations are not part of it, an
import prefix names no type, and a type parameter (the typedef's own,
or one a generic function type or an old-style generic function-typed
parameter declares) is skipped unless written through a prefix. Kernel
and wasm output are identical; a new parity fixture,
TortureTypedefs.dart, pins every form in LF and CRLF.

Validation, kernel path, arms built from dbb6aa9b and from this change,
each with its own kernel:
- felangel/bloc b9be1e25: 17,875 -> 17,884 edges (+9, 0 removed).
- rrousselGit/riverpod 4ded4f01: 84,726 -> 84,761 (+35, 0 removed).
- jonataslaw/getx e3c8fa04: 8,436 -> 8,456 (+20, 0 removed).
- alibaba/flutter-go a41b2124 (9 legacy typedefs, 6 with a return
  type): 7,994 -> 7,968; 39 wrong links removed (34 `Widget` and 5
  `Future` uses that had gone to typedefs misnamed `Widget` and
  `Future`), 13 added (4 from typedefs, 9 fields typed by the renamed
  ones).
- peng8350/flutter_pulltorefresh 0be096c1 (8 legacy, 5 with a return
  type): 3,186 -> 2,976; 221 wrong `Widget` links removed, 11 added.
All 88 added non-contains edges were checked against the source; every
removed edge pointed at a typedef misnamed after its return type. The
other new typedef refs name SDK or Flutter types and stay unresolved.
gin and zustand dumps are byte-identical across the arms, kernel and
wasm dumps are byte-identical on bloc, riverpod and pulltorefresh, and
the parity sweep over the five Dart repos is 2,330/2,633 files at
parity, 0 diffs, 303 deferred on both arms. Index time is unchanged
within noise (median of 5 interleaved runs: riverpod 12.6 s vs 11.9 s,
bloc 5.0 s vs 5.0 s, getx 2.8 s vs 2.8 s), and the DB grows by 8 to
24 KB (pulltorefresh shrinks by 64 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:46:20 +00:00

92 KiB
Raw Permalink Blame History

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.4–20.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:

    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, constant_constructor_signature, redirecting_factory_constructor_signature] (:126 — the last two since 2026-10-06); interfaceTypes=[] ; structTypes=[]; enumTypes=[enum_declaration] (:129); enumMemberTypes=[enum_constant] (:130); typeAliasTypes=[type_alias] (:131); importTypes=[import_or_export, part_directive] (:132 — the second since 2026-10-06); 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). Its name is the last direct type_identifier before the =, type_parameters or formal_parameter_list child: a legacy return type comes first (typedef Report X(int n)), and until 2026-10-21 the first-identifier fallback named such a typedef after its return type (dartTypeAliasName / type_alias_name now, via resolveName). 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 — both carry NO fields (children: const_builtin? "factory"? identifier [. identifier] formal_parameter_list, then for a redirect = and the target: type_identifier [type_arguments] [. name], where the target constructor's name is a type_identifier without type arguments and an identifier after them). Until 2026-10-06 neither was in methodTypes (no node, no refs); now both mint methods exactly like the bare constructor_signature (§isMisparsedFunction, §resolveName). Abstract and external members are declaration > function_signature too (an anon external/static keyword may precede the signature). In every one of these the member's dartdoc and annotations are siblings of the declaration, not of the signature — §getDeclarationWrapper. 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 }). When the constant is minted, the hook pushes it and walks the WHOLE declaration with the body walker (ctx.walkInitializer(node)— visitFunctionBody that records the node as walked), then pops: the initializer's calls, instantiations, static and member reads, types, closures (block bodies and local functions included) and fn-ref candidates (thevarinitcapture of the declaration itself included) are the CONSTANT's. 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) {…; if (constant) walk}; return true). The dispatcher then runs scanFnRefSubtree(node, 0, walked)` — which skips a walked declaration outright, so only an unminted one is scanned — and never descends.
  • visitNode — the field / top-level variable branch. node.type === 'initialized_identifier' whose parent is an initialized_identifier_list directly under program, or under a declaration in a class_body/extension_body/enum_body → ctx.walkInitializer(node) with no push (the stack top is the file or the class/enum), return true. These declarations mint no node, so their initializers are the FILE's or the CLASS's. An initialized_identifier anywhere else — the second variable of a local for (var i = 0, j = n(); …), under initialized_variable_definition — is left to its function's body walk (the double-walk's pass 2a reaches it too, so the parent check is load-bearing). The declaration and program type rows push only the DECLARED type's refs; initializer types come from the walk. 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).
  • getDeclarationWrapper — node's parent is a declaration AND node is that declaration's FIRST named child (firstNamedChild.equals(node); kernel named_child(0) == node — an anon external/static keyword before the signature doesn't count) → the declaration, else undefined. Read by tree-sitter.ts's docstringFor (every docstring lookup, so the kernel's docstring_of wraps all five preceding_docstring calls) and by extractDecoratorsFor's scan #2 (§Decorators); scan #1 and getBodyDocstring keep the node itself. Only Dart defines it — never add declaration to DOCSTRING_WRAPPER_TYPES / docstring.rs is_wrapper, which C/C++ (whose declaration wraps declarators) share.
  • 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). dartCtorInfo reads only the identifiers BEFORE the formal_parameter_list, so a redirecting factory's target (factory Foo() = _Impl.named;) never names it. A constant_constructor_signature / redirecting_factory_constructor_signature that dartCtorInfo rejects (names no enclosing type — error recovery) is skipped too: it can be nothing else.
  • 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. const ctors and redirecting factories → params only too: no retType is looked up for them (a redirect's = _Impl type_identifier is its target, not a return type).
  • 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 (const and redirecting ones included) → 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; a part_directive holds its uri directly), quotes stripped via .replace(/['"]/g,'') (dartUriText). Returns {moduleName, signature} (no handledRefs) → import NODE (name = the URI, e.g. package:torture/other.dart, dart:async, src/reexported.dart, x.g.dart) + the generic imports ref (tree-sitter.ts:3183-3194) {from: file node (no namespace exists), name = URI, line/col of the directive node — at its first annotation when it has one}. 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 'x.g.dart'; is an import since 2026-10-06 (signature part 'x.g.dart';): the resolver reads its URI by the import/export rules (dartDirectiveFile), so the library's file links the part's file and a change to the part reaches the library's dependents. part of and library (part_of_directive, library_name) are in NO type list → invisible: the library's part edge already joins the two files. 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 and body-walked for its constant; a field's or top-level variable's initialized_identifier consumed and body-walked for the class/file (§Constants); handled → scanFnRefSubtree (skipping what was walked) + 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 type_alias node named by the resolveName hook (§Extractor config); getChildByField(node,'value') → null (no fields), so the dart branch — pushDartTypeAliasRefs / type_alias_refs, since 2026-10-21 (#2327; nothing before) — emits a references ref from the node for each type named in every named child but the name and annotations, through the isDartTypeName gate (typedef MapAlias = Map<String, WidgetT> → Map, WidgetT). A type parameter in scope is skipped unless written p.T: the typedef's own, and those of any node that declares type_parameters (a generic function_type, an old-style generic function-typed formal_parameter) within it. Returns false → children re-visited (they match nothing type-related; an annotated typedef's arguments are fn-ref capture points)
import_or_export, part_directive importTypes:1209 → extractImport:3170 §Extractor config. No skipChildren: the directive's children are still recursed, so an annotated directive's arguments are fn-ref capture points (@Tag(f) import 'a.dart'; captures f). The kernel arm recurses too — since 2026-10-06; before, its import_or_export arm returned and missed those (TortureDirectives.dart pins both)
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, constant_constructor_signature and redirecting_factory_constructor_signature hit methodTypes; each field initialized_identifier hits the hook (body-walked for the class); 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_of_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', '', …) — 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, extension_type containers, part of/library/deferred imports. (Named const constructors and redirecting factories mint methods since 2026-10-06, and a part directive an import; the unnamed constructors still mint nothing.) Initializers are code: every initializer is walked with the body walker — a constant's for the constant (riverpod's final repoProvider = Provider((ref) => Repository(ref.watch(dioProvider))); → calls Provider, Repository, ref.watch from repoProvider), a field's (instance, static var, typed, late) for its class, a top-level var/typed/late final one for the FILE. The full body matrix applies: calls, instantiates, static-member and member reads, type refs, closures with block bodies (a local function inside one mints a function under the constant, with the double-walk's pass 2b on its body), interpolation calls, fn-ref candidates. Each is walked ONCE: the hook consumes the entry, so the visitNode recursion that used to reach new_expression / maybeCaptureFnRefs inside a field initializer no longer does.

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. The FIRST type_identifier: const p.X.named() → p.named (prefix bug, preserved)
BlocProviderT<CounterT>.value(…); as a statement args selector; prev = .value selector; accessorPrev = a selector holding only type_arguments, whose prev is the identifier BlocProviderT.value (dartTypeArgumentsReceiver, since 2026-10-06 — bare value before); the .value().tail re-encode reads it the same way
=> BlocProviderT<CounterT>.value(…), and as an initializer, argument or return value constructor_invocation (type_identifier [. type_identifier] type_arguments . identifier arguments) BlocProviderT.value at the node position — the LAST type_identifier, so p.X<T>.named() → X.named (since 2026-10-06; nothing before)
generic<int>(5) args selector (type args ride argument_part) bare generic
ref.read<Repo>(p), Provider<int>((ref) => 0), X<T>(name: v) parsed as two comparisons, (ref.read < Repo) > (p) (§Misparsed generic calls) the < relational_operator (dartMisparsedGenericCall) the parsed call's ref — ref.read / Provider / X, at the <
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
final p = Provider((ref) => Repo(ref.watch(d))), static var s = make(), final _c = Controller(); in a class, var v = load(); the hook's walkInitializer (§Constants) the full matrix, from the constant / the class / the file
ctor initializers (: size = seed()), enum-constant args (ok(200)), default param values never body-walked NOTHING
tester // + newline + .state(x), box.grow /* by */ (3) comments are named SIBLINGS between the chain's parts tester.state / box.grow — every sibling step skips comment and documentation_comment (dartPrevNamed / dartNextNamed; kernel prev_named / next_named), so a comment changes nothing

Every sibling step along a member chain goes past comments: the callee before an argument part, dartReceiverOf / receiver_of, dartCalleeOfArgPart's two, dartTypeArgumentsReceiver's type, and dartMemberRead's receiver and its next-selector check. tree-sitter-dart keeps a comment as a named node wherever it is written, so before (2026-10-06) a comment between a member and its arguments lost the call and emitted a member read, a getter read or static access after a comment (Config //, dart format's line-break idiom) was lost, and a call after one kept only its bare name. The misparsed-generic-call layout checks below still read the raw siblings: a comment there means the code is not laid out as a call.

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.

Misparsed generic calls (dartMisparsedGenericCall / misparsed_generic_call)

tree-sitter-dart parses a generic call with arguments as two comparisons about as often as it parses it as a call — the GLR tie goes either way with the code around it: final a = ref.watch<int>(p);, ref.read<Repo>(p);, int g(Ref ref) => ref.read<int>(1); and Provider<int>((ref) => 0) come out as relational_expression(relational_expression(<callee> < <type>) > <args>), while print(ref.watch<int>(p) + 1), obj.a.b<Repo>(p) and ref.read<Repo>(a, b) parse as calls (2,853 nests vs 2,836 parsed generic calls with arguments across bloc, riverpod and flutter/samples). The args are a parenthesized_expression, or a record_literal when named (DropdownButton<String>(items: …)); the type part is identifier, a .Name selector (p.Repo) and/or a type_arguments-only selector (Map<K, V>); the callee part sits under unary_expression / await_expression for await and prefix operators. Recovered at the < when the type names a type (/^[_$]*[A-Z]/ or int double num bool dynamic void), < touches the callee and ( touches the >; a < B > (c), a<b>(c) and a <B>(c) stay comparisons, and so does a.b /* c */<B>(c): the comment is a sibling of its own against the <, so it is rejected as the callee (else the gate passed with no call, and the comment-skipping member read was suppressed). The recovered form emits what the parsed call does:

  • calls at the < (the parsed call's argument_part starts there);
  • references <Type> for the type argument, via isDartTypeName (§Type refs); the static-member path skips Map in <Map<K, V>> so it is not doubled;
  • no member read of the callee (ref.read, also under await) or of a prefixed type (p.Repo).

A call chained on a recovered one keeps its bare name (increment in BlocProvider.of<CounterCubit>(context).increment()), unlike the parsed chain's BlocProvider.of().increment. The resolver gives both the same method: a bare link is typed from the chain written before it, and so is an encoded chain whose inner call declares no type of its own to return — a generic factory's type parameter (static T of<T>) or a factory outside the project (#750). Nothing is gained by re-encoding the recovered chain.

The grammar can also end an arrow function in front of a generic call: (ref) => ref.watch<int>(p) → ((ref) => ref).watch<int>(p), in the relational and the selector form alike. dartReceiverOf / receiver_of read a receiver in that place from the end of the arrow's body (Dart writes a member access on an arrow itself in parentheses), for callee names and static-member refs: ref.watch, BlocProvider.of. dartCalleeOfArgPart / callee_of_arg_part do not, so () => BlocProvider.of<CounterCubit>(context).increment() keeps increment bare, which resolves as above.

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 the receiver (dartReceiverOf: the previous named sibling past comments) 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. visitNode contexts → never called; initializers are body-walked, so a read written in one emits from its constant / class / file.

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; so do const ctors. A redirecting factory walks its named children one by one and skips a DIRECT type_identifier that is not UpperCamel (/^[_$]*[A-Z]/): after = those are an import prefix (= p.Impl) or the target constructor's name (= _$QuestionImpl.fromJson, where fromJson is a type_identifier) — references _$QuestionImpl only.
  • 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 function or method they decorate (inside program / class_body); a class-like declaration's annotations open its own node instead (scan #1). 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 for functions and methods (no modifiers node either). A class-like declaration is different: class, mixin, extension and extension type nodes START at their first annotation, which is the node's own leading child, so scan #1 emits it (@immutable class Foo → decorates immutable). Enums and typedefs are built the same way, but nothing scans them.
  • Scan #2 (preceding siblings :5002-5023): walk BACKWARD from the declaration; annotation is in the accepted set (:5017); step over comment / documentation_comment siblings (since 2026-10-06, below); stop at the first other non-annotation sibling. consider(): target = first namedChild of accepted types → the identifier (override, deprecated, pragma, immutable, Deprecated) or scoped_identifier (meta.immutable, Foo.named, p.Foo.named) → <-strip + last-.-segment (@meta.immutable → immutable, @p.Foo.named(…) → named; the strip runs first, so @Foo<int>.named(…), which parses with an ERROR inside the name, → Foo) → decorates ref {from the decorated node, name, line/col of the ANNOTATION node, i.e. its @}. 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 docstring walk steps OVER annotations since 2026-10-06 — §Docstrings.)
  • Comments between the annotations and the declaration are stepped over (since 2026-10-06). An annotation always belongs to the next declaration, so dart.ts sets decoratorStepOverTypes: ['comment', 'documentation_comment'], which scan #2 skips, and the kernel's extract_decorators_for skips the same DECORATOR_STEP_OVER list. No other language sets one. Until then the comment ended the scan and the annotation above it attached to nothing. Now these all attach: @override // ignore: must_call_super void f(), @x /// Doc below. void f(), @x /* c */ void f(), a comment on the annotation's own line, and comments between stacked annotations (still in reverse order). Pinned in TortureAnnotatedComments.dart and dart-annotation-comments.test.ts.
    • Still ends the scan, comments or not: a field's declaration, a top-level variable, an import, the previous member's body (void a() {} // note @x void b() gives b only x), a class-like declaration.
    • Class-likes are unaffected. Their leading annotations are scan #1's, which reads every child, comments among them included.
    • Validation (bloc @b9be1e2 / riverpod @4ba1be2, kernel loaded, on top of the annotation resolution below): decorates refs +7 / +54, edges +0 / +16, nothing else in the dumps changed. bloc's 7 are all @override. riverpod's 54 (on 48 declarations) are @override 32, @internal 6 and @riverpod 16 (lint fixtures and one website snippet), and each @riverpod links const riverpod. Kernel and wasm full-index dumps are byte-identical, and the parity sweeps show 0 diffs.
    • The AST scan's 55th riverpod site is @override // ignore: … on a FIELD, which no extractor records. A scan that also counts annotation → comment runs INSIDE class-like nodes finds riverpod's 24 @riverpod // ignore: … class X lint fixtures and bloc's @JsonEnum(…) /// … enum. Scan #1 already reads those (an enum's, never: see above).
  • Members with no body (declaration-wrapped ctors, const ctors, redirecting factories, abstract and external members): the signature's PARENT is the declaration, so until 2026-10-06 the backward scan ran over the declaration's own children (the signature is namedChild(0) → declIdx 0 → nothing scanned) and an annotation before the declaration attached to NOTHING (@visibleForTesting Foo._();, @protected void m();). Now scan #2 starts at getDeclarationWrapper's declaration: same backward walk, same reverse order, and a field's declaration in between still stops it. Annotated fields themselves emit nothing (no extractor runs). Validation (bloc @b9be1e2 / riverpod @4ba1be2): +10 / +186 decorates refs, all SDK or package:meta names (useResult, override, internal, visibleForTesting, …), so no edges; before #2380, riverpod's 23 new @internal refs on X.internal(…) ctors resolved to the ctor itself.
  • Resolution is TS-side and Dart-specific (since 2026-10-06): resolveOneInner hands every Dart decorates ref to name-matcher.ts's matchDartAnnotation before any other strategy, and nothing falls through. A Dart annotation is a const variable or a const constructor call, never a method, getter or function, so the shared decorator ranking (findBestMatch: +25 function/method, +15 class, nothing for a constant) never sees one. That ranking sent riverpod's 506 @riverpod to the analyzer's extension getter RiverpodAnnotatedAnnotatedNodeOfX::riverpod (0.4) instead of const riverpod = Riverpod();.
    • The matcher re-reads the written form from the @ in comment-blanked source. It is a call only when ( follows on the same line, so a record return type on the next line is not taken for arguments.
    • @x: on a member, the enclosing type's own static constant / enum_member x (a class-like's own annotations stand outside its body); else a top-level constant the library can see (#2386's model, the own library shadowing imports). @X(…): a visible class X, not an extension.
    • @p.x / @p.X(…), when p is an import prefix (dart-libraries.ts dartImportPrefixes, the imports of the file's whole library): the same through that prefix. Otherwise @T.x → T's own constant / enum_member x, and @T.named(…) → T's named constructor (#2380's isDartConstructor); @p.T.x / @p.T.named(…) likewise through p.
    • A sole candidate links at 0.9 (exact-match, or qualified-name through a type). Several (only where the library model can't establish the library) fall back to findBestMatch's proximity at 0.7 / 0.4.
    • Not modelled: a final is not told apart from a const (compiling code never annotates with a visible final), nor a typedef used to call a constructor.
    • Validation (main 99246848, kernel loaded): bloc 0 edges changed (no annotation there names a project declaration); riverpod exactly the 506 @riverpod edges retargeted to const riverpod at 0.9, every other decorates edge ($internal, Riverpod, Dependencies, …) identical, nodes, refs and files identical. Every extracted annotation in both repos is written with one name, so the prefixed and T.x forms are pinned by dart-annotation-targets.test.ts only.

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; instead docstringFor starts from getDeclarationWrapper's declaration for a member with no body (until 2026-10-06 those looked inside the wrapper and found nothing, e.g. const BlocProvider.value(…)'s dartdoc; validation added 32 docstrings on bloc and 126 on riverpod, none changed or removed).

The walk steps OVER annotation siblings (since 2026-10-06). Until then an annotation between the comment run and the declaration ended the walk: /// Builds it. @override Widget build(…) lost its dartdoc, the dominant real-world loss since @override is everywhere. Now dart.ts sets docstringStepOverTypes: ['annotation'], which docstringFor hands to getPrecedingDocstring's stepOver. The kernel's docstring_of calls docstring.rs's preceding_docstring_stepping_over(…, &["annotation"]). preceding_docstring and every other language keep the empty list.

  • Stepped over: stacked annotations, multi-line ones (@Deprecated( … )), inline ones (/* c */ @x void f(), freezed's @optionalTypeArgs TResult maybeMap… on the declaration line), and blank lines, which are not nodes. This works from a declaration wrapper too.
  • Comments on either side of an annotation keep the join semantics: /// a @x // b void f() → a\nb (was b), and /// Doc. @override // ignore: … → Doc.\nignore: ….
  • Still ends the walk: any other named sibling, such as the previous member's function_body, a field's or a bodiless member's declaration, a top-level variable (static_final_declaration_list / initialized_identifier_list), an import_or_export or part_directive, or a class-like declaration. A comment trailing the previous member on its line (void a() {} // note) is collected, as it already was for a member with no annotation.
  • Class-like declarations needed nothing. A class, mixin, extension, extension type, enum or typedef node starts at its first annotation (§Decorators), so the dartdoc above the annotations is the node's previous sibling and was always kept. A comment written BETWEEN such an annotation and the keyword (@JsonEnum(…) /// The state… enum LinterRuleState) is inside the node and is NOT read (pinned). bloc has 1 such dartdoc, riverpod none (23 // ignore: lines and 1 plain comment). A member's comment below its annotation IS read: it is the signature's previous sibling.
  • Local functions get no docstring with or without annotations: the signature sits in local_function_declaration > lambda_expression, with no previous sibling.
  • Validation (bloc @b9be1e2 / riverpod @4ba1be2, 3f96f80f plus the declaration-wrapper fix, kernel loaded): docstrings +26 / +1,072, changed 0 / 9 (each a // ignore: line below the annotation, now joined after the dartdoc above), removed 0. Everything else in the dumps (nodes, edges, refs, files) is byte-identical; kernel and wasm full-index dumps are byte-identical; the parity sweeps show 0 diffs. bloc's 26 are all hand-written dartdoc, mostly behind @mustCallSuper / @override. Of riverpod's 1,072, 667 runs hold a dartdoc (401 in generated .freezed.dart / .g.dart files, 266 hand-written) and 405 are plain comments only (// ignore: riverpod_lint/… in the lint fixtures, the website's snippet markers).

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 ✓ (from the constant aliasTop — the hook walks the declaration with it pushed); 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, and every initializer the hook walks — from the constant / class / file, lambdas included), visitNode:990 (the §double-walk twins from file/class scope), scanFnRefSubtree (only a static_final_declaration whose constant wasn't minted — the scan skips what the hook walked, so nothing is captured twice; it halts at function_expression :609).
  • 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.4–20.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 ~79–97% 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 — an import since 2026-10-06, pinned with every directive form in TortureDirectives.dart, and part of by library name in TorturePartOfName.dart); doc shapes (/// run, /** */ kept, // kept, a /// above an annotation); 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 until 2026-10-21, the aliased types since; every form is in TortureTypedefs.dart); extension type (container invisible, members leak as bare functions); 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.