首页
/ Codegraph Dart 内核移植完全指南:从 bug-for-bug 检查清单到 wasm 字节级对等

Codegraph Dart 内核移植完全指南:从 bug-for-bug 检查清单到 wasm 字节级对等

2026-09-06 12:02:17作者:俞予舒Fleming

Codegraph 正把各语言的提取逻辑从 TypeScript/wasm 一侧逐语言下沉到 Rust 内核(codegraph-kernel)。Dart 是 R7b 批次的收官语言,也是整个移植计划中"表面最怪"的一个:它的语法把函数体挂在签名节点的兄弟位上,导致同一份 body 被走两遍、同一节点以相同 id 被铸造两次。docs/design/dart-kernel-port-checklist.md 是这份移植的权威清单——它把 TS 侧 .dart 文件会触及的每个分支都用真实 dist/ 提取器钉死(pin)、每条语法规格都用生产 wasm 的完整 CST dump 验证过。本文以该清单为骨架,讲清楚 Dart 移植为什么难、难在哪、怎么验证,并指出当前仓库中已经落地的移植产物(codegraph-kernel/src/dart.rsvendored 语法parity 测试)。

移植契约:SURVEY COMPLETE 的 bug-for-bug 基线

清单开头声明了移植方法论,这是理解全文的关键:

  • 每个提取行为声明都钉在真实提取器上——不是读代码推断,而是用 extract-*.txt ground-truth dump 逐条对照;
  • 每个语法规格声明都经生产 wasm 探针验证——tree-sitter-wasms 0.1.13 的 dart 构建,通过完整 CST dump 而非源码阅读得出;
  • 字段存在性声明有 fieldNameForChild dump + 提取器行为双重佐证(文档特别提到 swift 的教训:单靠 dump 标签会撒谎);
  • 所有结论锚定在调研时 HEAD 45a53eb(clean main)上的 file:line,需与 rust-kernel-migration-plan.md(§0a 配方、§2 边界、§4 tracker、§5 门禁)以及两个姊妹清单一起读:kotlin-kernel-port-checklist.md(结构上最接近的兄弟:visitNode hook 常量 + vendored-C 构建)和 swift-kernel-port-checklist.md(共享分支活性风格与高 defer 姿态)。

清单的三大 blocking findings 决定了整个移植的形态:

  1. sibling-body double-walk 是重心:Dart 语法把每个函数/方法的 BODY 作为签名的下一个兄弟节点挂载,TS walker 会消费 body 两次——一次经 resolveBody(归属于函数/方法),一次经由外层通用遍历(归属于文件/类)。结果是确定性的重复节点模式(同名同 id 的本地函数挂在不同父节点下)、重复的 instantiates/calls refs、以文件/类为归属的 function_ref 双胞胎。没有任何已移植 walker 有过这个表面,内核侧必须逐字节、按观察到的交错顺序复现。
  2. 双臂 parse-error 发生率 3.4–20.7%:flutter/flutter 高达 20.73%,因为 2025-10 的语法解析不了空对象模式TimerInitial() =>,主流 sealed-class 状态匹配惯用法)和匿名 library; 指令。parity 扫频必须带 --max-deferral 0.3(swift 式豁免)。3 个仓库 + 46 段 snippet 电池里 0 个 phantom error(不像 kotlin),但 defer 规则仍是"信 flag"。
  3. "语法升级"是已出货 wasm 的字节拷贝(对未固定依赖的去风险,而非版本变更),所以 bump 门禁平凡化——期待全部字节一致;真正的语法门禁只剩 kernel-grammar-parity(vendored-C 构建 ↔ vendored wasm 表恒等)。

语法准备:wasm 字节拷贝 + vendored-C 内核构建

调研时 dart 不在 VENDORED_WASM_LANGS 中(grammars.ts 当时的清单),生产路径解析 require.resolve('tree-sitter-wasms/out/tree-sitter-dart.wasm')dart: 'tree-sitter-dart.wasm' 映射在 grammars.ts:36;.dart → dart 在 grammars.ts:108,无内容嗅探、无方言)。隐患在于:tree-sitter-wasms 0.1.13 的 dart 依赖是未固定的 github:UserNobody14/tree-sitter-dart,任何一次 tree-sitter-wasms 升级都会静默改变 dart 语法——这正是 vendor 机制要消灭的风险。

调研锁定的溯源信息(批次 4 语法探针验证):

  • UserNobody14/tree-sitter-dart master@d4d8f3e337d8be23be27ffc35a0aef972343cd54(2025-10-04,"Fix set and get contextual keywords…" #89)——parser.c 的表与出货 wasm 位置级一致(ABI 15,STATE_COUNT 3640,SYMBOL_COUNT 524 + ALIAS_COUNT 1,TOKEN_COUNT 161,22 字段,EXTERNAL_TOKEN_COUNT 7)。
  • 生产 wasm sha256 7f5364e4256cf7e55efd01dd52421ef2663caa8061b82659b7e4bf61064545ec(984,666 字节)。vendor 方案:把这个文件字节拷贝src/extraction/wasm/tree-sitter-dart.wasm,加 VENDORED_WASM_LANGS += 'dart'。不重建、不 tree-sitter generate——出货字节就是参考物。copy-assets 已经 glob src/extraction/wasm/*.wasm。MIT 许可。
  • 外部扫描器:有——固定 commit 上存在 src/scanner.c,负责 7 个外部 token:5 个字符串模板字符类(_template_chars_*)、_block_comment_documentation_block_comment也就是说 /** */ 文档注释是扫描器产物;vendored C 必须包含 scanner.c,否则所有字符串/注释形态全毁。
  • 调研时记录的五份 vendor 文件 sha256(parser.cscanner.ctree_sitter/parser.halloc.harray.h)列在清单 §Grammar prep 中,供逐字节核对。
  • crates.io 的 tree-sitter-dart 0.20.0 是 nielsenko 的 fork——拒绝(谱系不同,批次 4 探针测出 480 处位置表不匹配)。这取代了计划 §4 长尾行里 dart 的 "crates.io" 路线。

内核侧复用 kotlin #1382 的 vendored-grammar-C 机制(第二次使用):把三件套(parser.c、scanner.c、tree_sitter/*.h)拷到 codegraph-kernel/grammars/dart/(当前仓库中该目录已就位),扩展 codegraph-kernel/build.rscc::Build 以该语法自带的 flag 编译两个 C 文件(其 check-in 的 bindings/rust/build.rs 使用 -Wno-unused-parameter-Wno-unused-but-set-variable-Wno-trigraphs、msvc -utf-8——与 kotlin vendored 同一组)。langs.rs 注册:

extern "C" { fn tree_sitter_dart() -> *const (); }
// …
"dart" => Some(unsafe { tree_sitter_language::LanguageFn::from_raw(tree_sitter_dart) }.into()),

外加 LANGUAGES 数组追加 "dart",以及 __tests__/kernel-grammar-parity.test.tsGRAMMAR_LANGUAGES += 'dart'——对 vendored wasm 逐 id 比较 ABI/kind/field 表就是这里的整套语法门禁(两个制品按构造同 commit;parity 测试证明它保持如此)。当前仓库的 langs.rs 中已能看到该 extern "C" 声明与 "dart" => 分支落地。

暂存顺序:wasm 字节拷贝 + VENDORED_WASM_LANGS 可以独立先行落地(全套测试绿;"新旧" dump 门禁退化为 cmp 一致——在一个仓库上跑一次冒烟、期待零 diff)。内核 C vendor + build.rs + langs.rs + grammar-parity 行随 walker 或与其同期落地——第一天起就同 commit。

双臂 parse-error 现实与 defer 政策

同一语法字节同时作用于两臂(按构造),调研用 error-sweep.cjs 扫了所有 ≤1 MiB 的 .dart 文件(跳过 .git/node_modules/.dart_tool):

仓库 文件数 hasError 比例 phantom
dart-lang/shelf(小,服务端 dart) 99 10 10.10% 0
felangel/bloc(中,Flutter + 包) 616 21 3.41% 0
flutter/flutter(大,框架本身) 6,465 1,340 20.73% 0

误差类(经采样 + 最小化复现,dart3-errors.txt + error-diag.cjs):

  • (a) 空对象模式Init() => / case Init(): / if (x case Init()) 必错(constant_pattern 缺 identifier);带字段的模式(Point(x: var a))解析干净。这是现代 sealed-class 状态匹配惯用法,单独驱动了 flutter 的 20.73% 和 bloc 的 flutter 示例;
  • (b) 匿名 library;(悬空文档惯用法,dart-lang 风格指南)→ 缺 identifier;library foo.bar; 则干净;
  • (c) null-aware 集合元素[?x]key: ?value,Dart 3.8)→ ERROR,经常掀翻整个文件根;
  • (d) mustache 模板 .dart 文件(bloc 的 bricks/{{name.snakeCase()}})——不是真 dart;
  • (e) augment class(罕见)。

