[dart2wasm] Fix handling of --define/-D

When parsing `--define` or `-D` arguments don't split the the value by
commas.

This is consistent with how dart2js handles `-D`, but inconsistent with
how VM handles it.

Example:

    void main() {
      print(const String.fromEnvironment("FOO"));
    }

When compiled with `dart compile js -DFOO="a, b"` and run, dart2js
prints

    a, b

VM prints (when compiled to exe)

    a

Between these two, I think dart2js' behavior is more common, so we
follow dart2js.

Also update compile_benchmark to avoid splitting a single argument "a b"
into "a" and "b" when parsing the arguments and then splicing them back
before calling `dart2wasm`.

Also update the test runner and ddc batch mode argument parser to handle
splitting quoted arguments in `// dart2jsOption = ...` and the same
options for ddc and dart2wasm, by moving dart2js's `splitLine` to a new
library and reusing it in the test runner and ddc.

Fixes https://github.com/flutter/flutter/issues/164873.

See also https://github.com/dart-lang/sdk/issues/60341 for relevant
future work.

Change-Id: Idbdf69072fa212c8e4a390990577eb5a57b49e8a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/415280
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
diff --git a/pkg/compiler/lib/src/dart2js.dart b/pkg/compiler/lib/src/dart2js.dart
index 43364f2..1bfdd5c 100644
--- a/pkg/compiler/lib/src/dart2js.dart
+++ b/pkg/compiler/lib/src/dart2js.dart
@@ -11,6 +11,7 @@
 
 // ignore: implementation_imports
 import 'package:front_end/src/api_unstable/dart2js.dart' as fe;
+import 'package:shell_arg_splitter/shell_arg_splitter.dart';
 
 import '../compiler_api.dart' as api;
 import 'commandline_options.dart';
@@ -20,7 +21,6 @@
 import 'options.dart'
     show CompilerOptions, CompilerStage, DumpInfoFormat, FeatureOptions;
 import 'source_file_provider.dart';
-import 'util/command_line.dart';
 import 'util/util.dart' show stackTraceFilePrefix;
 
 const String _defaultSpecificationUri = '../../../../sdk/lib/libraries.json';
diff --git a/pkg/compiler/pubspec.yaml b/pkg/compiler/pubspec.yaml
index e170571..e31c7c0 100644
--- a/pkg/compiler/pubspec.yaml
+++ b/pkg/compiler/pubspec.yaml
@@ -21,6 +21,7 @@
   kernel: any
   meta: any
   mmap: any
+  shell_arg_splitter: any
   vm_service: any
 
 # Use 'any' constraints here; we get our versions from the DEPS file.
diff --git a/pkg/dart2wasm/lib/dart2wasm.dart b/pkg/dart2wasm/lib/dart2wasm.dart
index 9cb8c5a..9138d76 100644
--- a/pkg/dart2wasm/lib/dart2wasm.dart
+++ b/pkg/dart2wasm/lib/dart2wasm.dart
@@ -68,7 +68,7 @@
       "watch", (o, values) => o.translatorOptions.watchPoints = values),
   StringMultiOption(
       "define", (o, values) => o.environment.addAll(processEnvironment(values)),
-      abbr: "D"),
+      abbr: "D", splitCommas: false),
   StringMultiOption(
       "enable-experiment",
       (o, values) =>
diff --git a/pkg/dart2wasm/lib/option.dart b/pkg/dart2wasm/lib/option.dart
index b0e8da4..89ddb5b 100644
--- a/pkg/dart2wasm/lib/option.dart
+++ b/pkg/dart2wasm/lib/option.dart
@@ -78,10 +78,12 @@
       void Function(WasmCompilerOptions o, List<T> v) applyToOptions,
       T Function(dynamic v) converter,
       {Iterable<String>? defaultsTo,
-      String? abbr})
+      String? abbr,
+      bool splitCommas = true})
       : super(
             name,
-            (a) => a.addMultiOption(name, abbr: abbr, defaultsTo: defaultsTo),
+            (a) => a.addMultiOption(name,
+                abbr: abbr, defaultsTo: defaultsTo, splitCommas: splitCommas),
             applyToOptions,
             (vs) => vs.map(converter).cast<T>().toList());
 }
