feat(kernel): R7b Dart walker — dart module, vendored-grammar-C d4d8f3e + wasm byte-copy vendor, dart default-routed (#1386)
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bdd687b49f
commit
d1b75a1a27
@@ -0,0 +1,39 @@
|
||||
/** Block dartdoc for blockDoc. */
|
||||
void blockDoc() {}
|
||||
|
||||
/// Line doc kept.
|
||||
// Plain comment also kept?
|
||||
void mixedDoc() {}
|
||||
|
||||
// Only plain.
|
||||
void plainDoc() {}
|
||||
|
||||
extension type Meters(double value) {
|
||||
double get km => value / 1000;
|
||||
void report() {
|
||||
print(km);
|
||||
}
|
||||
}
|
||||
|
||||
class Action {
|
||||
@override
|
||||
(int, String) reduce() => (1, 'a');
|
||||
void caller() {
|
||||
this.own();
|
||||
super.parent();
|
||||
generic<int>(5);
|
||||
final t = reduce();
|
||||
}
|
||||
}
|
||||
|
||||
enum Flag { on, off }
|
||||
|
||||
void flagUse(Flag f) {
|
||||
final v = Flag.on;
|
||||
switch (f) {
|
||||
case Flag.off:
|
||||
use(v);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
final topFinal = seedValue();
|
||||
const topConst = 42;
|
||||
var topVar = 7;
|
||||
int topTyped = 8;
|
||||
final multiA = 1, multiB = 2;
|
||||
|
||||
int seedValue() => 9;
|
||||
|
||||
void hostFn() {
|
||||
void localFn(int n) {
|
||||
inner(n);
|
||||
}
|
||||
localFn(3);
|
||||
int localWithNew() {
|
||||
final w = new Holder(1);
|
||||
return w.n;
|
||||
}
|
||||
localWithNew();
|
||||
}
|
||||
|
||||
class Holder {
|
||||
final int n;
|
||||
final untypedInit = 5;
|
||||
static var mutable = 3;
|
||||
Holder(this.n);
|
||||
Holder.other() : n = compute();
|
||||
void useNew() {
|
||||
final h = new Holder(2);
|
||||
void methodLocal() {
|
||||
h.touch();
|
||||
}
|
||||
methodLocal();
|
||||
}
|
||||
int get computed => helperCall();
|
||||
}
|
||||
|
||||
void takeConst() {
|
||||
pad(const EdgeInsets.all(8.0));
|
||||
make(const Holder(3));
|
||||
}
|
||||
|
||||
Future<int> asyncFn() async => 1;
|
||||
Stream<int> genFn() async* {
|
||||
yield 1;
|
||||
}
|
||||
Iterable<int> syncGen() sync* {
|
||||
yield 2;
|
||||
}
|
||||
|
||||
/// Doc for annotated.
|
||||
@deprecated
|
||||
void annotated() {}
|
||||
|
||||
@Deprecated('use other')
|
||||
class OldClass {}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void pragged() {}
|
||||
|
||||
class Cfg {
|
||||
static const retries = 3;
|
||||
}
|
||||
|
||||
void reader() {
|
||||
use(Cfg.retries);
|
||||
final msg = 'retry $topConst times ${topFinal}';
|
||||
print(msg);
|
||||
}
|
||||
|
||||
void shadower() {
|
||||
final topConst = 1;
|
||||
use(topConst);
|
||||
}
|
||||
|
||||
void refTaker() {
|
||||
register(seedValue);
|
||||
obj.cb = seedValue;
|
||||
final table = [seedValue, hostFn];
|
||||
final map = {'k': seedValue, 'j': notDefinedHere};
|
||||
reg2(cb: seedValue);
|
||||
final alias = seedValue;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
void assigned() {}
|
||||
void listed() {}
|
||||
void mapped() {}
|
||||
void selfStore() {}
|
||||
class H {
|
||||
var cb;
|
||||
void wire(dynamic selfStore) {
|
||||
this.cb = assigned;
|
||||
cb = selfStore;
|
||||
}
|
||||
}
|
||||
final tableTop = [listed];
|
||||
final mapTop = {'k': mapped};
|
||||
void aliased() {}
|
||||
final aliasTop = aliased;
|
||||
class K {
|
||||
static final aliasStatic = aliased;
|
||||
}
|
||||
void onlyNamed() {}
|
||||
void otherFn() {}
|
||||
void positional() {}
|
||||
void taker() {
|
||||
reg(cb: onlyNamed);
|
||||
reg2(handler: otherFn, plain: 1);
|
||||
reg3(positional);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'dart:async';
|
||||
import 'package:foo/bar.dart' as bar show Baz hide Qux;
|
||||
import 'pkg.dart' deferred as lazy;
|
||||
export 'src/out.dart' show Pub;
|
||||
part 'part1.dart';
|
||||
|
||||
/// Doc line one.
|
||||
/// Doc line two.
|
||||
void topFn(int a, String b) {
|
||||
var local = 5;
|
||||
int typed = 6;
|
||||
final con = 7;
|
||||
int uninit;
|
||||
uninit = 8;
|
||||
local = a;
|
||||
helper(a);
|
||||
obj.method(a);
|
||||
Config.setting;
|
||||
Config.load();
|
||||
w?.render();
|
||||
list..add(1)..add(2);
|
||||
final w2 = Widget(1);
|
||||
final w3 = new Widget(2);
|
||||
const e = EdgeInsets.all(8.0);
|
||||
Foo.create().run();
|
||||
lower().chain();
|
||||
print('sum ${a + compute()} $local');
|
||||
}
|
||||
|
||||
// plain comment
|
||||
int get topGetter => 42;
|
||||
set topSetter(int v) {}
|
||||
|
||||
class Widget extends Base with Mix1, Mix2 implements Draw, Paint {
|
||||
static const int kMax = 10;
|
||||
static final shared = Widget(0);
|
||||
final int size;
|
||||
int count = 0;
|
||||
var loose;
|
||||
late String name;
|
||||
Widget(this.size);
|
||||
Widget.named(this.size) { init(); }
|
||||
factory Widget.create() => Widget(1);
|
||||
Widget._() : size = 0;
|
||||
@override
|
||||
void render(Canvas c) { c.draw(); }
|
||||
static Widget make() => Widget(3);
|
||||
int get area => size * size;
|
||||
set area(int v) { count = v; }
|
||||
Future<void> load() async { await fetch(); }
|
||||
operator +(Widget o) => Widget(size + o.size);
|
||||
}
|
||||
|
||||
mixin Mix1 on Base {
|
||||
void mixMethod() { helper(0); }
|
||||
}
|
||||
|
||||
extension WidgetExt on Widget {
|
||||
void extMethod() { render(null); }
|
||||
}
|
||||
|
||||
enum Color { red, green, blue }
|
||||
|
||||
enum Status with Mix1 implements Draw {
|
||||
ok(200),
|
||||
err(500);
|
||||
final int code;
|
||||
const Status(this.code);
|
||||
bool get good => code < 400;
|
||||
static Status parse(int c) => ok;
|
||||
}
|
||||
|
||||
typedef IntFn = int Function(int);
|
||||
typedef void OldStyle(int x);
|
||||
|
||||
abstract class Base {}
|
||||
class Draw {}
|
||||
@@ -0,0 +1,42 @@
|
||||
library my.lib;
|
||||
|
||||
part of 'other.dart';
|
||||
|
||||
void params({int? a, required Widget child, String note = 'x'}) {}
|
||||
void optional([int b = 0, Widget? w]) {}
|
||||
void fnTypedParam(void cb(int x), int Function(String) modern) {}
|
||||
num numRet(num n) => n;
|
||||
dynamic dynRet(dynamic d) => d;
|
||||
Object objRet(Object o) => o;
|
||||
double dblRet(double d) => d;
|
||||
|
||||
external void externalFn();
|
||||
|
||||
class Redir {
|
||||
Redir();
|
||||
Redir.a() : this();
|
||||
const factory Redir.b() = RedirImpl;
|
||||
}
|
||||
|
||||
void lambdas(List<int> xs) {
|
||||
xs.forEach((e) => use(e));
|
||||
final f = (int a) {
|
||||
helper(a);
|
||||
};
|
||||
f(1);
|
||||
xs.map((e) => e * 2).toList();
|
||||
}
|
||||
|
||||
void bodyTypes(Object x) {
|
||||
if (x is Widget) {
|
||||
use(x);
|
||||
}
|
||||
final y = x as Widget;
|
||||
final list = <Widget>[];
|
||||
final map = <String, Widget>{};
|
||||
throw StateError('bad');
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void doubleAnno() {}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:foo/util.dart' as util;
|
||||
|
||||
const SHARED_MAX = 10;
|
||||
const kLimit = 20;
|
||||
final DERIVED = SHARED_MAX + 1;
|
||||
const low = 30;
|
||||
|
||||
class Table {
|
||||
static const COL_LIMIT = 5;
|
||||
static const plain = 6;
|
||||
void reads() {
|
||||
use(SHARED_MAX);
|
||||
use(Table.COL_LIMIT);
|
||||
log('cap $kLimit and ${SHARED_MAX}');
|
||||
}
|
||||
void shadowed() {
|
||||
final SHARED_MAX = 1;
|
||||
use(SHARED_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
void freeReader() {
|
||||
use(kLimit);
|
||||
}
|
||||
|
||||
void uninitShadow() {
|
||||
int DERIVED;
|
||||
DERIVED = 2;
|
||||
use(DERIVED);
|
||||
}
|
||||
|
||||
void assignOnly() {
|
||||
low = 5;
|
||||
}
|
||||
|
||||
void prefixedCalls() {
|
||||
util.helper(1);
|
||||
util.Config.load();
|
||||
Widget.named(2);
|
||||
}
|
||||
|
||||
/// Doc on classy.
|
||||
class Classy {}
|
||||
|
||||
/// Doc on num const?
|
||||
const DOCED = 1;
|
||||
|
||||
/// Doc on enum.
|
||||
enum E2 { a }
|
||||
|
||||
/// Doc broken.
|
||||
@override
|
||||
void afterAnno() {}
|
||||
|
||||
/// Kept doc.
|
||||
void unicodeNext() {}
|
||||
|
||||
// π and émoji 🎯 in a comment
|
||||
void afterUnicode(String s) {
|
||||
emit('café ☕ done');
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/// File-level doc for torture (glued to import? no — imports precede nothing).
|
||||
import 'dart:async';
|
||||
import 'package:torture/other.dart' as other show OtherClass;
|
||||
export 'src/reexported.dart' hide Hidden;
|
||||
part 'torture_part.dart';
|
||||
|
||||
/// Doc line one.
|
||||
/// Doc line two.
|
||||
void topLevel(int count, String label) {
|
||||
helper(count);
|
||||
}
|
||||
|
||||
/** Block dartdoc kept. */
|
||||
int blockDoc() => 1;
|
||||
|
||||
// Plain comment doc.
|
||||
String plainDoc() => 'x';
|
||||
|
||||
/// Broken by annotation.
|
||||
@deprecated
|
||||
void annotated() {}
|
||||
|
||||
@Deprecated('with args')
|
||||
@pragma('vm:entry-point')
|
||||
void doubleAnno() {}
|
||||
|
||||
const SHARED_MAX = 10;
|
||||
final DERIVED_VAL = SHARED_MAX + 1;
|
||||
const lowercase_const = 1;
|
||||
final typedTop = compute();
|
||||
var topVar = 5;
|
||||
int topTyped = 6;
|
||||
final multiA = 1, multiB = 2;
|
||||
|
||||
int get topGetter => 7;
|
||||
set topSetter(int v) {}
|
||||
|
||||
Future<String> asyncFn() async => 'a';
|
||||
Stream<int> genStar() async* {
|
||||
yield 1;
|
||||
}
|
||||
Iterable<int> syncStar() sync* {
|
||||
yield 2;
|
||||
}
|
||||
|
||||
num numback(num n) => n;
|
||||
dynamic dyn(dynamic d) => d;
|
||||
Object obj(Object o) => o;
|
||||
List<WidgetT> listRet(Map<String, WidgetT> m) => [];
|
||||
WidgetT? nullableRet() => null;
|
||||
other.OtherClass prefixedRet() => other.OtherClass();
|
||||
T generic<T>(T v) => v;
|
||||
|
||||
external void externalFn();
|
||||
|
||||
void params({int? named, required WidgetT child, String note = 'x'}) {}
|
||||
void optionals([int pos = 0, WidgetT? w]) {}
|
||||
void fnTyped(void cb(int x), int Function(String) modern) {}
|
||||
|
||||
void bodyShapes(List<int> xs, Object o) {
|
||||
var local = 1;
|
||||
int typed = 2;
|
||||
final con = 3;
|
||||
int uninit;
|
||||
uninit = 4;
|
||||
local = uninit;
|
||||
helper(local);
|
||||
obj.method(local);
|
||||
this_like.deep.call3(local);
|
||||
ConfigT.load();
|
||||
ConfigT.setting;
|
||||
w?.render();
|
||||
y2..add(1)..add(2);
|
||||
final w1 = WidgetT(1);
|
||||
final w2 = new WidgetT(2);
|
||||
pad(const EdgeInsetsT.all(8.0));
|
||||
FactoryT.create().run();
|
||||
lower().chain();
|
||||
WidgetT.named(3).chainTail();
|
||||
xs.map((e) => e * 2).toList();
|
||||
xs.forEach((e) => use(e));
|
||||
final lam = (int a) {
|
||||
helper(a);
|
||||
};
|
||||
lam(5);
|
||||
void localFn(int n) {
|
||||
inner(n);
|
||||
}
|
||||
localFn(6);
|
||||
if (o is WidgetT) {
|
||||
use(o);
|
||||
}
|
||||
final cast = o as WidgetT;
|
||||
final tl = <WidgetT>[];
|
||||
throw StateError('bad');
|
||||
}
|
||||
|
||||
void interpolation(int count) {
|
||||
log('count $count and ${SHARED_MAX} via ${refresh()}');
|
||||
}
|
||||
|
||||
void refTaker() {
|
||||
register(topLevel);
|
||||
obj.cb = topLevel;
|
||||
final table = [topLevel, blockDoc];
|
||||
final m = {'k': topLevel, 'x': undefinedName};
|
||||
reg2(cb: topLevel);
|
||||
forward(topLevel: topLevel);
|
||||
final alias = topLevel;
|
||||
}
|
||||
|
||||
/// Class doc.
|
||||
@immutable
|
||||
class WidgetT extends BaseT with MixA, MixB implements DrawT {
|
||||
static const int K_MAX = 9;
|
||||
static final sharedInst = WidgetT(0);
|
||||
static var mutableStatic = 1;
|
||||
final int size;
|
||||
final untyped = 5;
|
||||
int counter = 0;
|
||||
late String title;
|
||||
WidgetT(this.size);
|
||||
WidgetT.named(this.size) {
|
||||
init();
|
||||
}
|
||||
WidgetT.bodilessNamed() : size = seed();
|
||||
factory WidgetT.create() => WidgetT(1);
|
||||
const factory WidgetT.redir() = WidgetT2;
|
||||
@override
|
||||
void render(CanvasT c) {
|
||||
c.draw();
|
||||
}
|
||||
static WidgetT make() => WidgetT(3);
|
||||
int get area => size * size;
|
||||
set area(int v) {
|
||||
counter = v;
|
||||
}
|
||||
bool get privado => _check();
|
||||
bool _check() => true;
|
||||
Future<void> load() async {
|
||||
await fetch();
|
||||
}
|
||||
WidgetT operator +(WidgetT o) => WidgetT(size + o.size);
|
||||
void useLocalNew() {
|
||||
final h = new HolderT(2);
|
||||
use(h);
|
||||
}
|
||||
void readsConst() {
|
||||
use(K_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
class WidgetT2 extends WidgetT {
|
||||
WidgetT2() : super(0);
|
||||
void _privateMethod() {}
|
||||
}
|
||||
|
||||
class BaseT {}
|
||||
|
||||
class DrawT {}
|
||||
|
||||
class OnlyMix with MixA {}
|
||||
|
||||
abstract class AbstractT {
|
||||
void mustImpl(WidgetT w);
|
||||
int get abstractGetter;
|
||||
}
|
||||
|
||||
sealed class ShapeT {}
|
||||
|
||||
class CircleT extends ShapeT {}
|
||||
|
||||
mixin MixA on BaseT {
|
||||
void mixMethod() {
|
||||
helper(1);
|
||||
}
|
||||
}
|
||||
|
||||
mixin MixB implements DrawT {
|
||||
int get mixGetter => 2;
|
||||
}
|
||||
|
||||
extension WidgetTExt on WidgetT {
|
||||
void extMethod() {
|
||||
render(CanvasT());
|
||||
}
|
||||
|
||||
int get extGetter => 4;
|
||||
}
|
||||
|
||||
extension on String {
|
||||
void anonExt() {}
|
||||
}
|
||||
|
||||
enum ColorT { red, green, blue }
|
||||
|
||||
/// Enum doc.
|
||||
enum StatusT with MixB implements DrawT {
|
||||
ok(200),
|
||||
err(500);
|
||||
|
||||
final int code;
|
||||
const StatusT(this.code);
|
||||
bool get good => code < 400;
|
||||
static StatusT parse(int c) => ok;
|
||||
}
|
||||
|
||||
typedef IntFn = int Function(int);
|
||||
typedef void LegacyCb(int x);
|
||||
typedef MapAlias = Map<String, WidgetT>;
|
||||
|
||||
extension type MetersT(double value) {
|
||||
double get km => value / 1000;
|
||||
}
|
||||
|
||||
void patternUser(Object o) {
|
||||
final v = ColorT.red;
|
||||
switch (o) {
|
||||
case ColorT.blue:
|
||||
use(v);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
final (a, b) = (1, 2);
|
||||
use(a);
|
||||
}
|
||||
|
||||
// π unicode: “smart quotes” précède — column check ☕
|
||||
void afterUnicode(String seance) {
|
||||
emit('café ☕ done');
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Kernel↔wasm Dart extraction parity (R7b batch 4 of the kernel migration —
|
||||
* the final R7b language).
|
||||
*
|
||||
* Asserts the native walker (codegraph-kernel/src/dart.rs) produces the SAME
|
||||
* ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
|
||||
* unresolved refs compared as canonicalized multisets — over the checked-in
|
||||
* fixtures (torture.dart: the master inventory — imports incl. deferred
|
||||
* invisibility, dartdoc in all three comment forms with the
|
||||
* annotation-broken chain, stacked annotations in reverse order, the
|
||||
* static_final_declaration constants hook, the full ctor set with the
|
||||
* unnamed-ctor skip and named-ctor renaming, operator methods as
|
||||
* `<anonymous>`, the extractBareCall matrix incl. cascade invisibility and
|
||||
* `?.`-as-`.`, the `ConfigT.load()` calls+references double emission,
|
||||
* extends/with/implements ref kinds, enum `with` silence, anonymous
|
||||
* extensions named after the ON type, value-ref targets with the sibling
|
||||
* body pull; TortureDoubleWalk.dart: THE SIBLING-BODY DOUBLE-WALK — the
|
||||
* duplicate local-function nodes with the same id under different parents
|
||||
* and the exact duplicated-ref interleave; TortureFnrefDart.dart: fn-ref
|
||||
* capture channels incl. named-argument non-capture and the file/class
|
||||
* twins; TortureMini/TortureSigs/TortureCtors/TortureVrefDart: signatures
|
||||
* verbatim, prefixed-return-type prefix bug, const factories invisible,
|
||||
* value-ref matrix with `$X` vs `${X}` asymmetry) and their CRLF variants
|
||||
* (derived in-memory — #1329), plus defer and generated-file pins.
|
||||
*
|
||||
* The full-repo sweeps live in scripts/kernel-parity.mjs (shelf/bloc/flutter
|
||||
* with --max-deferral 0.3); this suite keeps the invariant alive in
|
||||
* `npm test`. Skips when no kernel binary is staged; CODEGRAPH_KERNEL_EXPECT=1
|
||||
* turns that into a failure.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
||||
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
|
||||
import type { ExtractionResult } from '../src/types';
|
||||
|
||||
const KERNEL_PATH = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'codegraph-kernel',
|
||||
'prebuilds',
|
||||
`${process.platform}-${process.arch}`,
|
||||
'codegraph-kernel.node'
|
||||
);
|
||||
const kernelBuilt = fs.existsSync(KERNEL_PATH);
|
||||
|
||||
const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'kernel-parity');
|
||||
|
||||
function canon(result: ExtractionResult): { nodes: string[]; edges: string[]; refs: string[] } {
|
||||
return {
|
||||
nodes: result.nodes
|
||||
.map(({ updatedAt: _u, ...n }) => JSON.stringify(n, Object.keys(n).sort()))
|
||||
.sort(),
|
||||
edges: result.edges.map((e) => JSON.stringify(e, Object.keys(e).sort())).sort(),
|
||||
refs: result.unresolvedReferences
|
||||
.map((r) => JSON.stringify(r, Object.keys(r).sort()))
|
||||
.sort(),
|
||||
};
|
||||
}
|
||||
|
||||
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
|
||||
let savedEnv: Record<string, string | undefined>;
|
||||
|
||||
describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => {
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['dart']);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
||||
resetKernelForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const k of ENV_KEYS) {
|
||||
if (savedEnv[k] === undefined) delete process.env[k];
|
||||
else process.env[k] = savedEnv[k];
|
||||
}
|
||||
resetKernelForTests();
|
||||
});
|
||||
|
||||
function assertParity(filePath: string, source: string, minNodes = 2): ExtractionResult {
|
||||
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
const viaKernel = tryKernelExtract(filePath, source, 'dart');
|
||||
expect(viaKernel, `kernel extraction failed for ${filePath}`).not.toBeNull();
|
||||
|
||||
process.env.CODEGRAPH_KERNEL = '0';
|
||||
const viaWasm = extractFromSource(filePath, source, 'dart');
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
|
||||
const k = canon(viaKernel!);
|
||||
const w = canon(viaWasm);
|
||||
expect(k.nodes, `${filePath}: nodes`).toEqual(w.nodes);
|
||||
expect(k.edges, `${filePath}: edges`).toEqual(w.edges);
|
||||
expect(k.refs, `${filePath}: refs`).toEqual(w.refs);
|
||||
expect(viaWasm.nodes.length).toBeGreaterThanOrEqual(minNodes);
|
||||
return viaKernel!;
|
||||
}
|
||||
|
||||
const FIXTURES = [
|
||||
['torture.dart', 30],
|
||||
['TortureDoubleWalk.dart', 5],
|
||||
['TortureFnrefDart.dart', 3],
|
||||
['TortureMini.dart', 5],
|
||||
['TortureSigs.dart', 4],
|
||||
['TortureCtors.dart', 3],
|
||||
['TortureVrefDart.dart', 4],
|
||||
] as const;
|
||||
|
||||
for (const [file, minNodes] of FIXTURES) {
|
||||
it(`${file}: parity`, () => {
|
||||
const src = fs.readFileSync(path.join(FIXTURE_DIR, file), 'utf8');
|
||||
assertParity(`fixtures/${file}`, src, minNodes);
|
||||
});
|
||||
|
||||
it(`${file}: CRLF parity`, () => {
|
||||
const src = fs.readFileSync(path.join(FIXTURE_DIR, file), 'utf8');
|
||||
const crlf = src.replace(/(?<!\r)\n/g, '\r\n');
|
||||
assertParity(`fixtures/${file} (crlf)`, crlf, minNodes);
|
||||
});
|
||||
}
|
||||
|
||||
it('double-walk pins: duplicate local-fn nodes share an id; refs interleave', () => {
|
||||
const src = fs.readFileSync(path.join(FIXTURE_DIR, 'TortureDoubleWalk.dart'), 'utf8');
|
||||
const result = assertParity('fixtures/TortureDoubleWalk.dart', src, 5);
|
||||
// Local functions are minted TWICE — same (kind,name,line) → the SAME id
|
||||
// — once under the enclosing function, once under the file/class (the
|
||||
// sibling-body revisit). A dedupe here would silently diverge.
|
||||
const byId = new Map<string, number>();
|
||||
for (const n of result.nodes) byId.set(n.id, (byId.get(n.id) ?? 0) + 1);
|
||||
const dupes = [...byId.values()].filter((c) => c > 1);
|
||||
expect(dupes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('generated files extract but skip fn-ref and value-ref flushes', () => {
|
||||
const src = fs.readFileSync(path.join(FIXTURE_DIR, 'TortureVrefDart.dart'), 'utf8');
|
||||
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
const viaKernel = tryKernelExtract('lib/model.g.dart', src, 'dart');
|
||||
expect(viaKernel).not.toBeNull();
|
||||
process.env.CODEGRAPH_KERNEL = '0';
|
||||
const viaWasm = extractFromSource('lib/model.g.dart', src, 'dart');
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
const k = canon(viaKernel!);
|
||||
const w = canon(viaWasm);
|
||||
expect(k.nodes).toEqual(w.nodes);
|
||||
expect(k.edges).toEqual(w.edges);
|
||||
expect(k.refs).toEqual(w.refs);
|
||||
// The skips: no function_ref refs, no valueRef edges.
|
||||
expect(viaKernel!.unresolvedReferences.some((r) => r.referenceKind === 'function_ref')).toBe(
|
||||
false
|
||||
);
|
||||
expect(viaKernel!.edges.some((e) => e.metadata?.valueRef === true)).toBe(false);
|
||||
});
|
||||
|
||||
it('empty object patterns defer (the dominant dart-3 error class)', () => {
|
||||
const broken = 'int f(Object x) => switch (x) { Init() => 1, _ => 0 };\n';
|
||||
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
expect(tryKernelExtract('lib/pat.dart', broken, 'dart')).toBeNull();
|
||||
process.env.CODEGRAPH_KERNEL = '0';
|
||||
const viaWasm = extractFromSource('lib/pat.dart', broken, 'dart');
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
expect(viaWasm.nodes.some((n) => n.kind === 'file')).toBe(true);
|
||||
});
|
||||
|
||||
it('unnamed `library;` defers', () => {
|
||||
const broken = '/// Doc.\nlibrary;\n\nvoid f() {}\n';
|
||||
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
expect(tryKernelExtract('lib/lib.dart', broken, 'dart')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ const kernelBuilt = fs.existsSync(KERNEL_PATH);
|
||||
|
||||
// Every kernel-capable language. `jsx` shares the javascript grammar on BOTH
|
||||
// paths (langs.rs mirrors WASM_GRAMMAR_FILES), so the distinct grammars are:
|
||||
const GRAMMAR_LANGUAGES: Language[] = ['typescript', 'tsx', 'javascript', 'java', 'python', 'go', 'c', 'cpp', 'rust', 'csharp', 'ruby', 'php', 'swift', 'kotlin', 'r', 'lua', 'luau', 'scala'];
|
||||
const GRAMMAR_LANGUAGES: Language[] = ['typescript', 'tsx', 'javascript', 'java', 'python', 'go', 'c', 'cpp', 'rust', 'csharp', 'ruby', 'php', 'swift', 'kotlin', 'r', 'lua', 'luau', 'scala', 'dart'];
|
||||
|
||||
describe.skipIf(!kernelBuilt)('kernel↔wasm grammar parity', () => {
|
||||
beforeAll(async () => {
|
||||
|
||||
Reference in New Issue
Block a user