其余探测全部干净:records、非空模式、switch 表达式、sealed/base/final/interface/mixin 类修饰、extension types、super-params、单行类体(无 kotlin 式 phantom)、shebang #!、数字分隔符、CRLF。注意非 ASCII 标识符会报错String séance)——但那本来就是非法 Dart(标识符按规范只有 ASCII);fixture 标识符保持 ASCII,非 ASCII 文本放注释/字符串里。

Defer 政策:逐文件 has_error()defer:(信 flag,绝不看 ERROR 节点存在与否);扫频用 --max-deferral 0.3(给 flutter 的 20.7% 留余量,shelf/bloc 远低于此)。defer 率相对表格跳变是 walker bug;率本身是语法现实。

架构决策:单 walker、无 hook、双路径

清单列出的六条架构决策,直接决定 dart.rs 的形态:

  1. 无 preParse、无 POST_PASSdartExtractor 没有 preParse hook(languages/dart.ts 全文)→ preParsedSourcekernel/index.ts)是 no-op;两臂都解析原始字节。POST_PASSES 为空 → tryKernelExtractRaw 保持合格。
  2. 不存在 dart framework resolver(grep src/resolution/frameworks/ 无果,没有 Flutter resolver)。但通用vue/astro resolver(无 languages 列表 ⇒ 对每种语言生效)带 extract() hook → 在检测到 vue/astro 的仓库中,parse-worker 会把 .dart 文件强行切到解码后的 extractFromSource 路径(它们的 extract() hook 对 .dart 内容是 no-op——传输路径不同,输出必须相同)。纯 dart/Flutter 仓库走 raw buffers 路径。三个门禁仓库预期全部走 raw 路径;混合 Vue+dart 仓库就是解码路径的冒烟检查。
  3. 单 walker 模块(建议 codegraph-kernel/src/dart.rs),注册进 langs.rs;逐文件 has_error()defer:。抄写蓝本:kotlin.rs 的 visitNode-hook 消费常量 + hook/scan 交互;java.rs 的类式作用域栈、静态成员 refs、装饰器、类型注解遍历。三个表面在所有已移植 walker 中无先例,只能从本文档转录:(a) extractBareCall 选择器遍历(dart 是第一个 callTypes=[] 的语言);(b) sibling-body double-walk 复现;(c) 构造函数命名/跳过 hook(resolveName/isMisparsedFunction)。
  4. 接线备忘(KERNEL_ABI_VERSION 2,layout.ts:20):没有任何 dart 提取路径发出携带 filePath 的 ref(全部 ground-truth dump 验证为零)→ REF_FLAG_FILE_PATH 保持未用,每个 ref 都是 flag 0(同 swift/rust)。function_ref 线码 200。节点 decorators 线字段永不为 dart 填充(无 extractModifiers hook——注解只是 decorates REFS)。节点 signature 字段重度使用(函数/方法以及 hook 铸造的常量)。
  5. MAX_FILE_SIZE (1 MiB) 与跳过目录.dart_tool.pub-cache)在编排器/TS 侧,共享。isGeneratedFile 匹配 .g.dart.freezed.dart.pb.dart.pbgrpc.dart.chopper.dart——这些文件照常提取跳过 function_ref flush 和 value-ref pass;build_runner 产物在 Flutter 仓库是巨大族群,移植时必须保留这个 skip。
  6. Kind 清单(dart 能发出的节点类型):file、import、function、method、class、enum、enum_member、type_alias、constant。绝不发出:field、property、variable、struct、interface、trait、namespace、module。实例字段完全不铸造节点(§Constants);没有 namespace/package 节点(无 packageTypes hook——library foo.bar; 不可见);接口不是 dart 概念(abstract 类就是 kind class)。

Extractor 配置与语法事实(languages/dart.ts,380 行)

配置侧的类型表(锚点 src/extraction/languages/dart.ts):

  • functionTypes=[function_signature];classTypes=[class_definition];methodTypes=[method_signature, constructor_signature];interfaceTypes=[];structTypes=[];enumTypes=[enum_declaration];enumMemberTypes=[enum_constant];typeAliasTypes=[type_alias];importTypes=[import_or_export];callTypes=[]extractCall 对 dart 永不运行,所有调用 ref 走 body walker 的 extractBareCall);variableTypes=[];extraClassNodeTypes=[mixin_declaration, extension_declaration]。nameField=name,bodyField=body,paramsField=formal_parameter_list),returnField=type(同样)。