@@ -97,9 +99,9 @@
 class StringMultiOption extends MultiValueOption<String> {
   StringMultiOption(String name,
       void Function(WasmCompilerOptions o, List<String> v) applyToOptions,
-      {String? abbr, Iterable<String>? defaultsTo})
+      {String? abbr, Iterable<String>? defaultsTo, bool splitCommas = true})
       : super(name, applyToOptions, (v) => v,
-            abbr: abbr, defaultsTo: defaultsTo);
+            abbr: abbr, defaultsTo: defaultsTo, splitCommas: splitCommas);
 }
 
 class UriMultiOption extends MultiValueOption<Uri> {
diff --git a/pkg/dart2wasm/tool/compile_benchmark b/pkg/dart2wasm/tool/compile_benchmark
index b2aaa93..e9dab8c 100755
--- a/pkg/dart2wasm/tool/compile_benchmark
+++ b/pkg/dart2wasm/tool/compile_benchmark
@@ -133,7 +133,7 @@
       ;;
 
     --extra-compiler-option=*)
-      DART2WASM_ARGS+=(${1#--extra-compiler-option=})
+      DART2WASM_ARGS+=("${1#--extra-compiler-option=}")
       shift
       ;;
 
@@ -219,7 +219,7 @@
 binaryen_command=("$BINARYEN" "${BINARYEN_FLAGS[@]}" "$WASM_FILE" -o "$WASM_FILE")
 
 if [ -n "$COMPILE_BENCHMARK_BASE_NAME" ]; then
-  measure ${dart2wasm_command[@]}
+  measure "${dart2wasm_command[@]}"
   COMPILER_TIME=$TIME
   COMPILER_MEMORY=$MEMORY
 
@@ -262,6 +262,6 @@
   echo "$COMPILE_BENCHMARK_BASE_NAME.MemoryUse.Dart2Wasm(MemoryUse): $COMPILER_MEMORY bytes"
   run_if_binaryen_enabled echo "$COMPILE_BENCHMARK_BASE_NAME.MemoryUse.Wasm2WasmOpt(MemoryUse): $BINARYEN_MEMORY bytes"
 else
-  ${dart2wasm_command[@]}
+  "${dart2wasm_command[@]}"
   run_if_binaryen_enabled ${binaryen_command[@]}
 fi
diff --git a/pkg/dartdev/lib/src/commands/compile.dart b/pkg/dartdev/lib/src/commands/compile.dart
index ce98b11..2829f7e 100644
--- a/pkg/dartdev/lib/src/commands/compile.dart
+++ b/pkg/dartdev/lib/src/commands/compile.dart
@@ -799,6 +799,7 @@
         help: defineOption.help,
         abbr: defineOption.abbr,
         valueHelp: defineOption.valueHelp,
+        splitCommas: false,
       )
       ..addExperimentalFlags(verbose: verbose);
   }
diff --git a/pkg/dev_compiler/lib/ddc.dart b/pkg/dev_compiler/lib/ddc.dart
index 3526364..55c739a 100755
--- a/pkg/dev_compiler/lib/ddc.dart
+++ b/pkg/dev_compiler/lib/ddc.dart
@@ -14,6 +14,7 @@
 import 'dart:isolate';
 import 'package:bazel_worker/bazel_worker.dart';
 import 'package:kernel/ast.dart' show clearDummyTreeNodesParentPointer;
+import 'package:shell_arg_splitter/shell_arg_splitter.dart';
 
 import 'src/command/arguments.dart';
 import 'src/command/command.dart';
