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>
92 KiB
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, "Fixsetandgetcontextual 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 tosrc/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, notree-sitter generate— the shipped bytes ARE the reference.copy-assetsalready globssrc/extraction/wasm/*.wasm. MIT license. -
External scanner: YES —
src/scanner.cPRESENT 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.c5a42b47abb4d494f125dbdee9138979248041689b1aa36355550fa3e28dcb8b8src/scanner.c07a7b7818b175e9460523e705dd88d20f7b5141bac95c593d4426e6d52284996src/tree_sitter/parser.h180b893c8734778fd32f372dfbc27bd6ad1cd2221f26150b31256ff6716320d2src/tree_sitter/alloc.hb29c1c9fb7cc82f58c84b376df1297d6e2737a1d655fd356db0859e3c29c2feasrc/tree_sitter/array.h5bdf6ed1a78e3409fd443e085ca967a64c188a5d082aaf7f819bccd53a471c94
-
crates.io
tree-sitter-dart0.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/; extendcodegraph-kernel/build.rs's cc::Build to compile both C files with the grammar's own flags (its checked-inbindings/rust/build.rsuses-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:39GRAMMAR_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_toolskipped):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 inconstant_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) unnamedlibrary;(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.dartfiles (bloc'sbricks/{{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
- No preParse, no POST_PASS.
dartExtractorhas nopreParsehook (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 →tryKernelExtractRawstays eligible. - No dart framework resolver exists (grep src/resolution/frameworks/ —
nothing lists dart; no Flutter resolver). BUT the universal
vueandastroresolvers (nolanguageslist ⇒ applicable to every language, frameworks/index.ts:110-118) carryextract()hooks → in a repo where vue/astro is DETECTED, parse-worker.ts:92-99 forces.dartfiles onto the decodedextractFromSourcepath (their extract() hooks no-op on.dartcontent — 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. - One walker module (suggest
codegraph-kernel/src/dart.rs), registered in langs.rs; per-filehas_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)extractBareCallselector-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). - 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 nodedecoratorswire field (layout.ts:56) is never populated for dart (noextractModifiershook — annotations aredecoratesREFS only). The nodesignaturefield (layout.ts:55) is heavily used (functions/methods AND hook-minted constants). - 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. - 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
packageTypeshook —library foo.bar;is invisible); interfaces don't exist as a dart concept (abstract classes are kindclass).
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/ bareconstructor_signature) whose body is the next named siblingfunction_body.function_bodywraps either ablockor a bare=>expression (arrow bodies have no block child). Theasync/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 unfieldedtype_identifier/void_typeBEFORE the name).method_signaturehas NO fields (its single child is the inner signature).getter_signature.name/setter_signature.name✓.constructor_signaturehasnamefields on BOTH identifiers (name: Widget,name: named— childForFieldName('name') returns the FIRST = the class) +parameters:field.factory_constructor_signaturechildren 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 namedmixinkeyword node, identifier, on-types as bare type_identifiers, class_body).enum_declaration:name:+body:(enum_body);enum_constant.name✓.type_aliashas NO fields (both moderntypedef X = Tand legacytypedef void X(int)are nodetype_alias). Its name is the last directtype_identifierbefore the=,type_parametersorformal_parameter_listchild: 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_namenow, via resolveName).initialized_variable_definition:name:+ one-or-morevalue: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)+ siblingfunction_body, both direct children of class_body. BODILESS constructors (Widget(this.size);,Widget._() : x = 0;) wrap in adeclarationnode instead (declaration > constructor_signature [+ initializers]) — reached by the walker via plain recursion intodeclaration.constconstructors aredeclaration > constant_constructor_signatureand redirecting factories aredeclaration > 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 atype_identifierwithout type arguments and anidentifierafter them). Until 2026-10-06 neither was in methodTypes (no node, no refs); now both mint methods exactly like the bareconstructor_signature(§isMisparsedFunction, §resolveName). Abstract andexternalmembers aredeclaration > function_signaturetoo (an anonexternal/statickeyword may precede the signature). In every one of these the member's dartdoc and annotations are siblings of thedeclaration, not of the signature — §getDeclarationWrapper. Fields aredeclaration > (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 eachselectorwrapsunconditional_assignable_selector(.name) /conditional_assignable_selector(?.name) /argument_part > arguments > argument*.new Foo(1)IS a singlenew_expression(children: unfielded type_identifier + arguments).const Foo.bar(1)in EXPRESSION position is aconst_object_expression; in aconst x = …declaration the const rides the declaration and the value is a plain flat chain. Cascades arecascade_section > cascade_selector + argument_part(NO selector node → invisible to everything, §Calls). Assignments areassignment_expressionwithleft:(anassignable_expression) andright:fields. Locals arelocal_variable_declaration > initialized_variable_definition(even without initializer). Lambdas arefunction_expression(children incl.body: function_expression_body). String templates:string_literal > template_substitutionwrapping either a full expression (${…}) or anidentifier_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 typeidentifier; if present: valueNode =nameNode.nextNamedSibling(the FIRST value child only — a flattened chain likeWidgetT(0)captures justWidgetT; a wrapped expression likeSHARED_MAX + 1captures 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 runsscanFnRefSubtree(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 aninitialized_identifier_listdirectly underprogram, or under adeclarationin 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. Aninitialized_identifieranywhere else — the second variable of a localfor (var i = 0, j = n(); …), underinitialized_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). Thedeclarationandprogramtype 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= afinal/constdeclaration WITH an initializer that is top-level (const SHARED_MAX = 10;,final typedTop = compute();, typed or untyped, incl. multi-declarationsfinal multiA = 1, multiB = 2;→ one node per list entry) or class-level WITHstatic(static const int K_MAX = 9;,static final sharedInst = WidgetT(0);). Instancefinal untyped = 5;/final int size;/ anyvar/typed-var member →initialized_identifier→ NO NODE (dart emits zerofieldnodes, ever). Top-levelvar 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.nextNamedSiblingif it's afunction_body, else null (NOTE: a bareconstructor_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 theirinitializerssibling is left to plain visitNode recursion). For class/mixin/extension nodes: standardbodyfield first (class_definition ✓, extension_declaration ✓), else the first namedChild of typeclass_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.namedL123-125 spans its body). - getDeclarationWrapper — node's parent is a
declarationAND node is that declaration's FIRST named child (firstNamedChild.equals(node); kernelnamed_child(0) == node— an anonexternal/statickeyword before the signature doesn't count) → thedeclaration, else undefined. Read by tree-sitter.ts'sdocstringFor(every docstring lookup, so the kernel'sdocstring_ofwraps all fivepreceding_docstringcalls) and by extractDecoratorsFor's scan #2 (§Decorators); scan #1 andgetBodyDocstringkeep the node itself. Only Dart defines it — never adddeclarationto DOCSTRING_WRAPPER_TYPES / docstring.rsis_wrapper, which C/C++ (whosedeclarationwraps 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); prefixedother.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 leavesm()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) andreduce≠Action→ treated as the method it is (pinned:extract-probe4.txt— methodreduce, position starting ATreduce, 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. Aconstant_constructor_signature/redirecting_factory_constructor_signaturethat 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 namedChildtype_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 returnFuture<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.constctors and redirecting factories → params only too: no retType is looked up for them (a redirect's= _Impltype_identifier is its target, not a return type). - getVisibility (:209-222) — method_signature → unwrap to
function/getter/setter signature → its first
identifierchild; 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 thenameFIELD 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 →_Privateclass 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*andsync*→ FALSE (different token text → different anon type — generators are not "async"); bodiless (external/ abstract) → false. Note the anon child's TYPE is exactlyasync(probed via behavior). - isStatic (:234-243) — node.type === 'method_signature' → scan ALL
children for type
'static'→ true; else false.static WidgetT make()→ true ✓ (thestatickeyword is an anon child of method_signature); bare constructor_signature → false. Top-level functions → false. - resolveName (:244-260) — dartCtorInfo: named ctor/factory (
constand 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 underlibrary_export; apart_directiveholds itsuridirectly), 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 genericimportsref (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/hidecombinators: IGNORED (not read).import 'x.dart' deferred as y;is INVISIBLE — the deferred form's import_specification holds a bareuri(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 (signaturepart '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 ofandlibrary(part_of_directive, library_name) are in NO type list → invisible: the library'spartedge 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); nestedfunction_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; QNlocalFnat top level,Holder::methodLocalin 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_declarationnever 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
hostFnwith locallocalFn { inner(n); }and locallocalWithNew { new Holder(1); }: nodeshostFn::localFn(contained by hostFn) … thenlocalFn(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
useNewwith a localmethodLocal { h.touch(); }andnew 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 nodeHolder::useNew::methodLocal+ duplicateHolder::methodLocal(kind function both, same id). - fn-ref twins:
register(seedValue)in a top-level fn →function_ref seedValue from=refTakerANDfrom=file:…(flush order: all in-scope candidates first, then the revisit's file/class twins — capture order); in a class method →from=method:wireANDfrom=class:Htwins (fnref2.dartpin). - 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 = literalfile:${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
(
namedL123→125;bodyShapesL60→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; membersWidgetT::render; nestedWidgetT::useNew::methodLocal(and the duplicate's shallowerWidgetT::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):
externalFnstarts col9 (theexternalkeyword sits OUTSIDE function_signature); the@override (T) m()misparse starts atm(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 firstidentifierchild) → 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
functionnode with class-prefixed QN (pinnedAbstractT:: 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; anonymousextension on String→ the extractName fallback finds the FIRST type_identifier = the ON type → a class node namedString(pinned — members becomeString::anonExt). The extended type is otherwise INVISIBLE (no ref — extractInheritance matches nothing on extension/mixin nodes). - Mixins:
mixin MixA on BaseT→ classMixA; theontypes are bare type_identifier children matching NO inheritance clause → no refs.mixin MixB implements DrawT→ theinterfaceschild DOES match → implements DrawT ✓. - extractEnum (:1914): body REQUIRED (enum_body via
bodyfield — present). extras {docstring, visibility, isExported undefined}. extractInheritance on enum_declaration:interfaceschild → implements ✓; themixinschild is a DIRECT child of enum_declaration (no superclass wrapper) and matches NO clause →enum StatusT with MixBemits NOTHING for the mixin (pinned — the class-side mixin handling lives inside thesuperclassbranch 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')— thenamefield EXISTS on enum_constant → ONE enum_member node per constant, positioned at the ENUM_CONSTANT node (extent coversok(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):
superclasschild (class_definition only) → the dart branch (:5368-5393): for each namedChild of the superclass node — directtype_identifier→extendsref; amixinschild → oneimplementsref pertype_identifierinside.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_argumentsa sibling →extends Baseclean. Position = each type_identifier.interfaceschild (:5437-5459, class + enum): targets = its namedChildren (no type_list in dart) → oneimplementsper child, name = FULL child text (children are type_identifiers; a genericimplements Comparable<T>would ride the type_identifier + sibling type_arguments shape → nameComparable).- 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_definitionbranch (: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 (
ontypes 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:
callsat the<(the parsed call's argument_part starts there);references <Type>for the type argument, via isDartTypeName (§Type refs); the static-member path skipsMapin<Map<K, V>>so it is not doubled;- no member read of the callee (
ref.read, also underawait) 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.loadselector 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 = lowercaseutil; later selectors' prev = selectors).this.x→ prev is athisnode → nothing. Case patterns (case ColorT.blue:) →constant_pattern > qualifiedshape, 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.voidis avoid_typenode → 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→ referencesotherANDOtherClass(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 doconstctors. 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, wherefromJsonis a type_identifier) —references _$QuestionImplonly. - Getters: references from the getter's TYPE (suppressed if builtin —
int get area→ nothing;WidgetT get w→ references WidgetT). - extractVariableTypeAnnotation (:6074) needs a
type_annotationchild — no such node type in this grammar → dead; the body-walkervariable_declaratorbranch (:5230) — no such node type → dead. LocalWidgetT w = …;, field types,is/astypes, 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
modifiersnode 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→ decoratesimmutable). Enums and typedefs are built the same way, but nothing scans them. - Scan #2 (preceding siblings :5002-5023): walk BACKWARD from the
declaration;
annotationis in the accepted set (:5017); step overcomment/documentation_commentsiblings (since 2026-10-06, below); stop at the first other non-annotation sibling. consider(): target = first namedChild of accepted types → theidentifier(override,deprecated,pragma,immutable,Deprecated) orscoped_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) →decoratesref {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()→ decoratespragmaFIRST, thenDeprecated(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'sextract_decorators_forskips the sameDECORATOR_STEP_OVERlist. 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_supervoid 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()givesbonlyx), 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):
decoratesrefs +7 / +54, edges +0 / +16, nothing else in the dumps changed. bloc's 7 are all@override. riverpod's 54 (on 48 declarations) are@override32,@internal6 and@riverpod16 (lint fixtures and one website snippet), and each@riverpodlinksconst 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 Xlint fixtures and bloc's@JsonEnum(…)/// …enum. Scan #1 already reads those (an enum's, never: see above).
- Still ends the scan, comments or not: a field's
- Members with no body (declaration-wrapped ctors, const ctors, redirecting
factories, abstract and
externalmembers): the signature's PARENT is thedeclaration, 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'sdeclaration: same backward walk, same reverse order, and a field'sdeclarationin between still stops it. Annotated fields themselves emit nothing (no extractor runs). Validation (bloc @b9be1e2 / riverpod @4ba1be2): +10 / +186decoratesrefs, all SDK or package:meta names (useResult,override,internal,visibleForTesting, …), so no edges; before #2380, riverpod's 23 new@internalrefs onX.internal(…)ctors resolved to the ctor itself. - Resolution is TS-side and Dart-specific (since 2026-10-06):
resolveOneInner hands every Dart
decoratesref to name-matcher.ts'smatchDartAnnotationbefore 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@riverpodto the analyzer's extension getterRiverpodAnnotatedAnnotatedNodeOfX::riverpod(0.4) instead ofconst 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 staticconstant/enum_memberx(a class-like's own annotations stand outside its body); else a top-levelconstantthe library can see (#2386's model, the own library shadowing imports).@X(…): a visible classX, not an extension.@p.x/@p.X(…), whenpis an import prefix (dart-libraries.tsdartImportPrefixes, the imports of the file's whole library): the same through that prefix. Otherwise@T.x→ T's ownconstant/enum_memberx, and@T.named(…)→ T's named constructor (#2380'sisDartConstructor);@p.T.x/@p.T.named(…)likewise throughp.- A sole candidate links at 0.9 (
exact-match, orqualified-namethrough 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
finalis not told apart from aconst(compiling code never annotates with a visiblefinal), 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@riverpodedges retargeted toconst riverpodat 0.9, every otherdecoratesedge ($internal,Riverpod,Dependencies, …) identical, nodes, refs and files identical. Every extracted annotation in both repos is written with one name, so the prefixed andT.xforms are pinned by dart-annotation-targets.test.ts only.
- The matcher re-reads the written form from the
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 adeclarationwrapper too. - Comments on either side of an annotation keep the join semantics:
/// a@x// bvoid f()→a\nb(wasb), 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'sdeclaration, a top-level variable (static_final_declaration_list/initialized_identifier_list), animport_or_exportorpart_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,
3f96f80fplus 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.dartfiles, 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 ONLYconstant(kind variable never occurs) → targets = hook constants under file: or class: (an enum-scopedstatic constwould 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 typefunction_body(orblock), 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: pinnedDERIVED_VAL → SHARED_MAXedge); 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 firstidentifier-typed namedChild. Uninitialized locals (int DERIVED;) still bump (pinned prune); a method-localfinal SHARED_MAX = 1;prunes the file-wide target (pinned).assignment_ expressionis NOT a prune case (the :829assignmentcase is python's — the swift-lesson check comes out DEAD for dart): an assignment-only rebind never bumps, andlow = 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_identifiernever occur in dart trees). Any identifier text mapping to a live target emits — including the member half ofTable.COL_LIMITnavigation (pinned) and${SHARED_MAX}interpolations (template_substitution > identifier).$SHARED_MAX(no braces) isidentifier_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 isnamed_argument— not in layers, not an idType → NOTHING (pinned,namedarg.dart). Flutter'sonPressed: handleridiom is therefore invisible — bug-for-bug, do NOT "fix" in the port (candidate future accuracy PR: addnamed_argument/labelhandling 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 = assignedkept (cb ≠ assigned), a hypotheticalthis.cb = cbskipped.cb = selfStorewhere 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 constantaliasTop— the hook walks the declaration with it pushed); a LOCALfinal 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_declarationwhose constant wasn't minted — the scan skips what the hook walked, so nothing is captured twice; it halts atfunction_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 methodagates any bare arga— pinned false positive, PRESERVE) and<anonymous>. Nothis./::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 asfunctionnodes (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.txtdiff-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'gmstrips →js_multiline_stripin docstring.rs. Multi-line signature slices and ≥100-char initializer captures include raw\rbytes 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 capitalizedcalls WidgetTrefs resolve as constructions; depends on ctor calls staying PLAIN calls refs (dart emitsinstantiatesONLY fornew_expression).matchDottedCallChain(name-matcher.ts:2147-2166, dart listed) — consumes theFoo.create().runre-encode + the method nodes'returnType(extractDartReturnType's class-name-for-ctors rule is what makesWidgetT.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 vsCODEGRAPH_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 kindmust 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 withCODEGRAPH_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
__tests__/fixtures/kernel-parity/torture.dart— seed from the survey'ssvy-dart/torture.dart(231 lines, parses clean; itsextract-torture.txtis the expected-output pin). Inventory by branch: imports (dart:, package: withas+show, export withhide, deferred → invisible, part → invisible — an import since 2026-10-06, pinned with every directive form in TortureDirectives.dart, andpart ofby 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 →otherbug,Tleak, void); signature matrix (params verbatim incl. named/optional/defaults/ function-typed, getter type-only, setter params-only, generic-returnFuture ()); 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; mixinon(nothing) + mixin implements; named + anonymous extension (class named after the ON type); abstract class (bodiless members → kindfunction; abstract getter invisible); sealed/base modifiers (plain classes); enum (simple + enhanced: ctor'd constants at constant positions, args unwalked,withGAP vsimplements✓, 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 barefunctions); the FULL call matrix (§Calls rows verbatim — incl. cascade invisibility,?., const-object args, chain re-encodes, interpolation calls,$xvs${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$Xnon-read;X.membermember-half read; local shadow prune; uninitialized-local prune; assignment non-prune; lowercase non-target); a unicode comment + string before a symbol (UTF-16 columns).
- CRLF variants derived in-memory (kernel-tsjs-parity pattern).
- Defer fixture #1:
switch (x) { Init() => 1, _ => 0 }(empty object pattern) — kernel defers (defer:), wasm serves the recovery output. - Defer fixture #2:
library;header file — same contract. - Generated-file fixture (
x.g.dartnaming) — 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.