配置依赖的关键语法规格事实(全部经探针,mini-cst.txt/torture-cst.txt):

  • 签名/体分离:每个函数/方法是一个 SIGNATURE 节点(function_signature / method_signature / 裸 constructor_signature),其 body 是下一个具名兄弟 function_bodyfunction_body 包一个 block 或裸 => 表达式(箭头体没有 block 子节点)。async/async*/sync* 关键字是 function_body 的匿名子节点。
  • 字段真值表fieldNameForChild + 行为钉死):function_signature.name → identifier ✓(formal_parameter_list 是无字段子节点;返回类型是名字之前的无字段 type_identifier/void_type);method_signature 无任何字段(其单子节点是内层签名);getter_signature.name/setter_signature.name ✓;constructor_signature两个 identifier 都有 name 字段name: Widgetname: named——childForFieldName('name') 返回第一个 = 类)+ parameters: 字段;factory_constructor_signature 子是无字段 identifier;class_definitionname: ✓、superclass:interfaces:body: ✓;extension_declarationname:(匿名扩展时缺失)、class:(on 类型)、body:mixin_declaration完全无字段enum_declarationname: + body:enum_constant.name ✓;type_alias 无字段(现代 typedef X = T 与遗留 typedef void X(int) 都是同一节点);initialized_variable_definitionname: + 一个或多个 value: 字段(RHS 后置链被摊平成多个带 value 字段的兄弟)。
  • 类内成员形态:方法/getter/setter/operator/factory 与带体构造函数是 method_signature > (function_signature | getter_signature | setter_signature | operator_signature | constructor_signature | factory_constructor_signature) + 兄弟 function_body,两者都是 class_body 的直接子节点。无体构造函数Widget(this.size);Widget._() : x = 0;)包在 declaration 节点里(declaration > constructor_signature [+ initializers]),walker 靠对 declaration 的普通递归到达。const 构造函数是 declaration > constant_constructor_signature,重定向工厂是 declaration > redirecting_factory_constructor_signature——两者都不在 methodTypes 里 ⇒ const ctor 与 const factory X.r() = Impl; 不可见(无节点、无 refs)
  • 语句/表达式:调用没有 call 节点——后置链是扁平兄弟identifier selector selector …,每个 selectorunconditional_assignable_selector.name)/ conditional_assignable_selector?.name)/ argument_part > arguments > argument*new Foo(1) 是单个 new_expression(子:无字段 type_identifier + arguments)。const Foo.bar(1) 在表达式位是 const_object_expression;在 const x = … 声明里 const 挂在声明上,值是普通扁平链。级联是 cascade_section > cascade_selector + argument_part(无 selector 节点 ⇒ 对一切都不可见)。赋值是 assignment_expressionleft:/right: 字段。局部变量是 local_variable_declaration > initialized_variable_definition(即使无初始化器)。Lambda 是 function_expression(含 body: function_expression_body 子)。字符串模板:string_literal > template_substitution 包完整表达式(${…})或 identifier_dollar_escaped$name)。

