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