R7b batch 4 #4 — the FINAL R7b language (docs/design/dart-kernel-port-checklist.md is the authoritative quirk list). The fourth vendored-grammar-C language, with a twist: production dart resolved its wasm from tree-sitter-wasms, whose dart dependency is an UNPINNED github:UserNobody14/tree-sitter-dart — a routine dependency update would have silently changed dart's grammar. This PR byte-copies the shipping 0.1.13 artifact into src/extraction/wasm/ (VENDORED_WASM_LANGS += dart) and compiles the same-commit (d4d8f3e337d8) parser.c/scanner.c in the kernel — table identity proven by the kernel-grammar-parity row. crates.io tree-sitter-dart is the nielsenko fork (different lineage) — rejected. The center of gravity is THE SIBLING-BODY DOUBLE-WALK, reproduced bug-for-bug: dart attaches every function/method body as a NEXT SIBLING of its signature, and the TS walkers consume each body TWICE — once via resolveBody (attributed to the function/method) and once via the enclosing generic walk (attributed to the file/class). Duplicate local-function nodes with the SAME id under different parents, duplicated calls/instantiates refs, and file/class-attributed fn-ref twins all emit in the exact observed interleave (a dedicated fixture pins the duplicate-id rows; the bloc kind-census spot-check pins the counts). Also preserved (probe-pinned): the extractBareCall selector matrix (the first callTypes=[] language — cascades completely invisible, `?.` encodes like `.`, the `ConfigT.load()` calls+references double emission with no callee-of-call skip, capitalized-chain `Foo.create().run` re-encode, const-object callee names); the constructor hooks (unnamed ctor skipped, named ctors/factories renamed to the CTOR name with the class as returnType, `@override (T) m()` record-misparse rescued by class-name validation); operator methods minting `method "<anonymous>"`; static_final_declaration constants via the visitNode hook while instance fields mint NOTHING; the prefixed-return-type prefix bug (`other.OtherClass f()` → returnType `other`); enum `with` mixins silent vs `implements` working; anonymous extensions named after the ON type; deferred imports invisible; named-argument callbacks NOT fn-ref-captured (the Flutter `onPressed:` idiom — future accuracy PR, TS-side first); `async*`/`sync*` NOT async; value-refs with the LIVE dart sibling-body pull and the `$X`-vs-`${X}` interpolation asymmetry; dartdoc kept in all three comment forms with the annotation-broken chain. Gates: parity sweeps first-run 0-diff on shelf/bloc/flutter — 5,815 clean files byte-parity, deferrals 10/21/1341 ≈ the survey's 10/21/~1340 (both-arm grammar reality: empty object patterns — the sealed-class idiom — and unnamed `library;` dominate; --max-deferral 0.3); full-init dumps byte-identical ×3 (shelf 7,959 / bloc 40,026 / flutter 1,855,319 dump lines); bloc per-kind node census identical across arms (the double-walk duplicate rows survive the store identically); kernel-dart-parity suite (7 fixtures + in-memory CRLF variants + double-walk duplicate-id pin + generated-file skip pin + two defer pins); full suite 2,688 green ×2 with CODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += dart (20 langs — R7b COMPLETE). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
72 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] (:126); interfaceTypes=[] ; structTypes=[];
enumTypes=[enum_declaration] (:129); enumMemberTypes=[enum_constant]
(:130); typeAliasTypes=[type_alias] (:131); importTypes=[import_or_export]
(:132); callTypes=[] (:133 — extractCall NEVER runs for dart; all call refs
ride extractBareCall in the body walker); variableTypes=[] (:134 —
extractVariable never runs); extraClassNodeTypes=[mixin_declaration,
extension_declaration] (:135). nameField=name (:172), bodyField=body
(:173), paramsField=formal_parameter_list (:174 — DEAD, see
§Type-annotation refs), returnField=type (:175 — DEAD likewise).
Grammar-shape facts the config leans on (all probed, mini-cst.txt /
torture-cst.txt):
- Signature/body split: every function and method is a SIGNATURE node
(
function_signature/method_signature/ 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).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— NEITHER is in methodTypes → const ctors andconst factory X.r() = Impl;are INVISIBLE (no node, no refs). 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 }). Return **true** (consumed — even when nameNode missing? NO: name found is required for createNode but the hook returns true for EVERY static_final_declaration reached, node minted or not — transcribe the early-return shape exactly:if (nameNode) {…}; return true). The dispatcher then runsscanFnRefSubtree(node, 0)and never descends → §Function-as-value for what still gets captured. Everything else → false. **Reality of the node type (probed,probe2-cst.txt):**static_final_declaration= 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). - 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). - 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. - 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 → 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), quotes stripped via.replace(/['"]/g,''). Returns {moduleName, signature} (no handledRefs) → import NODE (name = the URI, e.g.package:torture/other.dart,dart:async,src/reexported.dart) + the genericimportsref (tree-sitter.ts:3183-3194) {from: file node (no namespace exists), name = URI, line/col of the import_or_export node}.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/part of/librarydirectives are different node types (part_directive, part_of_directive, library_name) in NO type list → invisible. None of the TS/py/rust/php/ruby binding emitters (:3197-3234) fire for dart. - extractBareCall (:305-379) — §Calls, the full matrix.
Hooks ABSENT (the walker must NOT do these): preParse, recoverMangledName,
classifyClassNode (class_definition is always kind class — abstract/
sealed/base/final/interface-modified classes included), classifyMethodNode,
extractPropertyName, propertyTypes, fieldTypes, getReceiverType
(never diverts extractFunction:1522; no receiver QNs; no owner-contains
:1799 — extension members ride the extension's own class node),
isConst, isExported (undefined everywhere except the file node's
literal false), interfaceKind, extractPackage/packageTypes (no
namespace node — top-level QNs are bare), extractModifiers (node
decorators never set), synthesizeMembers, skipBodilessClass (bodiless
class Base {} still mints — dart classes always have {} anyway),
methodsAreTopLevel, resolveTypeAliasKind (type_alias is always kind
type_alias).
Registration: EXTRACTORS.dart (languages/index.ts:56), FN_REF_SPECS.dart
(function-ref.ts:394).
tree-sitter.ts branches (anchors as of 45a53eb)
visitNode dispatch — what each dart node hits (ladder at 936-1303)
| Node | Branch | Behavior |
|---|---|---|
| every node | visitNode hook first (:943) | static_final_declaration consumed (§Constants); handled → scanFnRefSubtree + STOP |
| every node | maybeCaptureFnRefs (:990) | fires for arguments/assignment_expression/pair/list_literal/static_final_declaration in visitNode context — the source of the file/class-attributed fn-ref twins (§double-walk) |
function_signature |
functionTypes:994 | methodTypes does NOT include it → always extractFunction:1517, even inside a class (isInsideClassLike && !methodTypes.includes → else-arm). In-class function_signatures occur only for ABSTRACT/bodiless methods, reached via their declaration wrapper (declaration > function_signature, probed) → kind function contained by the class (pinned: AbstractT::mustImpl is a function; the sibling declaration > getter_signature abstract getter stays invisible). skipChildren |
class_definition |
classTypes:1005 | no classifyClassNode → always extractClass:1679 (abstract/sealed/base/etc. included) |
mixin_declaration, extension_declaration |
extraClassNodeTypes:1022 | extractClass(node) → kind class |
method_signature, constructor_signature |
methodTypes:1027 | NOT class-gated at the ladder — extractMethod:1737 runs anywhere; its own gate :1747 (not class-like, no methodsAreTopLevel, no receiver) falls back: parent object/object_expression? (never in dart) else extractFunction — this is how extension-TYPE members and any stray non-class method_signature become plain function nodes (pinned: extension type MetersT → function km, bare QN) |
enum_declaration |
enumTypes:1064 → extractEnum:1914 | §Class family |
type_alias |
typeAliasTypes:1071 → extractTypeAlias:2890 | plain type_alias node; getChildByField(node,'value') → null (no fields) → NO refs from the aliased type (typedef MapAlias = Map<String, WidgetT> emits nothing); returns false → children re-visited (function_type/formal_parameter_list children match nothing) |
import_or_export |
importTypes:1209 → extractImport:3170 | §Extractor config |
new_expression |
INSTANTIATION_KINDS:1255 (new_expression ∈ :354-361) |
extractInstantiation:4610 → ctor field lookups null → namedChild(0) = type_identifier → instantiates ref from stack top; <-strip + last-.-segment apply (new p.Foo<T>() → Foo). findAnonymousClassBody → always null for dart. Children still recursed |
function_body (sibling of a consumed signature) |
no branch | recursed → THE DOUBLE-WALK (§below) |
declaration (fields, bodiless ctors) |
no branch | recursed → constructor_signature hits methodTypes; initialized_identifier/list, constant_constructor_signature, redirecting_factory_constructor_signature, initializers, annotations-in-place: nothing |
getter_signature / setter_signature BARE (top level) |
no branch | top-level getters/setters are INVISIBLE (no node; their sibling function_body is visitNode-recursed where calls don't extract) — in classes they're method_signature-wrapped → methods |
const_object_expression, selector, cascade_section, assignment_expression, local_variable_declaration, patterns, extension_type_declaration, part_directive, library_name, lambdas |
no branch | recursed; calls only extract in the BODY walker (extractBareCall is not consulted by visitNode!) — §Calls for the consequences |
property_signature/method_signature TS branch (:1282) |
shadowed | method_signature is consumed at :1027 first; property_signature isn't a dart kind — branch unreachable |
THE DOUBLE-WALK (sibling bodies) — reproduce it exactly
Because bodies are SIBLINGS, every signature-consuming extractor walks its
body via resolveBody, and then the ENCLOSING loop (program level, class-body
loop, or an outer visitFunctionBody) visits the same function_body node
again as an ordinary child. What fires on each pass:
- Pass 1 (visitFunctionBody, attributed to the function/method): the full body matrix — extractBareCall calls, instantiates, static-member refs, nested named functions (:5245), fn-ref capture (:5137).
- Pass 2a (visitNode recursion — program/class-body sibling visit): ONLY
the visitNode-dispatched branches:
new_expression→ instantiates from the FILE (top level) or CLASS (class body); 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, const constructors, redirecting factories, extension_type
containers, part/part of/library/deferred imports. Initializer
side-effects: hook-consumed constants' initializers are NOT walked → no
calls/instantiates from them (only scanFnRefSubtree capture — §fn-refs);
initialized_identifier fields' initializers ARE recursed by visitNode but
only INSTANTIATION_KINDS fires there → int counter = 0; emits nothing,
but a field final w = new Widget(); would emit instantiates Widget from
the CLASS (and a top-level var w = new Widget(); from the FILE). No
static-member refs from any of these contexts (body-walker only).
Calls — extractBareCall (dart.ts:305-379) in the body walker (:5159-5173)
Only visitFunctionBody consults extractBareCall; the ref is {from: nodeStack top, name, line/col of the MATCHED node (the selector/const-object node — NOT the chain head)}. The dart matrix (all pinned in extract-torture.txt / extract-mini.txt):
| Source shape | Matched node | Emitted calls ref |
|---|---|---|
helper(count) |
selector>argument_part; prev = identifier | helper at the selector position |
WidgetT(1) |
same | WidgetT (constructor = plain capitalized call; the resolution side's CONSTRUCTS_VIA_BARE_CALL — name-matcher.ts:918 — depends on exactly this shape) |
obj.method(x) |
args selector; prev = .method selector; accessorPrev = identifier |
obj.method (receiver kept, lowercase or not) |
ConfigT.load() |
same | ConfigT.load + a references ConfigT static-member ref from the .load selector (§Static-member) — the double emission |
WidgetT.named(3) |
same | WidgetT.named + references WidgetT |
other.OtherClass() (prefixed ctor) |
same | other.OtherClass |
this.own() / super.parent() |
args selector; prev = .own selector; accessorPrev = this/super node (not identifier) |
bare own / parent |
a.b.call3(x) (2+ hops) |
accessorPrev = another non-argpart selector | bare call3 |
FactoryT.create().run() |
outer args selector; accessorPrev = inner argument_part selector | FactoryT.create().run — the #750 re-encode via dartCalleeOfArgPart (:100-116): innerCallee FactoryT.create is /^[A-Z]/ → `${innerCallee}().${method}`; plus the inner FactoryT.create ref from its own selector; plus references FactoryT |
WidgetT.named(3).chainTail() |
same | WidgetT.named().chainTail + WidgetT.named + references WidgetT |
lower().chain() |
innerCallee lower lowercase |
bare chain + lower |
xs.map((e) => …).toList() |
innerCallee xs.map lowercase |
bare toList + xs.map |
w?.render() |
conditional_assignable_selector | w.render — ?. is encoded exactly like . |
y2..add(1)..add(2) (cascades) |
cascade_section (argument_part NOT inside a selector) | NOTHING — cascade calls are completely invisible |
new WidgetT(2) |
new_expression → INSTANTIATION branch :5145 FIRST | instantiates WidgetT (extractBareCall's new_expression arm :363-367 is DEAD — the else-if never reaches it); args still recursed |
pad(const EdgeInsetsT.all(8.0)) |
const_object_expression :369-376 | EdgeInsetsT.all at the CONST node position (typeId + '.' + nameId; type-only form → EdgeInsetsT); children recursed after |
generic<int>(5) |
args selector (type args ride argument_part) | bare generic |
await fetch() |
recursion through unary/await_expression | fetch at the selector |
throw StateError('bad') |
recursion | StateError |
'sum ${a + compute()}' |
template_substitution recursion | compute (interpolation calls EMIT); $name → identifier_dollar_escaped → nothing |
local-lambda body final lam = (int a) { helper(a); } |
function_expression recursed transparently | helper attributed to the ENCLOSING function; lam(5) → lam |
ctor initializers (: size = seed()), enum-constant args (ok(200)), default param values, hook-consumed constant initializers |
never body-walked | NOTHING |
extractCall (:3684), LITERAL_RECEIVER_TYPES (:373-388), SKIP_RECEIVERS, the parenthesized-conversion regex (:4530), template-strip — ALL UNREACHABLE for dart (callTypes empty). Do not port them.
Static-member / value-read refs — dart branch (:4759-4767), STATIC_MEMBER_LANGS:346
Called from the body walker only (:5218). The DART-SPECIFIC branch (the
shared MEMBER_ACCESS_TYPES path is never reached — it returns first):
node.type === selector AND it has NO argument_part child AND
previousNamedSibling is an identifier matching /^[A-Z][A-Za-z0-9_]*$/ →
references <identifier text> from the enclosing symbol at the
IDENTIFIER's (receiver's) position (pushStaticMemberRef :4800). Pins:
ConfigT.setting;→ references ConfigT (value read).ConfigT.load()→ references ConfigT TOO — the.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. Class-field/constant initializers and visitNode contexts → never called.
Type-annotation references — dart ∈ TYPE_ANNOTATION_LANGUAGES (:5753), the dart branch (:5819-5833) is LIVE
For every function/method node, extractTypeAnnotations takes the DART path:
sig = node; if node.type === 'method_signature' → sig = first inner
function/getter/setter/constructor/factory_constructor signature (?? node
— operators fall back to the method_signature itself). Then
extractTypeRefsFromSubtree(sig) (:6090) — one references ref per
type_identifier LEAF in the whole signature subtree, skipping
BUILTIN_TYPES (:5768-5782), at each leaf's position. Consequences (pinned):
- Return types AND param types in one sweep:
void render(CanvasT c)→ references CanvasT;List<WidgetT> listRet(Map<String, WidgetT> m)→ references List (List is NOT builtin!), WidgetT, Map (not builtin), WidgetT. - BUILTIN suppressions that matter for dart:
int,double,String(capital-S — the Scala row),Boolean,bool,float,long,char. NOT suppressed (noise refs, PRESERVE):num,dynamic,Object,List,Map,Set,Future,Stream,Iterable,T-style generic params.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. - 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 declaration they decorate (inside program / class_body).
extractDecoratorsFor (:4897) is called for classes (:1710), functions
(:1599), methods (:1819) — NOT for hook-minted constants, enums(!), or
type aliases (extractEnum/extractTypeAlias never call it — an annotated
enum emits nothing). Mechanics for dart:
- Scan #1 (direct children :4976-4988): annotations are never children of
the signature → inert (no
modifiersnode either). - Scan #2 (preceding siblings :5002-5023): walk BACKWARD from the
declaration;
annotationis in the accepted set (:5017); stop at the first non-annotation sibling. consider(): target = first namedChild of accepted types → theidentifier(override,deprecated,pragma,immutable,Deprecated) →<-strip + last-.-segment (@ui.Widgetstyle would strip toWidget) →decoratesref {from the decorated node, name, line/col of the ANNOTATION node}. With-args annotations emit their NAME; the argument expressions are never visited (no refs from@Deprecated('use other')'s string). - Stacked annotations emit in REVERSE source order (the backward walk):
@Deprecated('x')\n@pragma('vm:entry-point')\nvoid f()→ decoratespragmaFIRST, thenDeprecated(pinned). - For a method: the previous member's function_body (or any declaration) breaks the chain correctly. The annotation-BETWEEN-doc-and-decl also breaks the DOCSTRING chain (§Docstrings).
- Bodiless ctors (declaration-wrapped): extractMethod runs on
constructor_signature whose PARENT is the
declarationnode — the backward scan runs over declaration's children (constructor_signature is namedChild(0) → declIdx 0 → no siblings scanned) → an annotation before the declaration attaches to NOTHING. Annotated fields likewise emit nothing (no extractor runs).
Docstrings (tree-sitter-helpers.ts:95-127) — dartdoc is KEPT, both forms
Dart comment node kinds: comment (// and /* */) and
documentation_comment (/// AND /** */ — the block form is
scanner-produced, external _documentation_block_comment). BOTH kinds are
in getPrecedingDocstring's accepted set → /// runs, /** */ blocks, and
plain // comments all become docstrings and accumulate together (pinned:
/// Line doc kept. + // Plain comment also kept? → joined two-line doc;
/** Block dartdoc kept. */ → kept). cleanCommentMarkers: the /*-open
strip + ^\/\/[/!]?\s? + ^\s*\*\s? gm strips fire — all gm strips
ride js_multiline_strip in docstring.rs (#1329 CRLF semantics) — call the
shared code, port nothing. DOCSTRING_WRAPPER_TYPES contains no dart kinds
→ no anchor climbing. An annotation between the comment run and the
declaration BREAKS the chain (pinned: /// Broken by annotation.
@deprecated void annotated() → doc undefined — the dominant real-world
loss since @override is ubiquitous). Docstrings attach to: functions,
methods (incl. <anonymous> operators), classes/mixins/extensions, enums,
type aliases. NOT to: hook-minted constants (extra carries only signature —
pinned drop), enum members, imports, the file node. No comment-gluing into
import extents (probed — import_or_export ends at the ;).
Value-reference edges (:398-931) — dart IS in VALUE_REF_LANGS (:401)
Port the full machinery (crib java.rs/kotlin.rs): CODEGRAPH_VALUE_REFS=0
kill; MAX_VALUE_REF_NODES = 20,000 caps both DFS passes; isGeneratedFile
skip (.g.dart and friends!).
- Targets (captureValueRefScope:735): kind constant|variable, name ≥3
chars AND
/[A-Z_]/, parent id prefix ∈ {file:, class:, module:, struct:, enum:}. Dart mints 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 ✓ (pinned from=file / from=class); 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), visitNode:990 (the
§double-walk twins from file/class scope), scanFnRefSubtree (hook-consumed
constant initializers — halts at
function_expression:609, so lambdas inside a constant's initializer don't leak candidates). - Flush gate (:639-728): effectively "defined in this file" ONLY — dart
import refs are URIs (
package:foo/util.dart,dart:async) which match neither SIMPLE_NAME nor QUALIFIED_IMPORT (:and/excluded) → importedNames is always EMPTY for dart. definedHere = same-file function/method NAMES — which includes single-letter method names (a 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); doc shapes (///run,/** */kept,//kept, annotation-broken chain); annotations (bare, with-args, stacked → reverse order, on class); top-level constants (CAPS/lowercase/multi/typed/derived, sig truncation ≥100 chars); top-level var/typed-var/getter/setter (all invisible); async vs async*/sync* (isAsync true/false/false); returnType matrix (builtin, generic container, nullable, prefixed →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); 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.