存在的 hooks(逐个精确移植)

  • visitNode(:144-157)——常量分支node.type === 'static_final_declaration' → nameNode = 首个 identifier 具名子;若存在:valueNode = nameNode.nextNamedSibling只取第一个 value 子——摊平链如 WidgetT(0) 只捕获 WidgetT;包裹表达式如 SHARED_MAX + 1 捕获整个表达式节点);initValue = 其文本 .slice(0,100)(UTF-16);ctx.createNode('constant', name, node, { signature: initValue ? \= initValue{initValue}{initValue.length >= 100 ? '...' : ''}` : undefined })。返回 **true**(消费——name 找到才 createNode,但 hook 对**每一个**到达的 static_final_declaration 都返回 true,铸造与否皆然;精确转录 early-return 形状:if (nameNode) {…}; return true)。dispatcher 随后运行 scanFnRefSubtree(node, 0) 且不再下探。 **节点类型的现实**(probe2-cst.txt 探针):static_final_declaration= 带初始化器的final/const 声明,且是**顶层**(const SHARED_MAX = 10;final typedTop = compute();,有类型无类型均可,含多声明 final multiA = 1, multiB = 2;→ 每个列表项一个节点)**或**类级**带static**(static const int K_MAX = 9;static final sharedInst = WidgetT(0);)。实例 final untyped = 5;/final int size;/ 任何var/带类型 var 成员 → initialized_identifier→ **无节点**(dart 永远零field节点)。顶层var topVar = 5;/int topTyped = 6; → initialized_identifier → 同样不可见。常量**无 docstring、无 visibility、无 isExported**——只有 signature(钉死:/// Doc on num const?` 被丢弃)。
  • resolveBody(:158-171)——function_signature/method_signature → 若 node.nextNamedSiblingfunction_body 则取之,否则 null(注意:裸 constructor_signature——declaration 包裹的无体 ctor——走另一分支:childForFieldName('body') → null → 在子中找 class_body/extension_body → null ⇒ 具名无体 ctor 从不走 body,其 initializers 兄弟留给普通 visitNode 递归)。对 class/mixin/extension 节点:先标准 body 字段(class_definition ✓、extension_declaration ✓),否则找 class_body | extension_body 类型的具名子(mixin_declaration 无 body 字段 → 按类型找)。它被 extractClass/extractEnum 的 body 解析使用,也被 createNode 的 endLine 扩展使用(tree-sitter.ts:1322-1334——对 dart 活跃且承重:那里的注释点名了 Dart;方法/函数节点的 endLine 延伸到兄弟 body 的结尾,如 named L123-125 覆盖其 body)。
  • getReturnType = extractDartReturnType(:80-92)——ctor = dartCtorInfo(§下):验证过的 ctor 返回类名(具名 ctor、factory → ret = 外层类)。否则 sig = dartInnerSignature(:9——method_signature 解包到 function/getter/setter 签名;不含 constructor/factory/operator——那些保持 sig = method_signature,类型搜索找不到东西)→ 首个 type_identifier 具名子 → 文本,.replace(/<[^>]*>/g,'').trim()、取最后一段 .、须匹配 /^[A-Za-z_]\w*$/。钉死:Future<void>Future(type_arguments 是兄弟节点,所以 strip 是 no-op);List<WidgetT>ListWidgetT?WidgetT(nullable_type 位置级解包——type_identifier 仍是第一个);void → undefined(void_type ≠ type_identifier);num/dynamic/Object → 自身;T(泛型参数)→ T(泄漏);前缀 other.OtherClassother(前缀和名字是两个 type_identifier 叶子,第一个是前缀——.pop() 永远看不到点。BUG,保留)。getter → 其类型;setter → undefined;operator → undefined(内层解包漏掉 operator_signature);ctor → 类名。
  • isMisparsedFunction(:177-188)——dartCtorInfo != null 且 ctorName === className → 跳过无名构造函数 Widget(this.size)(无节点;extractFunction/extractMethod 仍解析+遍历 body——对 declaration 包裹形态是 null,所以实际什么都不发)。具名 ctor/factory 保留。@override (T) m() 误解析(注解吞掉 record 返回类型后 m() 长得像单 identifier 的 constructor_signature)跳过——dartCtorInfo 经 dartEnclosingTypeName(:38——向上走 class_definition/mixin_declaration/extension_declaration/enum_declaration 并读其 name 字段)验证 ids[0] 与外层类型名,reduceAction → 当作它本来的方法(钉死于 extract-probe4.txt:方法 reduce,位置从 reduce 开始,sig/ret undefined,decorates override 完好)。
  • getSignature(:189-208)——method_signature 解包到 function/getter/setter 签名(含 ctor/factory/operator);params = 找具名子 formal_parameter_list;retType = 找具名子 type_identifier | void_type;两者皆无 → undefined;结果 = [retType 文本 + 空格] + [params 文本],trim。钉死形状:void (int a, String b);getter → int(仅 retType);setter → (int v)(仅 params);泛型返回 Future<void> load()Future ()(type_arguments 不含在内——retType 就是那个 identifier);完整参数文本是原始源码切片——具名/可选括号、默认值、嵌套函数类型参数原样照搬(void ({int? named, required WidgetT child, String note = 'x'}));带体具名 ctor(method_signature > constructor_signature)→ undefined(解包漏掉 ctor;method_signature 上没有直接的 formal_parameter_list);无体具名 ctor(declaration > constructor_signature)→ "()" 式仅参数 sig(节点本身是 ctor 签名;其 formal_parameter_list 按类型找到)——两种 ctor sig 形状都要钉;operator → undefined。
  • getVisibility(:209-222)——method_signature → 解包到 function/getter/setter 签名 → 其首个 identifier 子;其他节点 → childForFieldName('name')。名字以 _ 开头 → 'private',否则 'public'。怪癖,保留每个构造函数都是 'public'——method_signature 包裹的 ctor 解包漏掉 constructor_signature(nameNode null → public);裸 constructor_signature 的 name 字段是标识符(Widget 而非 _)→ Widget._() 也是 'public'(钉死)。operator → public。_check/_privateMethod → private ✓。类/enum 在各自提取器上经同一 hook 获得 visibility。
  • isAsync(:223-233)——nextNamedSibling 是 function_body → 扫描其所有子(含匿名)找 child.type === 'async' → true。钉死:async → TRUE;async*sync* → FALSE(不同 token 文本 → 不同匿名类型——生成器不算 "async");无体(external/abstract)→ false。
  • isStatic(:234-243)——node.type === 'method_signature' → 扫描所有子找类型 'static' → true;否则 false。static WidgetT make() → true ✓(static 关键字是 method_signature 的匿名子)。裸 constructor_signature → false。顶层函数 → false。
  • resolveName(:244-260)——dartCtorInfo:具名 ctor/factory → ctor 名(namedcreate_);无名 ctor 或非 ctor → undefined(落到 extractName)。
  • extractImport(:261-304)——importText = trim 后整节点切片;moduleName = URI 字符串内容:library_import > import_specification > configurable_uri > uri > string_literal(否则 library_export 下的同链),引号经 .replace(/['"]/g,'') 剥离。返回 {moduleName, signature}(无 handledRefs)→ import 节点(name = URI,如 package:torture/other.dartdart:asyncsrc/reexported.dart)+ 通用 imports ref(tree-sitter.ts:3183-3194,{from: 文件节点(无 namespace 存在), name = URI, import_or_export 节点的 line/col})。as aliasshow/hide 组合子:忽略(不读)。import 'x.dart' deferred as y; 不可见——deferred 形态的 import_specification 持裸 uri(无 configurable_uri 包裹)→ hook 返回 null → 落到多 import 内联处理器(无一是 import_or_export)→ 无节点无 ref(钉死,extract-mini.txt)。part/part of/library 指令是不同节点类型(part_directive、part_of_directive、library_name)不在任何类型表 → 不可见。
  • extractBareCall(:305-379)——§Calls,完整矩阵见后。

不存在的 hooks(walker 绝不做这些):preParserecoverMangledNameclassifyClassNode(class_definition 永远是 kind class——abstract/sealed/base/final/interface-modified 全含)、classifyMethodNodeextractPropertyNamepropertyTypesfieldTypesgetReceiverType(永不转向 extractFunction;无 receiver QN;无 owner-contains——extension 成员挂在 extension 自己的 class 节点上)、isConstisExported(除文件节点的字面 false 外处处 undefined)、interfaceKindextractPackage/packageTypes(无 namespace 节点——顶层 QN 是裸的)、extractModifiers(节点 decorators 永不设置)、synthesizeMembersskipBodilessClass(无体 class Base {} 照样铸造——dart 类总有 {} 嘛)、methodsAreTopLevelresolveTypeAliasKind(type_alias 永远 kind type_alias)。

注册点:EXTRACTORS.dartlanguages/index.ts)、FN_REF_SPECS.dartfunction-ref.ts)。

重心:THE SIBLING-BODY DOUBLE-WALK(逐字复现)

因为 body 是兄弟,每个消费签名的提取器经 resolveBody 走一遍 body,随后外层循环(program 级、class-body 循环、或外层 visitFunctionBody)又把同一 function_body 节点当普通子访问一次。每趟各触发什么:

  • Pass 1(visitFunctionBody,归属于函数/方法):完整 body 矩阵——extractBareCall 调用、instantiates、静态成员 refs、嵌套具名函数、fn-ref 捕获。
  • Pass 2a(visitNode 递归——program/class-body 兄弟访问):只有 visitNode 分派的分支:new_expression来自 FILE(顶层)或 CLASS(类体)的 instantiates;嵌套 function_signature第二次 extractFunction(functionTypes 命中)铸造重复节点——同 (kind, name, line) ⇒ 同一节点 id——但其父是文件/类(contains 边来自文件/类;顶层 QN localFn,类内 Holder::methodLocal),且它自己的 body walk 重新发出归属于重复节点的 refs;maybeCaptureFnRefs → 来自文件/类的 fn-ref 候选双胞胎static_final_declaration 从不出现在 body 里(局部是 initialized_variable_definition)→ 无常量重复。裸调用、静态读、级联:什么都不是(visitNode 里没有 extractBareCall/extractStaticMemberRef)。
  • Pass 2b(外层 visitFunctionBody 的兄弟访问——LOCAL 函数 body 被其外层 body walker 看到):完整 body 矩阵再来一遍,归属于外层函数——于是局部函数里的裸调用从局部函数(pass 1)和外层函数(pass 2b)以及重复局部函数(pass 2a 自己的 body walk)三方发出。

钉死的交错(extract-probe2.txt,作为 parity 期望逐字转录):

  • 顶层 hostFn 含局部 localFn { inner(n); } 与局部 localWithNew { new Holder(1); }:节点 hostFn::localFn(contains 于 hostFn)……然后是 localFn(裸 QN,contains 于文件,同 id,在 pass-2a 重访后);refs 顺序:calls inner from=localFn(嵌套 extract 的 pass 1)、calls inner from=hostFn(pass 2b)、calls localFn from=hostFninstantiates Holder from=localWithNewinstantiates Holder from=hostFncalls localWithNew from=hostFn——然后 program 级重访:calls inner from=localFn(第二节点的 walk)、instantiates Holder from=localWithNew(第二)、instantiates Holder from=file
  • 类内 useNew 含局部 methodLocal { h.touch(); }new Holder(2)instantiates Holder from=useNewcalls h.touch from=methodLocalcalls h.touch from=useNewcalls methodLocal from=useNew,然后 class-body 重访:instantiates Holder from=class:Holdercalls h.touch from=methodLocal(第二)——且节点 Holder::useNew::methodLocal + 重复的 Holder::methodLocal(两者 kind 都是 function,同 id)。
  • fn-ref 双胞胎:顶层函数里的 register(seedValue)function_ref seedValue from=refTakerfrom=file:…(flush 顺序:先全部作用域内候选,再重访的文件/类双胞胎——捕获顺序);类方法里 → from=method:wirefrom=class:H 双胞胎(fnref2.dart pin)。
  • 逐声明顺序:一个函数的 pass-1 refs 全部先于其 pass-2a refs,后者先于下一个声明的 refs(program 循环访问签名、然后 body、然后下一个签名)。class_body 内类成员同理。

去重现实:节点重复共享 id(generateNodeId 无唯一化器)——两条节点行、两条 contains 边;下游 store 按 id last-writer-wins,但 WIRE/parity dump 携带两行——字节对等要求两行都发、按序发。内核 node_ids vec 因此不得自去重:TS 发什么就发什么。

调用矩阵:extractBareCall 的完整行为表

只有 visitFunctionBody 咨询 extractBareCall;ref 是 {from: nodeStack 顶, name, 匹配节点(selector/const-object 节点,不是链头)的 line/col}。矩阵(全部钉死于 extract-torture.txt / extract-mini.txt):

源码形态 匹配节点 发出的 calls ref
helper(count) selector>argument_part;prev = identifier selector 位置的 helper
WidgetT(1) 同上 WidgetT(ctor = 普通大写调用;解析侧的 CONSTRUCTS_VIA_BARE_CALL——name-matcher.ts:918——正依赖这个形状)
obj.method(x) args selector;prev = .method selector;accessorPrev = identifier obj.method(receiver 保留,大小写不论)
ConfigT.load() 同上 ConfigT.load + 来自 .load selector 的 references ConfigT 静态成员 ref(§静态成员)——双重发出
other.OtherClass()(前缀 ctor) 同上 other.OtherClass
this.own() / super.parent() args selector;accessorPrev = this/super 节点(非 identifier) own / parent
a.b.call3(x)(2+ 跳) accessorPrev = 另一个非 argument_part selector call3
FactoryT.create().run() 外层 args selector;accessorPrev = 内层 argument_part selector FactoryT.create().run——#750 经 dartCalleeOfArgPart(:100-116)重编码:innerCallee FactoryT.create 匹配 /^[A-Z]/`${innerCallee}().${method}`;加上来自其自身 selector 的内层 FactoryT.create ref;加上 references FactoryT
w?.render() conditional_assignable_selector w.render——?. 编码得与 . 完全相同
y2..add(1)..add(2)(级联) cascade_section(argument_part 不在 selector 内) 无——级联调用完全不可见
new WidgetT(2) new_expression → INSTANTIATION 分支优先 instantiates WidgetT(extractBareCall 的 new_expression 臂是死的——else-if 永远到不了);args 仍递归
pad(const EdgeInsetsT.all(8.0)) const_object_expression const 节点位置的 EdgeInsetsT.all(typeId + '.' + nameId;仅类型形态 → EdgeInsetsT);children 递归
'sum ${a + compute()}' template_substitution 递归 compute(插值调用$name → identifier_dollar_escaped → 无)
局部 lambda 体 final lam = (int a) { helper(a); } function_expression 透明递归 helper 归属于外层函数;lam(5)lam
ctor initializers(: size = seed())、enum 常量参数(ok(200))、默认参数值、hook 消费常量初始化器 永不 body-walk

extractCall、LITERAL_RECEIVER_TYPES、SKIP_RECEIVERS、括号转换正则、模板剥离——对 dart 全部不可达(callTypes 为空)。不要移植。

静态成员/value-read refs:dart 专属分支(dart 在 STATIC_MEMBER_LANGS 中)——node.type === selector 且无 argument_part 子且 previousNamedSibling 是匹配 /^[A-Z][A-Za-z0-9_]*$/identifier → 从 enclosing 符号在 identifier(receiver)位置references <identifier 文本>。钉死:

  • ConfigT.setting; → references ConfigT(value read);
  • ConfigT.load() 同样发 references ConfigT——.load selector 无 argument_part(args 是下一个 selector),且 dart 分支没有 callee-of-call 跳过 → 每个大写 receiver 方法调用都双发(references + calls)。保留——这是真实仓库上最大的 ref 量怪癖
  • util.Config.load() → 无(首 selector 的 prev = 小写 util);
  • this.x → prev 是 this 节点 → 无。case 模式(case ColorT.blue:)→ constant_pattern > qualified 形状,无 selector → 无(钉死的缺口)。

类型注解 refs:dart ∈ TYPE_ANNOTATION_LANGUAGES,dart 分支活跃——每个函数/方法节点,extractTypeAnnotations 走 DART 路径:sig = 节点;method_signature → 首个内层 function/getter/setter/constructor/factory_constructor 签名(operator 回落到 method_signature 自身)。然后 extractTypeRefsFromSubtree(sig)——签名子树中每个 type_identifier 叶子一条 references ref(跳过 BUILTIN_TYPES)。后果(钉死):返回类型和参数类型一次扫完(List<WidgetT> listRet(Map<String, WidgetT> m) → references List——List 不是 builtin!、WidgetT、Map、WidgetT);抑制的 builtin:intdoubleString(大写 S——Scala 行)、Booleanboolfloatlongchar不抑制(噪声 refs,保留):numdynamicObjectListMapSetFutureStreamIterableT 式泛型参数void 是 void_type 节点 → 结构上静默。泛型声明自引用:T generic<T>(T v)references T ×3。前缀类型:other.OtherClass → references otherOtherClass(两个叶子)。extractVariableTypeAnnotation 需要 type_annotation 子——本语法无此节点类型 → 死;body walker 的 variable_declarator 分支同死。局部 WidgetT w = …;、字段类型、is/as 类型、body 里集合类型参数 → 无 refs(钉死)。

装饰器:dart 注解经兄弟扫描decorates。注解是 annotation 节点(带 name: 字段;带参形态有 arguments 子)——@override(裸)与 @Deprecated('x')(带参)都是 annotation,是被修饰声明的前置兄弟。extractDecoratorsFor 对类、函数、方法调用——对 hook 铸造的常量、enum(!)或 type alias(带注解的 enum 什么都不发)。机制:扫描 #1(直接子)惰性(注解从不是签名的子);扫描 #2(前置兄弟)从声明向后走;annotation 在接受集合中;在首个非注解兄弟处停。consider():target = 首个具名子 → identifier → < 剥离 + 取最后 . 段(@ui.Widget 式剥成 Widget)→ decorates ref {from 被修饰节点, name, ANNOTATION 节点的 line/col}。带参注解发其名字;参数字表达式永不访问。叠放注解按源码逆序发出(后向遍历):@Deprecated('x') @pragma('vm:entry-point') void f() → decorates pragma 先,然后 Deprecated(钉死)。无体 ctor(declaration 包裹):extractMethod 跑在父为 declaration 的 constructor_signature 上——后向扫描扫的是 declaration 的子(constructor_signature 是 namedChild(0) → declIdx 0 → 不扫兄弟)→ declaration 前的注解挂不到任何东西

Docstrings:dart 注释节点种类:comment///* */)和 documentation_comment////** */——块式是扫描器产物,外部 _documentation_block_comment)。两种都在 getPrecedingDocstring 接受集合 → /// 行、/** */ 块、普通 // 注释都成为 docstring 且累积在一起(钉死:/// Line doc kept. + // Plain comment also kept? → 合并两行 doc;/** Block dartdoc kept. */ → 保留)。cleanCommentMarkers 的各 gm 剥离全部走 docstring.rs 的 js_multiline_strip(#1329 CRLF 语义)——调共享代码,不单独移植。注释行与声明之间的 annotation 会打断链(钉死:/// Broken by annotation. @deprecated void annotated() → doc undefined——@override 无处不在,这是主导性的真实世界损失)。Docstring 挂到:函数、方法(含 <anonymous> operator)、类/mixin/extension、enum、type alias。挂到:hook 常量(extra 只带 signature——钉死的丢弃)、enum 成员、imports、文件节点。

Value-reference edges:dart ∈ VALUE_REF_LANGS——移植全套机制(蓝本 java.rs/kotlin.rs):CODEGRAPH_VALUE_REFS=0 kill;MAX_VALUE_REF_NODES = 20,000 封顶两趟 DFS;isGeneratedFile skip(.g.dart 之流!)。要点:

  • 目标(captureValueRefScope):kind constant|variable、名字 ≥3 字符且匹配 /[A-Z_]/、父 id 前缀 ∈ {file:, class:, module:, struct:, enum:}。dart 只铸造 constant → 目标 = 文件: 或类: 下的 hook 常量(enum 作用域的 static const 走 enum:)。lowercase_const/plain/low(无大写无 _)→ 非目标;kLimit/typedTop(内嵌大写)→ 目标。
  • 读者作用域:每个 function/method/constant 节点。dart 兄弟体拉取(:883-892)活跃且承重:函数/方法读者作用域的节点是 SIGNATURE——其 nextNamedSibling 若是 function_body(或 block)就压入读者 DFS;没有它,所有方法/函数 body 读都不可见。常量的读者子树是 static_final_declaration 本身(初始化器读:钉死 DERIVED_VAL → SHARED_MAX 边)。重复局部函数节点(§double-walk)也是读者作用域——同读、双作用域(按 (scope,target) 去重,作用域不同 ⇒ 局部函数读目标时来自孪生作用域的重复 value-ref 边——若 fixture 里局部函数读常量,包含此形状)。
  • Shadow prune:dart 声明器情形——static_final_declaration(目标本身)、initialized_identifier(字段/顶层 var)、initialized_variable_definition(局部)——各对其首个 identifier 类型具名子 bump。未初始化局部(int DERIVED;)也 bump(钉死 prune);方法局部 final SHARED_MAX = 1; 剪掉文件级目标(钉死)。assignment_expression 不是 prune 情形(swift 式教训检查对 dart 是死的):纯赋值重绑定从不 bump,low = 5; 式写对 prune 不可见。
  • 发射:每读者作用域,栈 DFS(具名子按序压入、弹出——逆源序访问;边序随之);匹配节点类型 identifier。任何映射到活目标的 identifier 文本都发——包括 Table.COL_LIMIT 导航的成员半边(钉死)和 ${SHARED_MAX} 插值(template_substitution > identifier)。$SHARED_MAX(无花括号)是 identifier_dollar_escaped——不接受 → 无读(钉死的不称性)。跳过 self-id + 同名,按 (scope,target) 去重 → EDGE {kind:'references', metadata:{valueRef:true}},追加在所有其他边之后(每个 dump 的最后行)。

函数即值捕获(#756)— DART_SPEC:idTypes = {identifier}(裸 identifier 就是候选)。分派:arguments → args;assignment_expression → rhs(字段 right);pair → value(字段 value);list_literal → list;static_final_declaration → varinit(无字段 → 最后一个具名子规则:要求 ≥2 个具名子;name 字段守卫惰性——static_final_declaration 无 name 字段)。layers:argument → null(扇出)。无 special、无 unwrap、无 ungatedModes、无 addressOfOnly。钉死:

  • register(topLevel) → 经 arguments→argument→identifier ✓;
  • 具名参数不捕获reg(cb: onlyNamed) → 子是 named_argument——不在 layers 不是 idType → 无(钉死,namedarg.dart)。Flutter 的 onPressed: handler 惯用法因此不可见——bug-for-bug,移植中不要"修"(候选未来精度 PR:TS 侧先加 named_argument/label 处理);
  • obj.cb = assigned → rhs 捕获 ✓;param-storage skip 比较 LHS 尾部 identifier 与 rhs 文本——this.cb = assigned 保留(cb ≠ assigned),假想的 this.cb = cb 跳过。cb = selfStore 中 selfStore 是遮蔽同名函数的参数 → 捕获 + 过 gate(假阳性,保留——钉死);
  • [topLevel, blockDoc] 列表 ✓(局部的 list_literals 也捕获——分派在节点被走到处就发);{'k': topLevel} pair ✓;final aliasTop = aliased; 顶层/static → varinit 裸 identifier ✓(钉死 from=file / from=class);局部 final alias = topLevel; → initialized_variable_definition 不在分派中 → 不捕获;
  • 捕获点:visitFunctionBody(body)、visitNode(§double-walk 的文件/类孪生)、scanFnRefSubtree(hook 消费常量初始化器——在 function_expression 处停,常量初始化器里的 lambda 不泄漏候选)。
  • Flush gate:实际就是"定义在本文件"——dart import ref 是 URI(package:foo/util.dartdart:async),既不匹配 SIMPLE_NAME 也不匹配 QUALIFIED_IMPORT(排除 :/)→ importedNames 对 dart 永远。definedHere = 同文件函数/方法名——包括单字母方法名(方法 a 会 gate 任何裸参数 a——钉死假阳性,保留)和 <anonymous>。每个候选都走 definedHere gate。按 ${fromNodeId}|${name} 去重 → 幸存者作为 {referenceKind:'function_ref'}(线码 200)追加在所有 walk refs 之后——先作用域内候选,后 double-walk 文件/类孪生(捕获顺序)。

节点身份、行号与发射顺序

  • createNode:id = generateNodeId(filePath, kind, name, startRow+1) = `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}`。FILE 节点 id = 字面 file:${filePath},name = basename,qualifiedName = filePath,endLine = source.split('\n').length,isExported false。
  • endLine 扩展对 function/method 种类活跃:resolveBody 的兄弟 function_body 把 endLine 延伸到签名之外(named L123→125;bodyShapes L60→96)。类/enum 等跨度是各自节点区间。
  • 每个创建节点一条 contains 边(来自 nodeStack 顶);qualifiedName = nodeStack 名字以 :: 连接(文件除外);无 package 节点 → 顶层 QN 裸;成员 WidgetT::render;嵌套 WidgetT::useNew::methodLocal(及重复的更浅 WidgetT::methodLocal——§double-walk)。
  • isInsideClassLikeNode:栈顶 kind ∈ {class, struct, interface, trait, enum, module}——对 dart 意味着 class(含 mixin/extension)和 enum。
  • 节点位置 = SIGNATURE 节点(函数/方法):externalFn 起于 col9(external 关键字在 function_signature 之外);@override (T) m() 误解析起于 m(col16 钉死);常量在 static_final_declaration(名字起点);enum 成员在 enum_constant;import 在 import_or_export。
  • 类家族细节:extractClass 对 class_definition + mixin_declaration + extension_declaration 运行;extractInheritance 之前于 body walk;extractCsharpPrimaryCtorParamRefs 有语言门(this.language !== 'csharp' 提前返回——移植为 no-op)。扩展名extension WidgetTExt on WidgetT → name 字段 → WidgetTExt匿名 extension on String → extractName 回落找到首个 type_identifier = ON 类型 → 名为 String 的类节点(钉死——成员变成 String::anonExt);被扩展类型本身不可见(无 ref——extractInheritance 对 extension/mixin 节点什么都匹配不到)。mixinmixin MixA on BaseT → class MixAon 类型是裸 type_identifier 子,匹配不到继承子句 → 无 refs;mixin MixB implements DrawTinterfaces 子匹配 → implements DrawT ✓。extractEnum:body 必须(body 字段的 enum_body);extractEnumMembersgetChildByField(node,'name')——enum_constant 有 name 字段 → 每常量一个 enum_member 节点,定位在 ENUM_CONSTANT 节点上(区间覆盖 ok(200)——id 行 = 常量的行),然后 RETURN。ctor 参数(argument_part)永不遍历 → enum 常量参数无 refs。继承:superclass 子(仅 class_definition)→ dart 分支:superclass 节点每个具名子——直接 type_identifierextends ref;mixins 子 → 每内部 type_identifier 一条 implements ref。class WidgetT extends BaseT with MixA, MixB → extends BaseT + implements MixA + implements MixB;class OnlyMix with MixA {} → superclass 只含 mixins → implements MixA、无 extends(钉死)。interfaces 子:每子一条 implements。顺序:extends+mixins 先(superclass 子在源码中先于 interfaces),然后 interfaces——全在 decorates 和 body 成员之前(钉死 ref 顺序:extends BaseT、implements MixA、implements MixB、implements DrawT、decorates immutable、然后成员 refs)。enum 侧:interfaces 子 → implements ✓;mixins 子是 enum_declaration 的直接子(无 superclass 包裹)且匹配不到任何子句 → enum StatusT with MixB 对 mixin 什么都不发(钉死——类侧 mixin 处理只在 superclass 分支内)。
  • 常量发射钉(extract-torture.txt):顶层 const SHARED_MAX = 10 → constant、QN 裸、sig = 10final DERIVED_VAL = SHARED_MAX + 1 → sig = SHARED_MAX + 1(整个表达式节点);final typedTop = compute() → sig = compute(摊平链 → 只取第一个 value 子);static final sharedInst = WidgetT(0) → 类下 constant、sig = WidgetT;多声明 → 各一个节点带各自列。永不铸造:实例字段(带/不带类型、late、var)、static var、顶层 var/带类型 var、顶层 getter/setter、const 构造、重定向工厂、extension_type 容器、part/part of/library/deferred import。初始化器副作用:hook 消费常量的初始化器不被遍历 → 无来自它们的 calls/instantiates(只有 scanFnRefSubtree 捕获);initialized_identifier 字段的初始化器被 visitNode 递归但只有 INSTANTIATION_KINDS 在那里发 → int counter = 0; 无输出,但字段 final w = new Widget(); 会从instantiates Widget(顶层 var w = new Widget(); 从文件发)。
  • visitFunctionBody 的 dart 行:maybeCaptureFnRefs 每节点;callTypes 死;INSTANTIATION_KINDS → new_expression → extractInstantiation(children 仍递归——嵌套 new Foo(bar()) 两发 instantiates 和内层调用);extractBareCall 无 skip/return → 子总在其后递归;variable_declarator 分支死(无此 kind);嵌套 function_signature → 具名 → extractFunction → 局部函数成 function 节点(QN 经栈嵌套),然后外层 walker 重走其兄弟 body(pass 2b)、外层 visitNode 重访重新提取它(pass 2a);body 内 classTypes/enumTypes 语法上不可能(dart 无局部类/enum)→ 死。递归透明穿越 if/for/while/switch(但 case 模式内部用 pattern 节点种类——constant_pattern > qualified——extractBareCall 和静态 refs 都不匹配 → case 标签里的 enum 读不可见,钉死)、try/catch、await/unary、throw、字符串模板、function_expression lambda(调用归属于外层符号)、集合字面量(<Widget>[…] 类型参数静默)。
  • 杂项共享路径:位置 line = startPosition.row + 1column = startPosition.column——UTF-16 码元(textutil::col16);startIndex/endIndex 子串同样(getNodeText 处处:签名、常量初始化器 .slice(0,100)、import 签名 .trim())。extract() 包装:文件节点 →(无 package hook → 无 namespace)→ 根 visitNode walk → flushFnRefCandidates → flushValueRefs → pop。表顺序:节点按创建顺序(含 double-walk 重复);contains 边与创建交错;value-ref 最后追加;walk 顺序 refs 然后 flush 时的 function_ref refs。Store/harness 对行序敏感——精确复现。CRLF:端到端探明干净——CRLF 转换的 torture fixture 与 LF 提取字节一致extract-crlf.txt 文件名归一后 diff-0)。dart.ts 无多行正则;唯一 CRLF 敏感共享路径是 cleanCommentMarkers 的 gm 剥离 → docstring.rs 的 js_multiline_strip。多行签名切片和 ≥100 字符初始化器捕获在 CRLF 源上含原始 \r 字节——两臂按构造一致(同一源切片)。语言显示名 Dart(grammars.ts:626)——纯装饰。

解析合约:walker 输出喂给谁(保持 TS 侧)

无 dart framework resolver、无 dart callback-synthesizer pass、dart ∉ CC_LANGUAGES——grep 验证。walker 输出喂的解析合约(行为保持稳态):

  • CONSTRUCTS_VIA_BARE_CALLname-matcher.ts)——裸大写 calls WidgetT refs 解析为构造;依赖 ctor 调用保持为普通 calls ref(dart 只对 new_expressioninstantiates);
  • matchDottedCallChain(name-matcher.ts,dart 在列)——消费 Foo.create().run 重编码 + 方法节点的 returnType(extractDartReturnType 的 ctor 返类名规则正是让 WidgetT.named(3).chainTail() 能解析的原因——#750/#645);
  • 局部变量 receiver 推断——对原始源码的正则(var lg = Logger(Logger lg,)——除节点名外无提取依赖;
  • CHAIN_LANGUAGES(resolution/index.ts)含 dart——相同输入;
  • matchMethodCall 的 bare-fn-only 豁免排除 dart——裸 fn-ref 名可解析到方法(隐式 self)——walker 只需供给名字。

门禁与 fixture:parity 机制("全都咬过")

  • Vendor 门禁(替代常规 bump 门禁):wasm 字节拷贝 + VENDORED_WASM_LANGS += 'dart' + 内核 C vendor + langs.rs + GRAMMAR_LANGUAGES += 'dart'——全套测试绿;一次冒烟 dump-diff(shelf 上旧解析路径 vs vendored 路径)预期字节一致(同字节);kernel-grammar-parity 证明 C 构建 ↔ wasm 表恒等。
  • Torture fixtures__tests__/fixtures/kernel-parity/torture.dart(调研的 231 行 torture.dart 播种;其 extract-torture.txt 是期望输出 pin),按分支清单覆盖:imports(dart:、带 as+show 的 package:、带 hide 的 export、deferred → 不可见、part → 不可见);doc 形状(/// 行、/** */ 保留、// 保留、annotation 断链);注解(裸、带参、叠放 → 逆序、挂类上);顶层常量(大写/小写/多/带类型/派生、≥100 字符 sig 截断);顶层 var/带类型 var/getter/setter(全不可见);async vs async*/sync*;returnType 矩阵(builtin、泛型容器、nullable、前缀 → other bug、T 泄漏、void);signature 矩阵(参数原样——含具名/可选/默认/函数类型、getter 仅类型、setter 仅参数、泛型返回 Future ());external fn(关键字后位置);完整 ctor 集合(无名跳过;具名带体——无 sig;具名无体——() sig + 初始化器调用不可见;factory;const factory + const ctor 不可见);operator → <anonymous> 方法;私有 _ 名 → visibility;extends+with+implements 类(ref 种类/顺序)+ with-only 类;mixin on(无)+ mixin implements;具名 + 匿名扩展(以 ON 类型命名的类);abstract 类(无体成员 → kind function;abstract getter 不可见);sealed/base 修饰(普通类);enum(简单 + 增强:常定位的 ctor'd 常量、参数不遍历、with 缺口 vs implements ✓、; 后成员);typedef ×3(现代/遗留/泛型——零 refs);extension type(容器不可见、成员泄漏为裸 function);完整调用矩阵(§Calls 行逐字——含级联不可见、?.、const-object 参数、链重编码、插值调用、$x vs ${x});静态读(X.memberX.method() 双发、3 段无、case-pattern 缺口);局部形状(局部/未初始化/lambda/局部函数——精确钉死 §double-walk 交错);fn-refs(位置参数、具名参数不捕获、rhs + param-storage-skip + shadow 假阳性、list、pair、顶层/static 裸 identifier varinit、局部 alias 不捕获、未定义名被 gate、file/class 孪生);value refs(目标 CAPS + kName;经兄弟拉取的读者;constant→constant;${X} 读 + $X 非读;X.member 成员半边读;局部 shadow prune;未初始化局部 prune;assignment 非 prune;小写非目标);符号前 unicode 注释 + 字符串(UTF-16 列)。
  • CRLF 变体内存派生(kernel-tsjs-parity 模式)。
  • Defer fixture #1switch (x) { Init() => 1, _ => 0 }(空对象模式)——内核 defer(defer:),wasm 提供恢复输出。Defer fixture #2library; 头文件——同合约。
  • 生成文件 fixturex.g.dart 命名)——提取正常但无 function_refs/value-refs(isGeneratedFile skip)。
  • Parity 扫频scripts/kernel-parity.mjs <dir>,顺序敏感的完整对象,--max-deferral 0.3):shelf(小,99 文件)、bloc(中,616 文件——Flutter widgets + 纯 dart 包 + 生成文件族群)、flutter(大,6,465 文件——框架;defer 重臂)。预期每个非 defer 文件 0-diff,且 defer 计数接近上表。然后同一三仓库 full-init dump-diff 字节一致(内核臂 vs CODEGRAPH_KERNEL=0scripts/dump-graph.mjs,cmp)。
  • 普查抽查(double-walk 双保险):bloc 内核臂索引后,select kind, count(*) from nodes where language='dart' group by kind 必须与 wasm 臂精确相等——重复节点行正是"有帮助的"去重在 dump 门禁运行前静默分叉的地方。
  • 套件:torture + CRLF + defer fixture(空对象模式文件——内核 defer、wasm 输出顶上)+ library; defer 形状;CODEGRAPH_KERNEL_EXPECT=1 下全套 ×2 绿。
  • DEFAULT_ROUTED += 'dart'kernel/index.ts)仅在以上全部之后;changelog 随现有内核条目走。路由后性能 sanity:三个门禁仓库全走 raw 路径(§arch-2);混合 Vue+dart 仓库是解码路径冒烟检查。dart 加速落在约 79–97% 文件上(defer 成本)。