@@ -129,7 +130,7 @@
 
   Future<void> _doIteration(ParsedArguments batchArgs, String line) async {
     totalTests++;
-    var args = batchArgs.merge(line.split(RegExp(r'\s+')));
+    var args = batchArgs.merge(splitLine(line));
 
     String outcome;
     try {
diff --git a/pkg/dev_compiler/pubspec.yaml b/pkg/dev_compiler/pubspec.yaml
index 89a6240..fe69c37 100644
--- a/pkg/dev_compiler/pubspec.yaml
+++ b/pkg/dev_compiler/pubspec.yaml
@@ -18,6 +18,7 @@
   js_shared: any
   kernel: any
   path: any
+  shell_arg_splitter: any
   source_maps: any
   source_span: any
 
diff --git a/pkg/shell_arg_splitter/analysis_options.yaml b/pkg/shell_arg_splitter/analysis_options.yaml
new file mode 100644
index 0000000..572dd23
--- /dev/null
+++ b/pkg/shell_arg_splitter/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
diff --git a/pkg/compiler/lib/src/util/command_line.dart b/pkg/shell_arg_splitter/lib/shell_arg_splitter.dart
similarity index 94%
rename from pkg/compiler/lib/src/util/command_line.dart
rename to pkg/shell_arg_splitter/lib/shell_arg_splitter.dart
index 4bb7923..4248b29 100644
--- a/pkg/compiler/lib/src/util/command_line.dart
+++ b/pkg/shell_arg_splitter/lib/shell_arg_splitter.dart
@@ -2,12 +2,10 @@
 // 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.
 
-library;
-
 /// The accepted escapes in the input of the --batch processor.
 ///
 /// Contrary to Dart strings it does not contain hex escapes (\u or \x).
-const Map<String, String> escapeMapping = {
+const Map<String, String> _escapeMapping = {
   'n': '\n',
   'r': '\r',
   't': '\t',
@@ -57,7 +55,7 @@
         i++;
 
         c = line[i];
-        String mapped = escapeMapping[c] ?? c;
+        String mapped = _escapeMapping[c] ?? c;
         buffer.write(mapped);
         continue;
       }
diff --git a/pkg/shell_arg_splitter/pubspec.yaml b/pkg/shell_arg_splitter/pubspec.yaml
new file mode 100644
index 0000000..d21c97b
--- /dev/null
+++ b/pkg/shell_arg_splitter/pubspec.yaml
@@ -0,0 +1,10 @@
+name: shell_arg_splitter
+# This package is not intended for consumption on pub.dev. DO NOT publish.
+publish_to: none
+
+environment:
+  sdk: '^3.7.0'
+
+dev_dependencies:
+  expect: any
+  lints: any
diff --git a/pkg/compiler/test/end_to_end/command_line_split_test.dart b/pkg/shell_arg_splitter/test/command_line_split_test.dart
similarity index 87%
rename from pkg/compiler/test/end_to_end/command_line_split_test.dart
rename to pkg/shell_arg_splitter/test/command_line_split_test.dart
index 6b82827..feedb3d 100644
--- a/pkg/compiler/test/end_to_end/command_line_split_test.dart
+++ b/pkg/shell_arg_splitter/test/command_line_split_test.dart
@@ -3,9 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import "package:expect/expect.dart";
-import 'package:compiler/src/util/command_line.dart';
+import "package:shell_arg_splitter/shell_arg_splitter.dart";
 
-main() {
+void main() {
   Expect.listEquals(["foo", "bar"], splitLine("foo bar"));
   Expect.listEquals(["foo", "bar"], splitLine("foo bar", windows: true));
 
@@ -28,13 +28,23 @@
     "''bar",
   ], splitLine(r"""foo"'" '"'bar" """, windows: true));
 
-  Expect.listEquals(["foo", "bar"], splitLine("'f''o''o' " + '"b""a""r"'));
+  Expect.listEquals(
+    ["foo", "bar"],
+    splitLine(
+      "'f''o''o' "
+      '"b""a""r"',
+    ),
+  );
   // TODO(johnniwinther): This is not actual Windows behavior: "b""a" is
   // interpreted as b"a but "b""a""r" is interpreted as b"ar.
-  Expect.listEquals([
-    "'f''o''o'",
-    "bar",
-  ], splitLine("'f''o''o' " + '"b""a""r"', windows: true));
+  Expect.listEquals(
+    ["'f''o''o'", "bar"],
+    splitLine(
+      "'f''o''o' "
+      '"b""a""r"',
+      windows: true,
+    ),
+  );
 
   Expect.listEquals([
     "\n",
diff --git a/pkg/test_runner/lib/src/process_queue.dart b/pkg/test_runner/lib/src/process_queue.dart
index 2ab2eb8..0851d59 100644
--- a/pkg/test_runner/lib/src/process_queue.dart
+++ b/pkg/test_runner/lib/src/process_queue.dart
@@ -1058,6 +1058,7 @@
   }
 
   String _createArgumentsLine(List<String> arguments, int timeout) {
+    arguments = arguments.map(escapeCommandLineArgument).toList();
     if (_useJson) {
       return "${jsonEncode(arguments)}\n";
     } else {
diff --git a/pkg/test_runner/lib/src/test_file.dart b/pkg/test_runner/lib/src/test_file.dart
index adcc01f..163a135 100644
--- a/pkg/test_runner/lib/src/test_file.dart
+++ b/pkg/test_runner/lib/src/test_file.dart
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 import 'dart:io';
 
+import 'package:shell_arg_splitter/shell_arg_splitter.dart';
+
 import 'feature.dart';
 import 'path.dart';
 import 'static_error.dart';
@@ -22,9 +24,6 @@
     r"""(?:(?:show|hide)\s+\w+\s*(?:,\s*\w+\s*))*;""",
     multiLine: true);
 
-List<String> _splitWords(String s) =>
-    s.split(' ')..removeWhere((s) => s.isEmpty);
-
 List<T> _parseOption<T>(
     String filePath, String contents, String name, T Function(String) convert,
     {bool allowMultiple = false}) {
@@ -37,7 +36,7 @@
 
   var options = <T>[];
   for (var match in matches) {
-    for (var option in _splitWords(match[1]!)) {
+    for (var option in splitLine(match[1]!)) {
       options.add(convert(option));
     }
   }
@@ -228,7 +227,7 @@
     var vmOptions = <List<String>>[];
     var matches = _vmOptionsRegExp.allMatches(contents);
     for (var match in matches) {
-      vmOptions.add(_splitWords(match[1]!));
+      vmOptions.add(splitLine(match[1]!));
     }
     if (vmOptions.isEmpty) vmOptions.add(<String>[]);
 
diff --git a/pkg/test_runner/pubspec.yaml b/pkg/test_runner/pubspec.yaml
index 15c4089..57f20b9 100644
--- a/pkg/test_runner/pubspec.yaml
+++ b/pkg/test_runner/pubspec.yaml
@@ -17,6 +17,7 @@
   package_config: any
   path: any
   pool: any
+  shell_arg_splitter: any
   smith: any
   status_file: any
   webdriver: any
diff --git a/tests/web/wasm_js_shared/env_defines_test.dart b/tests/web/wasm_js_shared/env_defines_test.dart
new file mode 100644
index 0000000..c78dec2
--- /dev/null
+++ b/tests/web/wasm_js_shared/env_defines_test.dart
@@ -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.
+
+// dart2jsOptions="-DFOO=a, b, c=123" --define=BAR=hi
+// ddcOptions="-DFOO=a, b, c=123" --define=BAR=hi
+// dart2wasmOptions="--extra-compiler-option=-DFOO=a, b, c=123" --define=BAR=hi
+
+import 'package:expect/expect.dart';
+
+void main() {
+  Expect.equals("a, b, c=123", const String.fromEnvironment("FOO"));
+  Expect.equals("hi", const String.fromEnvironment("BAR"));
+}