[dart2wasm] Add --dry-run flag to dart2wasm.

This new flag will run the CFE to create a kernel and then run a series of checks over the resulting kernel to look for errors that could block a wasm migration.

The compiler will then exit before actually starting the wasm compilation process. This means no output file will be emitted so callers must be aware of this.

This first CL is not meant to cover every check we could add here. It adds some initial checks and we can expand on this to include more in follow-up changes.

One of the checks implemented here is also provided by a lint. While ideally we would share code between lints and these checks, the delta in the CFE vs analyzer model makes that infeasible today.

Sample output:
```
Found incompatibilities with WebAssembly.

package:dryrun/test.dart 5:15 - Cannot test a JS value against String (3)
package:dryrun/test.dart 6:7 - JS interop class 'B' cannot extend Dart class 'A'. (2)
```

Bug: https://github.com/dart-lang/sdk/issues/60050
Change-Id: Ib2c8e3501cc42d57b86ebaa749359ce6c5dba974
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/437960
Commit-Queue: Nate Biggs <natebiggs@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Nate Biggs
2025-07-02 09:44:43 -07:00
committed by Commit Queue
parent 1af124cdc6
commit addbdcbd59
16 changed files with 663 additions and 3 deletions
+22 -2
View File
@@ -37,6 +37,7 @@ import 'package:wasm_builder/wasm_builder.dart' show Serializer;
import 'compiler_options.dart' as compiler;
import 'constant_evaluator.dart';
import 'deferred_loading.dart';
import 'dry_run.dart';
import 'dynamic_module_kernel_metadata.dart';
import 'dynamic_modules.dart';
import 'js/runtime_generator.dart' as js;
@@ -49,6 +50,12 @@ import 'translator.dart';
sealed class CompilationResult {}
abstract class CompilationDryRunResult extends CompilationResult {}
class CompilationDryRunError extends CompilationDryRunResult {}
class CompilationDryRunSuccess extends CompilationDryRunResult {}
class CompilationSuccess extends CompilationResult {
final Map<String, ({Uint8List moduleBytes, String? sourceMap})> wasmModules;
final String jsRuntime;
@@ -79,7 +86,9 @@ class CFECrashError extends CompilationError {
/// (We print them as soon as they are reported by CFE. i.e. we stream errors
/// instead of accumulating/batching all of them and reporting at the end.)
class CFECompileTimeErrors extends CompilationError {
CFECompileTimeErrors();
final Component? component;
CFECompileTimeErrors(this.component);
}
const List<String> _librariesToIndex = [
@@ -199,7 +208,18 @@ Future<CompilationResult> compileToModule(
} catch (e, s) {
return CFECrashError(e, s);
}
if (hadCompileTimeError) return CFECompileTimeErrors();
if (options.dryRun) {
final component = compilerResult?.component;
if (component == null) {
return CompilationDryRunError();
}
final summarizer = DryRunSummarizer(component);
final hasErrors = summarizer.summarize();
return hasErrors ? CompilationDryRunError() : CompilationDryRunSuccess();
}
if (hadCompileTimeError) {
return CFECompileTimeErrors(compilerResult?.component);
}
assert(compilerResult != null);
Component component = compilerResult!.component!;
+1
View File
@@ -29,6 +29,7 @@ class WasmCompilerOptions {
String? dumpKernelAfterCfe;
String? dumpKernelBeforeTfa;
String? dumpKernelAfterTfa;
bool dryRun = false;
factory WasmCompilerOptions.defaultOptions() =>
WasmCompilerOptions(mainUri: Uri(), outputFile: '');
+1
View File
@@ -26,6 +26,7 @@ final List<Option> options = [
defaultsTo: _d.translatorOptions.inlining),
Flag("minify", (o, value) => o.translatorOptions.minify = value,
defaultsTo: _d.translatorOptions.minify),
Flag("dry-run", (o, value) => o.dryRun = value, defaultsTo: _d.dryRun),
Flag("polymorphic-specialization",
(o, value) => o.translatorOptions.polymorphicSpecialization = value,
defaultsTo: _d.translatorOptions.polymorphicSpecialization),
+227
View File
@@ -0,0 +1,227 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_js_interop_checks/js_interop_checks.dart';
import 'package:collection/collection.dart';
import 'package:front_end/src/api_prototype/codes.dart'
show Message, LocatedMessage;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/target/targets.dart' show DiagnosticReporter;
import 'package:kernel/type_environment.dart';
enum _DryRunErrorCode {
noDartHtml(0),
noDartJs(1),
interopChecksError(2),
isTestValueError(3),
isTestTypeError(4),
isTestGenericTypeError(5),
;
const _DryRunErrorCode(this.code);
final int code;
}
class _DryRunError {
final _DryRunErrorCode code;
final String problemMessage;
final Uri? errorSourceUri;
final Location? errorLocation;
_DryRunError(this.code, this.problemMessage,
{this.errorSourceUri, this.errorLocation});
static String _locationToString(Uri? sourceUri, Location? location) {
if (sourceUri == null && location == null) return 'unknown location';
final uri = sourceUri ?? location?.file;
final lineCol =
location != null ? ' ${location.line}:${location.column}' : '';
return '$uri$lineCol';
}
@override
String toString() => '${_locationToString(errorSourceUri, errorLocation)} '
'- $problemMessage (${code.code})';
}
/// Runs several passes over the provided kernel to find any code in the
/// sources that could block a migration to WASM from JS.
///
/// At the end of execution, it emits a summary to stdout that looks like:
/// ```
/// Found incompatibilities with WebAssembly.
///
/// package:dryrun/test.dart 5:15 - Cannot test a JS value against String (3)
/// package:dryrun/test.dart 6:7 - JS interop class 'B' cannot extend Dart class 'A'. (2)
/// ````
class DryRunSummarizer {
final Component component;
late final CoreTypes coreTypes;
late final ClassHierarchy classHierarchy;
DryRunSummarizer(this.component) {
coreTypes = CoreTypes(component);
classHierarchy = ClassHierarchy(component, coreTypes);
}
static const Map<String, _DryRunErrorCode> _disallowedDartUris = {
'dart:html': _DryRunErrorCode.noDartHtml,
'dart:js': _DryRunErrorCode.noDartJs,
// 'dart:ffi' is handled by interop checks.
};
List<_DryRunError> _analyzeImports() {
final errors = <_DryRunError>[];
for (final library in component.libraries) {
if (library.importUri.scheme == 'dart') continue;
for (final dep in library.dependencies) {
final depLib = dep.importedLibraryReference.asLibrary;
final code = _disallowedDartUris[depLib.importUri.toString()];
if (code != null) {
errors.add(_DryRunError(code, '${depLib.importUri} unsupported',
errorSourceUri: library.importUri, errorLocation: dep.location));
}
}
}
return errors;
}
List<_DryRunError> _interopChecks() {
final collector = _CollectingDiagnosticReporter(component);
final reporter = JsInteropDiagnosticReporter(collector);
// These checks will already have been done by the CFE but the message
// format the CFE provides for those errors makes it hard to identify them
// as interop-specific errors. Instead we rerun and collect any errors here.
component.accept(JsInteropChecks(coreTypes, classHierarchy, reporter,
JsInteropChecks.getNativeClasses(component),
isDart2Wasm: true));
return collector.errors;
}
List<_DryRunError> _analyzeComponent() {
final analyzer = _AnalysisVisitor(coreTypes, classHierarchy);
component.accept(analyzer);
return analyzer.errors;
}
bool summarize() {
final errors = [
..._analyzeImports(),
..._interopChecks(),
..._analyzeComponent(),
];
if (errors.isNotEmpty) {
print('Found incompatibilities with WebAssembly.\n');
print(errors.join('\n'));
return true;
}
return false;
}
}
class _CollectingDiagnosticReporter
extends DiagnosticReporter<Message, LocatedMessage> {
final Component component;
final List<_DryRunError> errors = <_DryRunError>[];
_CollectingDiagnosticReporter(this.component);
@override
void report(Message message, int charOffset, int length, Uri? fileUri,
{List<LocatedMessage>? context}) {
final libraryUri = fileUri != null
? component.libraries
.firstWhereOrNull((e) => e.fileUri == fileUri)
?.importUri
: null;
final location =
fileUri != null ? component.getLocation(fileUri, charOffset) : null;
errors.add(_DryRunError(
_DryRunErrorCode.interopChecksError, message.problemMessage,
errorSourceUri: libraryUri ?? fileUri, errorLocation: location));
}
}
class _AnalysisVisitor extends RecursiveVisitor {
Library? _enclosingLibrary;
late StaticTypeContext _context;
final TypeEnvironment _typeEnvironment;
final DartType _jsAnyType;
final List<_DryRunError> errors = [];
_AnalysisVisitor(CoreTypes coreTypes, ClassHierarchy hierarchy)
: _typeEnvironment = TypeEnvironment(coreTypes, hierarchy),
_jsAnyType = ExtensionType(
coreTypes.index.getExtensionType('dart:js_interop', 'JSAny'),
Nullability.nullable);
@override
void visitLibrary(Library node) {
if (node.importUri.scheme == 'dart') return;
_enclosingLibrary = node;
_context = StaticTypeContext.forAnnotations(node, _typeEnvironment);
super.visitLibrary(node);
_enclosingLibrary = null;
}
@override
void visitProcedure(Procedure node) {
_context = StaticTypeContext(node, _typeEnvironment);
super.visitProcedure(node);
}
@override
void visitIsExpression(IsExpression node) {
final operandStaticType = node.operand.getStaticType(_context);
if (_typeEnvironment.isSubtypeOf(operandStaticType, _jsAnyType)) {
errors.add(_DryRunError(
_DryRunErrorCode.isTestValueError,
'Should not perform an `is` test on a JS value. Use `isA` with a JS '
'value type instead.',
errorSourceUri: _enclosingLibrary?.importUri,
errorLocation: node.location));
}
if (_typeEnvironment.isSubtypeOf(node.type, _jsAnyType)) {
errors.add(_DryRunError(
_DryRunErrorCode.isTestTypeError,
'Should not perform an `is` test against a JS value type. '
'Use `isA` instead.',
errorSourceUri: _enclosingLibrary?.importUri,
errorLocation: node.location));
} else if (_hasJsTypeArguments(node.type)) {
errors.add(_DryRunError(
_DryRunErrorCode.isTestGenericTypeError,
'Should not perform an `is` test against a generic DartType with JS '
'type arguments.',
errorSourceUri: _enclosingLibrary?.importUri,
errorLocation: node.location));
}
super.visitIsExpression(node);
}
bool _hasJsTypeArguments(DartType type) {
// Check InterfaceType and ExtensionType
if (type is TypeDeclarationType) {
final arguments = type.typeArguments;
if (arguments.any((e) => _typeEnvironment.isSubtypeOf(e, _jsAnyType))) {
return true;
}
return arguments.any(_hasJsTypeArguments);
} else if (type is RecordType) {
final fields = type.positional.followedBy(type.named.map((t) => t.type));
if (fields.any((e) => _typeEnvironment.isSubtypeOf(e, _jsAnyType))) {
return true;
}
return fields.any(_hasJsTypeArguments);
}
return false;
}
}
+9 -1
View File
@@ -78,9 +78,17 @@ Future<int> generateWasm(WasmCompilerOptions options,
CompilationResult result =
await compileToModule(options, relativeSourceMapUrlMapper, (message) {
printDiagnosticMessage(message, errorPrinter);
if (!options.dryRun) printDiagnosticMessage(message, errorPrinter);
});
if (result is CompilationDryRunResult) {
assert(options.dryRun);
if (result is CompilationDryRunError) {
return 254;
}
return 0;
}
// If the compilation to wasm failed we use appropriate exit codes recognized
// by our test infrastructure. We use the same exit codes as the VM does. See:
// runtime/bin/error_exit.h:kDartFrontendErrorExitCode
+2
View File
@@ -21,4 +21,6 @@ dependencies:
# Use 'any' constraints here; we get our versions from the DEPS file.
dev_dependencies:
expect: any
js: any
lints: any
@@ -0,0 +1,299 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:expect/expect.dart';
final dartAotExecutable = Uri.parse(Platform.resolvedExecutable)
.resolve('dartaotruntime')
.toFilePath();
final dart2wasmSnapshot = Uri.parse(Platform.resolvedExecutable)
.resolve('snapshots/dart2wasm_product.snapshot')
.toFilePath();
final platformDill = Uri.parse(Platform.resolvedExecutable)
.resolve('../lib/_internal/dart2wasm_platform.dill')
.toFilePath();
class TestExpectation {
final int expectedErrorCode;
final String? expectedErrorSubstring;
final int lineNumber;
TestExpectation(
this.expectedErrorCode, this.expectedErrorSubstring, this.lineNumber);
factory TestExpectation.fromLine(String line, int lineNumber) {
final errorText = line.replaceAll('// DRY_RUN:', '').trim().split(',');
final expectedErrorCode = int.parse(errorText[0].trim());
final expectedErrorSubstring =
errorText.length > 1 ? errorText.sublist(1).join(',').trim() : null;
return TestExpectation(
expectedErrorCode, expectedErrorSubstring, lineNumber);
}
}
class TestFinding {
final int errorCode;
final String problemMessage;
final Uri? errorSourceUri;
final int? errorLine;
final int? errorColumn;
TestFinding(this.errorCode, this.problemMessage, this.errorSourceUri,
this.errorLine, this.errorColumn);
factory TestFinding.fromLine(String line) {
final parts = line.split(' - ');
final locationInfo = parts[0].split(' ');
final uri = locationInfo[0].trim();
final lineCol = locationInfo[1].split(':');
final lineNumber = int.parse(lineCol[0].trim());
final columnNumber = int.parse(lineCol[1].trim());
final problemMessage = parts[1];
final errorCodeStart = problemMessage.lastIndexOf('(');
final errorCodeEnd = problemMessage.lastIndexOf(')');
final errorCode =
int.parse(problemMessage.substring(errorCodeStart + 1, errorCodeEnd));
return TestFinding(
errorCode,
problemMessage.substring(0, errorCodeStart).trim(),
Uri.parse(uri),
lineNumber,
columnNumber);
}
}
enum Status { pass, fail, crash }
class TestResults {
final String name;
final Status status;
final Duration time;
final String details;
TestResults(this.name, this.status, this.time, this.details);
String toRecordJson(String configuration) {
final outcome = switch (status) {
Status.pass => 'Pass',
Status.crash => 'Crash',
Status.fail => 'RuntimeError',
};
return jsonEncode({
'name': 'dry_run/$name',
'configuration': configuration,
'suite': 'dry_run',
'test_name': name,
'time_ms': time.inMilliseconds,
'expected': 'Pass',
'result': outcome,
'matches': status == Status.pass,
});
}
String toLogJson(String configuration) {
final outcome = switch (status) {
Status.pass => 'Pass',
Status.crash => 'Crash',
Status.fail => 'RuntimeError',
};
return jsonEncode({
'name': 'dry_run/$name',
'configuration': configuration,
'result': outcome,
'log': details,
});
}
}
class TestCase {
final Uri path;
final List<TestExpectation> expectations;
TestCase(this.path, this.expectations);
String get name => path.pathSegments.last.replaceAll('.dart', '');
}
Future<TestResults> runTest(TestCase testCase) async {
print('Executing test: ${testCase.name}');
final timer = Stopwatch();
timer.start();
ProcessResult result;
try {
result = await Process.run(dartAotExecutable, [
dart2wasmSnapshot,
testCase.path.toFilePath(),
'--platform=$platformDill',
'--dry-run',
'out.wasm',
]);
} catch (e, s) {
timer.stop();
return TestResults(testCase.name, Status.crash, timer.elapsed, '$e\n$s');
}
timer.stop();
try {
final exitCode = result.exitCode;
Expect.equals(testCase.expectations.isEmpty ? 0 : 254, exitCode);
final findings = _parseTestFindings(result.stdout);
_checkFindings(findings, testCase.expectations);
} catch (e, s) {
return TestResults(testCase.name, Status.fail, timer.elapsed, '$e\n$s');
}
return TestResults(testCase.name, Status.pass, timer.elapsed, '');
}
/// Generates a report of the test results in the JSON format
/// that is expected by our testing infrastructure.
int reportResults(
List<TestResults> results, {
String? configuration,
String? logDir,
}) {
bool fail = false;
print('Test results:');
for (var result in results) {
print(' ${result.name}: ${result.status}');
if (result.status != Status.pass) fail = true;
}
if (fail) print('Error: some tests failed');
if (logDir == null) {
print('Error: no output directory provided, logs won\'t be emitted.');
return 1;
}
if (configuration == null) {
print('Error: no configuration name provided, logs won\'t be emitted.');
return 1;
}
// Ensure the directory URI ends with a path separator.
var dirUri = Directory(logDir).uri;
File.fromUri(dirUri.resolve('results.json')).writeAsStringSync(
results.map((r) => '${r.toRecordJson(configuration)}\n').join(),
flush: true,
);
File.fromUri(dirUri.resolve('logs.json')).writeAsStringSync(
results
.where((r) => r.status != Status.pass)
.map((r) => '${r.toLogJson(configuration)}\n')
.join(),
flush: true,
);
print('Success: log files emitted under $dirUri');
return 0;
}
Future<List<TestCase>> _loadTestCases() async {
final testCaseDir = Directory.fromUri(Platform.script.resolve('testcases'));
final testCases = <TestCase>[];
for (final file in testCaseDir.listSync(recursive: true)) {
if (file is File && file.path.endsWith('.dart')) {
testCases.add(await _parseTestCase(file.uri));
}
}
return testCases;
}
Future<TestCase> _parseTestCase(Uri path) async {
final fileContents = await File.fromUri(path).readAsString();
final lines = fileContents.split('\n');
final expectations = <TestExpectation>[];
int lineIndex = 0;
for (final line in lines) {
if (line.contains('// DRY_RUN:')) {
final nextNonCommentLine =
lines.indexWhere((l) => !l.trim().startsWith('//'), lineIndex + 1) +
1;
expectations.add(TestExpectation.fromLine(line, nextNonCommentLine));
}
lineIndex++;
}
return TestCase(path, expectations);
}
List<TestFinding> _parseTestFindings(String result) {
final lines = result.split('\n');
final findings = <TestFinding>[];
// Skip header and newline.
for (final line in lines.skip(2)) {
if (line.isEmpty) continue;
findings.add(TestFinding.fromLine(line));
}
return findings;
}
void _checkFindings(
List<TestFinding> findings, List<TestExpectation> expectations) {
Expect.equals(
findings.length,
expectations.length,
'Incorrect number of findings. '
'Expected: ${expectations.length}, Actual: ${findings.length}');
for (final expectation in expectations) {
final lineNumber = expectation.lineNumber;
final lineFindings =
findings.where((finding) => finding.errorLine == lineNumber);
final matchingFinding = lineFindings.firstWhereOrNull(
(finding) => finding.errorCode == expectation.expectedErrorCode);
Expect.isNotNull(
matchingFinding,
'No finding found for expectation on line $lineNumber',
);
Expect.equals(
expectation.expectedErrorCode,
matchingFinding!.errorCode,
'Unexpected error code for expectation on line $lineNumber. '
'Expected: ${expectation.expectedErrorCode}, Actual: ${matchingFinding.errorCode}');
if (expectation.expectedErrorSubstring != null) {
Expect.contains(
expectation.expectedErrorSubstring!, matchingFinding.problemMessage);
}
}
}
Future<int> main(List<String> args) async {
final parser = ArgParser()
..addOption(
'configuration',
help: 'Configuration to use for reporting test results',
abbr: 'n',
)
..addOption(
'output-directory',
help: 'Location to emit the json-l result and log files',
)
..addFlag(
'verbose',
help: 'Show more information',
negatable: false,
abbr: 'v',
);
final parsedArgs = parser.parse(args);
final configuration = parsedArgs.option('configuration');
final outputDirectory = parsedArgs.option('output-directory');
final verbose = parsedArgs.flag('verbose');
final testCases = await _loadTestCases();
final results = <TestResults>[];
for (final testCase in testCases) {
final testResult = await runTest(testCase);
results.add(testResult);
if (verbose && testResult.status != Status.pass) {
print(testResult.details);
}
}
return reportResults(results,
configuration: configuration, logDir: outputDirectory);
}
@@ -0,0 +1,8 @@
include: package:lints/recommended.yaml
analyzer:
errors:
depend_on_referenced_packages: ignore
deprecated_member_use: ignore
invalid_runtime_check_with_js_interop_types: ignore
unused_import: ignore
@@ -0,0 +1,8 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// DRY_RUN: 2, dart:ffi
import 'dart:ffi';
void main() {}
@@ -0,0 +1,8 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// DRY_RUN: 0, dart:html
import 'dart:html';
void main() {}
@@ -0,0 +1,8 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// DRY_RUN: 1, dart:js
import 'dart:js';
void main() {}
@@ -0,0 +1,8 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// DRY_RUN: 2, package:js
import 'package:js/js.dart';
void main() {}
@@ -0,0 +1,34 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:js_interop';
void main() {
JSAny? jsValue;
// DRY_RUN: 3, Should not perform an `is` test on a JS value
if (jsValue is String) {
print(jsValue);
// DRY_RUN: 3, Should not perform an `is` test on a JS value
// DRY_RUN: 4, Should not perform an `is` test against a JS value type
} else if (jsValue is JSArray) {
print(jsValue);
}
Object? dartValue;
if (dartValue is String) {
print(dartValue);
// DRY_RUN: 4, Should not perform an `is` test against a JS value type
} else if (dartValue is JSArray) {
print(dartValue);
// DRY_RUN: 5, Should not perform an `is` test against a generic DartType with JS type arguments
} else if (dartValue is List<JSString>) {
print(dartValue);
// DRY_RUN: 5, Should not perform an `is` test against a generic DartType with JS type arguments
} else if (dartValue is (JSString,)) {
print(dartValue);
// DRY_RUN: 5, Should not perform an `is` test against a generic DartType with JS type arguments
} else if (dartValue is List<(JSString,)>) {
print(dartValue);
}
}
@@ -0,0 +1,14 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:js_interop';
void main() {
JSAny? jsValue;
if (jsValue.isA<JSString>()) {
print(jsValue);
} else if (jsValue.isA<JSArray>()) {
print(jsValue);
}
}
@@ -1031,6 +1031,11 @@ class CompileWasmCommand extends CompileSubcommandCommand {
final bool strip = args.flag('strip-wasm');
// When running in dry run mode there will not be any file emitted.
final isDryRun = extraCompilerOptions.any((e) => e.contains('dry-run'));
if (isDryRun) return 0;
if (runWasmOpt) {
final unoptFile = '$outputFileBasename.unopt.wasm';
File(outputFile).renameSync(unoptFile);
+9
View File
@@ -2663,6 +2663,15 @@
],
"shards": 8,
"fileset": "dart2wasm_hostasserts"
},
{
"name": "dart2wasm unit tests",
"script": "out/ReleaseX64/dart-sdk/bin/dart",
"testRunner": true,
"arguments": [
"pkg/dart2wasm/test/dry_run/dry_run_test.dart",
"-ndart2wasm-asserts-linux-chrome"
]
}
]
},