当前仓库中的落地证据

截至本文撰写,清单描述的移植产物已可在仓库中直接查验:

  • codegraph-kernel/src/dart.rs:约 1,550 行的 Rust walker,文件头注释明确自称"dart 路径的忠实 Rust 移植",并把"权威怪癖清单指向 docs/design/dart-kernel-port-checklist.md";注释逐条列出了 double-walk、callTypes 为空、ConfigT.load() 双发、operator <anonymous>、无名 ctor 跳过、前缀返回类型 bug 保留、enum with 静默、deferred import 不可见、具名参数不捕获、async*/sync* 非 async 等全部承重怪癖;
  • grammars.tsVENDORED_WASM_LANGS 已含 'dart',注释与清单 §Grammar prep 逐字对应(byte-copied 0.1.13 制品、未固定 github 依赖的去风险、crates.io fork 被拒);
  • codegraph-kernel/grammars/dart/:vendored C 三件套(parser.c、scanner.c、tree_sitter/ 头文件)+ LICENSE,即内核 cc::Build 编译源;
  • langs.rsextern "C" { fn tree_sitter_dart() }"dart" => ...LanguageFn::from_raw 注册分支已落地;
  • kernel-dart-parity.test.ts:测试头注释逐一对应清单的 fixture 设计——torture.dart 主清单、TortureDoubleWalk.dart(double-walk 同 id 重复节点与精确重复 ref 交错)、TortureFnrefDart.dart(fn-ref 捕获通道含具名参数非捕获与 file/class 孪生)、CRLF 变体(#1329)与 defer/生成文件 pin;并声明全仓库扫频由 scripts/kernel-parity.mjs 承载(shelf/bloc/flutter,--max-deferral 0.3);
  • tests/fixtures/kernel-parity/:torture.dart、TortureDoubleWalk.dart、TortureFnrefDart.dart、TortureMini.dart、TortureSigs.dart、TortureCtors.dart、TortureVrefDart.dart 等 fixture 均已入库。

这套"文档钉死行为 → Rust 转录复现 → 多仓库字节级扫频"的流程,是 codegraph 内核迁移中每个语言通行的范式;而 Dart 检查清单因其 double-walk 机制在全部已移植语言中独一份,成为该范式里信息密度最高的一份。

小结

Dart 移植的三条主线可以浓缩为:vendor 是字节拷贝去风险(wasm 字节不变、C 同 commit 编译、parity 测试守恒等)、walker 是行为转录(380 行 TS 配置的每个 hook 精确移植,callTypes 为空下由 extractBareCall + 静态成员 dart 分支承担全部调用/引用语义)、验证是顺序敏感的字节对等(double-walk 重复节点按观察交错全量发出,node_ids 不自行去重,defer 率即语法现实)。对想跟进其他语言移植的读者,这份清单的写法本身值得借鉴:每个声明都给出 file:line 锚点、探针产物名、"保留"与"死代码"的明确裁定,以及为什么安全的理由——这正是 bug-for-bug 移植能够被第三方复核的前提。

登录后查看全文
热门项目推荐
相关项目推荐