Tweak test_runner.

Clean up and optimize some RegExps, and fix uses of `.group`.
Switch to a newer language version, to be able to use newer features.
Add a little documentation about why some RegExps are as they are.

Add (tentative) warning for multitests.

Change-Id: I59f73b87ce30caaeca1c0e0aa7954af1b97abd1b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/382620
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Lasse Nielsen <lrn@google.com>
diff --git a/pkg/test_runner/lib/src/android.dart b/pkg/test_runner/lib/src/android.dart
index 417d643..3bc3bb4 100644
--- a/pkg/test_runner/lib/src/android.dart
+++ b/pkg/test_runner/lib/src/android.dart
@@ -341,7 +341,7 @@
 /// Helper to list all adb devices available.
 class AdbHelper {
   static final RegExp _deviceLineRegexp =
-      RegExp(r'^(([a-zA-Z0-9:_-]|\.)+)[ \t]+device$', multiLine: true);
+      RegExp(r'^([a-zA-Z0-9:_.\-]+)[ \t]+device$', multiLine: true);
 
   static Future<List<String>> listDevices() {
     return Process.run('adb', ['devices']).then((ProcessResult result) {
@@ -351,7 +351,7 @@
       }
       return _deviceLineRegexp
           .allMatches(result.stdout as String)
-          .map((Match m) => m.group(1)!)
+          .map((Match m) => m[1]!)
           .toList();
     });
   }
diff --git a/pkg/test_runner/lib/src/browser.dart b/pkg/test_runner/lib/src/browser.dart
index 32316af..f0964ca 100644
--- a/pkg/test_runner/lib/src/browser.dart
+++ b/pkg/test_runner/lib/src/browser.dart
@@ -60,24 +60,26 @@
       .replaceAll('-', '_'));
 }
 
+final _digitPattern = RegExp(r'\d');
+
 /// Escape [name] to make it into a valid identifier.
 String _toJSIdentifier(String name) {
   if (name.isEmpty) return r'$';
 
   // Escape any invalid characters
-  var result = name.replaceAllMapped(_invalidCharInIdentifier,
-      (match) => '\$${match.group(0)!.codeUnits.join("")}');
+  var result = name.replaceAllMapped(
+      _invalidCharInIdentifier, (match) => '\$${match[0]!.codeUnits.join("")}');
 
   // Ensure the identifier first character is not numeric and that the whole
   // identifier is not a keyword.
-  if (result.startsWith(RegExp('[0-9]')) || _invalidVariableName(result)) {
+  if (result.startsWith(_digitPattern) || _invalidVariableName(result)) {
     return '\$$result';
   }
   return result;
 }
 
 // Invalid characters for identifiers, which would need to be escaped.
-final _invalidCharInIdentifier = RegExp(r'[^A-Za-z_0-9]');
+final _invalidCharInIdentifier = RegExp(r'[^A-Za-z_\d]');
 
 bool _invalidVariableName(String keyword, {bool strictMode = true}) {
   switch (keyword) {
diff --git a/pkg/test_runner/lib/src/co19_test_config.dart b/pkg/test_runner/lib/src/co19_test_config.dart
index 68042956..66cc6d5 100644
--- a/pkg/test_runner/lib/src/co19_test_config.dart
+++ b/pkg/test_runner/lib/src/co19_test_config.dart
@@ -7,7 +7,7 @@
 import 'test_suite.dart';
 
 class Co19TestSuite extends StandardTestSuite {
-  static final _testRegExp = RegExp(r"t[0-9]{2,3}.dart$");
+  static final _testRegExp = RegExp(r"t\d{2,3}.dart$");
 
   Co19TestSuite(TestConfiguration configuration, String selector)
       : super(configuration, selector, Path("tests/$selector/src"), [
diff --git a/pkg/test_runner/lib/src/command_output.dart b/pkg/test_runner/lib/src/command_output.dart
index 7a100aa..adc4a4a 100644
--- a/pkg/test_runner/lib/src/command_output.dart
+++ b/pkg/test_runner/lib/src/command_output.dart
@@ -339,7 +339,7 @@
   @override
   void describe(TestCase testCase, Progress progress, OutputWriter output) {
     if (_jsonResult != null) {
-      _describeEvents(progress, output);
+      _describeEvents(progress, output, _jsonResult);
     } else {
       // We couldn't parse the events, so fallback to showing the last message.
       output.section("Last message");
@@ -359,7 +359,8 @@
     }
   }
 
-  void _describeEvents(Progress progress, OutputWriter output) {
+  void _describeEvents(Progress progress, OutputWriter output,
+      BrowserTestJsonResult jsonResult) {
     // Always show the error events since those are most useful.
     var errorShown = false;
 
@@ -374,19 +375,19 @@
 
       // Skip deobfuscation if there is no indication that there is a stack
       // trace in the string value.
-      if (!value!.contains(RegExp('\\.js:'))) return;
+      if (!value!.contains('.js:')) return;
       var stringStack = value
           // Convert `http:` URIs to relative `file:` URIs.
-          .replaceAll(RegExp('http://[^/]*/root_build/'), '$_buildDirectory/')
-          .replaceAll(RegExp('http://[^/]*/root_dart/'), '')
+          .replaceAll(RegExp(r'http://[^/]*/root_build/'), '$_buildDirectory/')
+          .replaceAll(RegExp(r'http://[^/]*/root_dart/'), '')
           // Remove query parameters (seen in .html URIs).
-          .replaceAll(RegExp('\\?[^:\n]*:'), ':');
+          .replaceAll(RegExp(r'\?[^:\n]*:'), ':');
       // TODO(sigmund): change internal deobfuscation code to avoid spurious
       // error messages when files do not have a corresponding source-map.
       _deobfuscateAndWriteStack(stringStack, output);
     }
 
-    for (var event in _jsonResult!.events) {
+    for (var event in jsonResult.events) {
       if (event["type"] == "sync_exception") {
         showError("Runtime error", event);
       } else if (event["type"] == "window_onerror") {
@@ -401,7 +402,7 @@
     }
 
     output.subsection("Events");
-    for (var event in _jsonResult!.events) {
+    for (var event in jsonResult.events) {
       switch (event["type"] as String?) {
         case "debug":
           output.write('- debug "${event["value"]}"');
diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart
index 0278dcd..9ebb610 100644
--- a/pkg/test_runner/lib/src/compiler_configuration.dart
+++ b/pkg/test_runner/lib/src/compiler_configuration.dart
@@ -801,7 +801,7 @@
           .replaceAll('/', '__')
           .replaceAll('-', '_')
           .replaceAll('.dart', '')
-          .replaceAllMapped(RegExp(r'[^A-Za-z_$0-9]'),
+          .replaceAllMapped(RegExp(r'[^A-Za-z_$\d]'),
               (Match m) => '\$${m[0]!.codeUnits.join('')}');
       var testPackageLoadStatements = [
         for (var package in testPackages) 'load("$pkgJsDir/$package.js");'
@@ -1421,7 +1421,7 @@
   }
 }
 
-abstract class VMKernelCompilerMixin {
+abstract mixin class VMKernelCompilerMixin {
   TestConfiguration get _configuration;
 
   bool get _useSdk;
diff --git a/pkg/test_runner/lib/src/multitest.dart b/pkg/test_runner/lib/src/multitest.dart
index cae39d3..f81bb49 100644
--- a/pkg/test_runner/lib/src/multitest.dart
+++ b/pkg/test_runner/lib/src/multitest.dart
@@ -92,7 +92,7 @@
   var lineSeparator =
       (firstNewline == 0 || contents[firstNewline - 1] != '\r') ? '\n' : '\r\n';
   var lines = contents.split(lineSeparator);
-  if (lines.last == '') lines.removeLast();
+  if (lines.last.isEmpty) lines.removeLast();
 
   // Create the set of multitests, which will have a new test added each
   // time we see a multitest line with a new key.
@@ -108,16 +108,16 @@
     var annotation = Annotation.tryParse(line);
     if (annotation != null) {
       testsAsLines.putIfAbsent(
-          annotation.key, () => List<String>.from(testsAsLines["none"]!));
+          annotation.key, () => List<String>.of(testsAsLines["none"]!));
       // Add line to test with annotation.key as key, empty line to the rest.
       for (var entry in testsAsLines.entries) {
         entry.value.add(annotation.key == entry.key ? line : "");
       }
-      outcomes.putIfAbsent(annotation.key, () => <String>{});
+      var outcome = outcomes.putIfAbsent(annotation.key, () => <String>{});
       if (annotation.rest != 'continued') {
         for (var nextOutcome in annotation.outcomes) {
           if (_multitestOutcomes.contains(nextOutcome)) {
-            outcomes[annotation.key]!.add(nextOutcome);
+            outcome.add(nextOutcome);
           } else {
             DebugLogger.warning(
                 "${filePath.toNativePath()}: Invalid expectation "
@@ -277,20 +277,20 @@
 Set<String> _findAllRelativeImports(Path topLibrary) {
   var found = <String>{};
   var libraryDir = topLibrary.directoryPath;
-  var relativeImportRegExp = RegExp(
-      '^(?:@.*\\s+)?' // Allow for a meta-data annotation.
-      '(import|part)'
-      '\\s+["\']'
-      '(?!(dart:|dart-ext:|data:|package:|/))' // Look-ahead: not in package.
-      '([^"\']*)' // The path to the imported file.
-      '["\']');
+  var relativeImportRegExp =
+      RegExp(r'^(?:@.*\s+)?' // Allow for a meta-data annotation.
+          r'(?:import|part)\s+'
+          r'''["']'''
+          r'(?!dart:|dart-ext:|data:|package:|/)' // Look-ahead: not in package.
+          r'([^]*?)' // The path to the imported file.
+          r'''["']''');
 
   processFile(Path filePath) {
     var file = File(filePath.toNativePath());
     for (var line in file.readAsLinesSync()) {
       var match = relativeImportRegExp.firstMatch(line);
       if (match == null) continue;
-      var relativePath = match.group(3)!;
+      var relativePath = match[1]!;
 
       // If a multitest deliberately imports a nonexistent file, don't try to
       // include it.
diff --git a/pkg/test_runner/lib/src/options.dart b/pkg/test_runner/lib/src/options.dart
index 35f1034..1733112 100644
--- a/pkg/test_runner/lib/src/options.dart
+++ b/pkg/test_runner/lib/src/options.dart
@@ -509,7 +509,7 @@
           // Remove the `src/` subdirectories from the co19 directories that do
           // not appear in the test names.
           if (selector.startsWith('co19')) {
-            selector = selector.replaceFirst(RegExp('src/'), '');
+            selector = selector.replaceFirst('src/', '');
           }
           break;
         }
@@ -858,7 +858,7 @@
       var pattern = selectors[i];
       var suite = pattern;
       var slashLocation = pattern.indexOf('/');
-      if (slashLocation != -1) {
+      if (slashLocation >= 0) {
         suite = pattern.substring(0, slashLocation);
         pattern = pattern.substring(slashLocation + 1);
         pattern = pattern.replaceAll('*', '.*');
diff --git a/pkg/test_runner/lib/src/static_error.dart b/pkg/test_runner/lib/src/static_error.dart
index ef8b5ea..dd30e53e 100644
--- a/pkg/test_runner/lib/src/static_error.dart
+++ b/pkg/test_runner/lib/src/static_error.dart
@@ -13,7 +13,7 @@
 /// A front end that can report static errors.
 class ErrorSource {
   static const analyzer = ErrorSource._("analyzer");
-  static const cfe = ErrorSource._("CFE");
+  static const cfe = ErrorSource._("CFE", marker: "cfe");
   static const web = ErrorSource._("web");
 
   /// Pseudo-front end for context messages.
@@ -41,9 +41,16 @@
   final String name;
 
   /// The string used to mark errors from this source in test files.
-  String get marker => name.toLowerCase();
+  ///
+  /// Always lower-case.
+  final String marker;
 
-  const ErrorSource._(this.name);
+  /// Creates error source.
+  ///
+  /// If [name] is not all lower-case, then a [marker] must be passed with
+  /// an all lower-case name. If `name` is lower-case, `marker` can be omitted
+  /// and then defaults to `name`.
+  const ErrorSource._(this.name, {String? marker}) : marker = marker ?? name;
 }
 
 /// Describes a single static error reported by a single front end at a specific
@@ -465,7 +472,7 @@
   ///     //      ^^^
   ///
   /// We look for a line that starts with a line comment followed by spaces and
-  /// carets.
+  /// carets. Only used on single lines.
   static final _caretLocationRegExp = RegExp(r"^\s*//\s*(\^+)\s*$");
 
   /// Matches an explicit error location with a length, like:
@@ -475,29 +482,26 @@
   /// or implicitly on the previous line
   ///
   ///     // [error column 17, length 3]
-  static final _explicitLocationAndLengthRegExp = RegExp(
-      r"^\s*//\s*\[\s*error (?:line\s+(\d+)\s*,)?\s*column\s+(\d+)\s*,\s*"
-      r"length\s+(\d+)\s*\]\s*$");
-
-  /// Matches an explicit error location without a length, like:
+  ///
+  /// or either without a length:
   ///
   ///     // [error line 1, column 17]
-  ///
-  /// or implicitly on the previous line.
-  ///
   ///     // [error column 17]
+  ///
+  ///  Only used on single lines.
   static final _explicitLocationRegExp = RegExp(
-      r"^\s*//\s*\[\s*error (?:line\s+(\d+)\s*,)?\s*column\s+(\d+)\s*\]\s*$");
+      r"^\s*//\s*\[\s*error (?:line\s+(\d+)\s*,)?\s*column\s+(\d+)\s*(?:,\s*"
+      r"length\s+(\d+)\s*)?\]\s*$");
 
   /// Matches the beginning of an error message, like `// [analyzer]`.
   ///
   /// May have an optional number like `// [cfe 32]`.
   static final _errorMessageRegExp =
-      RegExp(r"^\s*// \[(\w+)(\s+\d+)?\]\s*(.*)");
+      RegExp(r"^\s*// \[(\w+)(?:\s+(\d+))?\]\s*(.*)");
 
   /// An analyzer error code is a dotted identifier or the magic string
   /// "unspecified".
-  static final _errorCodeRegExp = RegExp(r"^\w+\.\w+|unspecified$");
+  static final _errorCodeRegExp = RegExp(r"^(?:\w+\.\w+|unspecified)$");
 
   /// Any line-comment-only lines after the first line of a CFE error message
   /// are part of it.
@@ -535,40 +539,29 @@
     while (_canPeek(0)) {
       var sourceLine = _peek(0);
 
-      var match = _caretLocationRegExp.firstMatch(sourceLine);
-      if (match != null) {
+      if (_caretLocationRegExp.firstMatch(sourceLine) case var match?) {
         if (_lastRealLine == -1) {
           _fail("An error expectation must follow some code.");
         }
-
+        var markerMatch = match[1]!;
         _parseErrors(
             path: path,
             line: _lastRealLine,
             column: sourceLine.indexOf("^") + 1,
-            length: match[1]!.length);
+            length: markerMatch.length);
         _advance();
         continue;
       }
 
-      match = _explicitLocationAndLengthRegExp.firstMatch(sourceLine);
-      if (match != null) {
+      if (_explicitLocationRegExp.firstMatch(sourceLine) case var match?) {
         var lineCapture = match[1];
+        var columnCapture = match[2]!;
+        var lengthCapture = match[3];
         _parseErrors(
             path: path,
             line: lineCapture == null ? _lastRealLine : int.parse(lineCapture),
-            column: int.parse(match[2]!),
-            length: int.parse(match[3]!));
-        _advance();
-        continue;
-      }
-
-      match = _explicitLocationRegExp.firstMatch(sourceLine);
-      if (match != null) {
-        var lineCapture = match[1];
-        _parseErrors(
-            path: path,
-            line: lineCapture == null ? _lastRealLine : int.parse(lineCapture),
-            column: int.parse(match[2]!));
+            column: int.parse(columnCapture),
+            length: lengthCapture == null ? 0 : int.parse(lengthCapture));
         _advance();
         continue;
       }
@@ -604,7 +597,7 @@
         _fail("Context messages must have an error number.");
       }
 
-      var message = match[3]!;
+      var message = StringBuffer(match[3]!);
       _advance();
       var sourceLines = {locationLine, _currentLine};
 
@@ -614,7 +607,6 @@
 
         // A location line shouldn't be treated as part of the message.
         if (_caretLocationRegExp.hasMatch(nextLine)) break;
-        if (_explicitLocationAndLengthRegExp.hasMatch(nextLine)) break;
         if (_explicitLocationRegExp.hasMatch(nextLine)) break;
 
         // The next source should not be treated as part of the message.
@@ -623,13 +615,15 @@
         var messageMatch = _errorMessageRestRegExp.firstMatch(nextLine);
         if (messageMatch == null) break;
 
-        message += "\n${messageMatch[1]!}";
+        message
+          ..write("\n")
+          ..write(messageMatch[1]!);
         _advance();
         sourceLines.add(_currentLine);
       }
 
       if (source == ErrorSource.analyzer &&
-          !_errorCodeRegExp.hasMatch(message)) {
+          !_errorCodeRegExp.hasMatch(message.toString())) {
         _fail("An analyzer error expectation should be a dotted identifier.");
       }
 
@@ -644,7 +638,7 @@
         errorLength = 0;
       }
 
-      var error = StaticError(source, message,
+      var error = StaticError(source, message.toString(),
           path: path,
           line: line,
           column: column,
@@ -654,9 +648,9 @@
       if (number != null) {
         // Make sure two errors don't claim the same number.
         if (source != ErrorSource.context) {
-          var existingError = _errors
-              .firstWhereOrNull((error) => _errorNumbers[error] == number);
-          if (existingError != null) {
+          var existingError =
+              _errors.any((error) => _errorNumbers[error] == number);
+          if (existingError) {
             _fail("Already have an error with number $number.");
           }
         }
@@ -700,9 +694,9 @@
       var number = _errorNumbers[error];
       if (number == null) continue;
 
-      var context = _contextMessages
-          .firstWhereOrNull((context) => _errorNumbers[context] == number);
-      if (context == null) {
+      var hasContext =
+          _contextMessages.any((context) => _errorNumbers[context] == number);
+      if (!hasContext) {
         throw FormatException("Missing context for numbered error $number "
             "'${error.message}'.");
       }
diff --git a/pkg/test_runner/lib/src/test_file.dart b/pkg/test_runner/lib/src/test_file.dart
index e5706c7..f9a90e1 100644
--- a/pkg/test_runner/lib/src/test_file.dart
+++ b/pkg/test_runner/lib/src/test_file.dart
@@ -6,28 +6,33 @@
 import 'feature.dart';
 import 'path.dart';
 import 'static_error.dart';
+import 'utils.dart';
 
-final _multitestRegExp = RegExp(r"//# \w+:(.*)");
+final _multitestRegExp = RegExp(r"//# \w+:");
 
-final _vmOptionsRegExp = RegExp(r"// VMOptions=(.*)");
-final _environmentRegExp = RegExp(r"// Environment=(.*)");
-final _packagesRegExp = RegExp(r"// Packages=(.*)");
+final _vmOptionsRegExp = RegExp(r"^[ \t]*// VMOptions=(.*)", multiLine: true);
+final _environmentRegExp =
+    RegExp(r"^[ \t]*// Environment=(.*)", multiLine: true);
+final _packagesRegExp = RegExp(r"^[ \t]*// Packages=(.*)", multiLine: true);
 final _experimentRegExp = RegExp(r"^--enable-experiment=([a-z0-9,-]+)$");
 final _localFileRegExp = RegExp(
-    r"""^\s*(?:import(?: augment)?|part) """
-    r"""['"](?!package:|dart:)(.*)['"]"""
-    r"""(?: deferred as \w+)?;""",
+    r"""^[ \t]*(?:import(?: augment)?|part)\s*"""
+    r"""['"](?!package:|dart:)(.*?)['"]\s*"""
+    r"""(?:(?:deferred\s+)?as\s+\w+\s*)?"""
+    r"""(?:(?:show|hide)\s+\w+\s*(?:,\s*\w+\s*))*;""",
     multiLine: true);
 
 List<String> _splitWords(String s) =>
-    s.split(' ').where((e) => e != '').toList();
+    s.split(' ')..removeWhere((s) => s.isEmpty);
 
 List<T> _parseOption<T>(
     String filePath, String contents, String name, T Function(String) convert,
     {bool allowMultiple = false}) {
-  var matches = RegExp('// $name=(.*)').allMatches(contents);
+  var matches = RegExp('^[ \t]*// $name=(.*)', multiLine: true)
+      .allMatches(contents)
+      .toList();
   if (!allowMultiple && matches.length > 1) {
-    throw Exception('More than one "// $name=" line in test $filePath');
+    throw FormatException('More than one "// $name=" line in test $filePath');
   }
 
   var options = <T>[];
@@ -99,7 +104,7 @@
       return path.toString().hashCode;
     }
 
-    return originPath.relativeTo(_suiteDirectory!).toString().hashCode;
+    return originPath.relativeTo(_suiteDirectory).toString().hashCode;
   }
 
   _TestFileBase(this._suiteDirectory, this.path, this.expectedErrors) {
@@ -117,15 +122,15 @@
     var directory = testNamePath.directoryPath;
     var filenameWithoutExt = testNamePath.filenameWithoutExtension;
 
-    String concat(String base, String part) {
-      if (base == "") return part;
-      if (part == "") return base;
+    String join(String base, String part) {
+      if (base.isEmpty) return part;
+      if (part.isEmpty) return base;
       return "$base/$part";
     }
 
     var result = "$directory";
-    result = concat(result, filenameWithoutExt);
-    result = concat(result, multitestKey);
+    result = join(result, filenameWithoutExt);
+    result = join(result, multitestKey);
     return result;
   }
 }
@@ -255,7 +260,7 @@
               "flags. Was:\n$sharedOption");
         }
 
-        experiments.addAll(match.group(1)!.split(","));
+        experiments.addAll(match[1]!.split(","));
         sharedOptions.removeAt(i);
         i--;
       }
@@ -266,9 +271,13 @@
     matches = _environmentRegExp.allMatches(contents);
     for (var match in matches) {
       var envDef = match[1]!;
+      var name = envDef;
+      var value = '';
       var pos = envDef.indexOf('=');
-      var name = (pos < 0) ? envDef : envDef.substring(0, pos);
-      var value = (pos < 0) ? '' : envDef.substring(pos + 1);
+      if (pos >= 0) {
+        name = envDef.substring(0, pos);
+        value = envDef.substring(pos + 1);
+      }
       environment[name] = value;
     }
 
@@ -278,18 +287,23 @@
     matches = _packagesRegExp.allMatches(contents);
     for (var match in matches) {
       if (packages != null) {
-        throw Exception('More than one "// Package..." line in test $filePath');
+        throw FormatException(
+            'More than one "// Package..." line in test $filePath');
       }
-      packages = match[1];
+      packages = match[1]!;
       if (packages != 'none') {
         // Packages=none means that no packages option should be given. Any
         // other value overrides packages.
         packages =
-            Uri.file(filePath).resolveUri(Uri.file(packages!)).toFilePath();
+            Uri.file(filePath).resolveUri(Uri.file(packages)).toFilePath();
       }
     }
 
     var isMultitest = _multitestRegExp.hasMatch(contents);
+    if (isMultitest) {
+      DebugLogger.warning(
+          "${Path(filePath).toNativePath()} is a legacy multi-test file.");
+    }
 
     var errorExpectations = <StaticError>[];
     try {
@@ -323,12 +337,12 @@
       {Set<String>? alreadyParsed}) {
     alreadyParsed ??= {};
     var file = File(path);
-
+    var pathUri = Uri.parse(path);
     // Missing files set no expectations.
     if (!file.existsSync()) return [];
 
     // Catch import loops.
-    if (!alreadyParsed.add(Uri.parse(path).toString())) return [];
+    if (!alreadyParsed.add(pathUri.toString())) return [];
 
     // Parse one file.
     var contents = File(path).readAsStringSync();
@@ -340,7 +354,7 @@
       var localPath = Uri.tryParse(match[1]!);
       // Broken import paths set no expectations.
       if (localPath == null) continue;
-      var uriString = Uri.parse(path).resolve(localPath.path).toString();
+      var uriString = pathUri.resolve(localPath.path).toString();
       result
           .addAll(_parseExpectations(uriString, alreadyParsed: alreadyParsed));
     }
diff --git a/pkg/test_runner/lib/src/update_errors.dart b/pkg/test_runner/lib/src/update_errors.dart
index bcff57e..f8068c4 100644
--- a/pkg/test_runner/lib/src/update_errors.dart
+++ b/pkg/test_runner/lib/src/update_errors.dart
@@ -3,10 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 import 'static_error.dart';
 
-/// Matches leading indentation in a string.
-final _indentationRegExp = RegExp(r"^(\s*)");
+/// Matches end of leading indentation in a line.
+///
+/// Only used on single lines.
+final _indentationRegExp = RegExp(r"(?=\S|$)");
 
 /// Matches a line that contains only a line comment.
+///
+/// Only used on single lines.
 final _lineCommentRegExp = RegExp(r"^\s*//");
 
 /// Removes existing static error marker comments in [source] and adds markers
@@ -190,5 +194,5 @@
 /// Returns the number of characters of leading spaces in [line].
 int _countIndentation(String line) {
   var match = _indentationRegExp.firstMatch(line)!;
-  return match.group(1)!.length;
+  return match.start;
 }
diff --git a/pkg/test_runner/lib/test_runner.dart b/pkg/test_runner/lib/test_runner.dart
index 2e68d78..028a7ab 100644
--- a/pkg/test_runner/lib/test_runner.dart
+++ b/pkg/test_runner/lib/test_runner.dart
@@ -34,7 +34,7 @@
 ///   bar becomes 'foo
 ///   bar'
 String shellSingleQuote(String string) {
-  return "'${string.replaceAll("'", "'\\''")}'";
+  return "'${string.replaceAll("'", r"'\''")}'";
 }
 
 /// Like [shellSingleQuote], but if the string only contains safe ASCII
@@ -43,7 +43,7 @@
 /// a shell keyword or a shell builtin in the first argument in a command. It
 /// should be safe to use this for the second argument onwards in a command.
 String simpleShellSingleQuote(String string) {
-  return RegExp(r"^[a-zA-Z0-9%+,./:_-]*$").hasMatch(string)
+  return RegExp(r"^[a-zA-Z\d%+,./:_\-]*$").hasMatch(string)
       ? string
       : shellSingleQuote(string);
 }
diff --git a/pkg/test_runner/pubspec.yaml b/pkg/test_runner/pubspec.yaml
index 31396d4..3ce5bf7 100644
--- a/pkg/test_runner/pubspec.yaml
+++ b/pkg/test_runner/pubspec.yaml
@@ -7,7 +7,7 @@
 publish_to: none
 
 environment:
-  sdk: '>=2.19.0 <3.0.0'
+  sdk: '>=3.4.0 <4.0.0'
 
 # Use 'any' constraints here; we get our versions from the DEPS file.
 dependencies:
diff --git a/runtime/tests/vm/dart/regress_46790_test.dart b/runtime/tests/vm/dart/regress_46790_test.dart
index ba2cfa5..8846059 100644
--- a/runtime/tests/vm/dart/regress_46790_test.dart
+++ b/runtime/tests/vm/dart/regress_46790_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// VMOptions=--stacktrace_every=137 --deterministic
+// VMOptions=--stacktrace_every=137 --deterministic
 
 // Reduced from
 // The Dart Project Fuzz Tester (1.91).
diff --git a/tests/language/deferred/prefix_importer_tree_shaken_test.dart b/tests/language/deferred/prefix_importer_tree_shaken_test.dart
index ccb0246..242302e 100644
--- a/tests/language/deferred/prefix_importer_tree_shaken_test.dart
+++ b/tests/language/deferred/prefix_importer_tree_shaken_test.dart
@@ -2,8 +2,8 @@
 // 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.
 
-/// VMOptions=--dwarf_stack_traces=true
-/// VMOptions=--dwarf_stack_traces=false
+// VMOptions=--dwarf_stack_traces=true
+// VMOptions=--dwarf_stack_traces=false
 
 import "prefix_importer_tree_shaken_immediate.dart" as i;
 
diff --git a/tests/language/vm/fuzzer_unsigned_shift_right_test.dart b/tests/language/vm/fuzzer_unsigned_shift_right_test.dart
index 01e935a..42660ed 100644
--- a/tests/language/vm/fuzzer_unsigned_shift_right_test.dart
+++ b/tests/language/vm/fuzzer_unsigned_shift_right_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// VMOptions=--deterministic
+// VMOptions=--deterministic
 
 // The Dart Project Fuzz Tester (1.93).
 // Program generated as:
diff --git a/tests/lib/developer/timeline_recorders_test.dart b/tests/lib/developer/timeline_recorders_test.dart
index 349cbba..2656f30 100644
--- a/tests/lib/developer/timeline_recorders_test.dart
+++ b/tests/lib/developer/timeline_recorders_test.dart
@@ -2,10 +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.
 
-/// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=endless
-/// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=ring
-/// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=startup
-/// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=systrace
+// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=endless
+// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=ring
+// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=startup
+// VMOptions=--timeline_streams=VM,Isolate,GC,Dart --timeline_recorder=systrace
 
 import 'dart:developer';
 
diff --git a/tests/lib/js/static_interop_test/external_dart_reference_test.dart b/tests/lib/js/static_interop_test/external_dart_reference_test.dart
index 33108e4..98bce34 100644
--- a/tests/lib/js/static_interop_test/external_dart_reference_test.dart
+++ b/tests/lib/js/static_interop_test/external_dart_reference_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// Requirements=checked-implicit-downcasts
+// Requirements=checked-implicit-downcasts
 
 import 'dart:js_interop';
 
diff --git a/tests/lib/js/static_interop_test/js_function_arity_test.dart b/tests/lib/js/static_interop_test/js_function_arity_test.dart
index a7ee811..5befc7e 100644
--- a/tests/lib/js/static_interop_test/js_function_arity_test.dart
+++ b/tests/lib/js/static_interop_test/js_function_arity_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// Requirements=checked-implicit-downcasts
+// Requirements=checked-implicit-downcasts
 
 import 'dart:js_interop';
 
diff --git a/tests/lib/js/static_interop_test/js_function_conversions_test.dart b/tests/lib/js/static_interop_test/js_function_conversions_test.dart
index 9719735..f800fbe 100644
--- a/tests/lib/js/static_interop_test/js_function_conversions_test.dart
+++ b/tests/lib/js/static_interop_test/js_function_conversions_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// Requirements=checked-implicit-downcasts
+// Requirements=checked-implicit-downcasts
 
 // Test that Function.toJS properly converts/casts arguments and return values
 // when using non-JS types.
diff --git a/tests/standalone/dwarf_stack_trace_invisible_functions_test.dart b/tests/standalone/dwarf_stack_trace_invisible_functions_test.dart
index cb155bb..da91c2e 100644
--- a/tests/standalone/dwarf_stack_trace_invisible_functions_test.dart
+++ b/tests/standalone/dwarf_stack_trace_invisible_functions_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// VMOptions=--dwarf-stack-traces --save-debugging-info=$TEST_COMPILATION_DIR/dwarf_invisible_functions.so
+// VMOptions=--dwarf-stack-traces --save-debugging-info=$TEST_COMPILATION_DIR/dwarf_invisible_functions.so
 
 import 'dart:io';
 
diff --git a/tests/standalone/dwarf_stack_trace_obfuscate_test.dart b/tests/standalone/dwarf_stack_trace_obfuscate_test.dart
index 01b5515..a6f09a7 100644
--- a/tests/standalone/dwarf_stack_trace_obfuscate_test.dart
+++ b/tests/standalone/dwarf_stack_trace_obfuscate_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// VMOptions=--dwarf-stack-traces --save-debugging-info=$TEST_COMPILATION_DIR/dwarf_obfuscate.so --obfuscate
+// VMOptions=--dwarf-stack-traces --save-debugging-info=$TEST_COMPILATION_DIR/dwarf_obfuscate.so --obfuscate
 
 import 'dart:io';
 
diff --git a/tests/standalone/dwarf_stack_trace_test.dart b/tests/standalone/dwarf_stack_trace_test.dart
index 53f35db..f6bfad8 100644
--- a/tests/standalone/dwarf_stack_trace_test.dart
+++ b/tests/standalone/dwarf_stack_trace_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// VMOptions=--dwarf-stack-traces --save-debugging-info=$TEST_COMPILATION_DIR/dwarf.so
+// VMOptions=--dwarf-stack-traces --save-debugging-info=$TEST_COMPILATION_DIR/dwarf.so
 
 import 'dart:convert';
 import 'dart:io';
diff --git a/tests/standalone/regress31114_test.dart b/tests/standalone/regress31114_test.dart
index 2977a21..8f822ef 100644
--- a/tests/standalone/regress31114_test.dart
+++ b/tests/standalone/regress31114_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-/// VMOptions=--background-compilation=false --optimization-counter-threshold=20
+// VMOptions=--background-compilation=false --optimization-counter-threshold=20
 
 import 'pow_test.dart' as test;
 
diff --git a/tests/web/consistent_subtract_error_test.dart b/tests/web/consistent_subtract_error_test.dart
index a34f50f..2a131a2 100644
--- a/tests/web/consistent_subtract_error_test.dart
+++ b/tests/web/consistent_subtract_error_test.dart
@@ -2,7 +2,7 @@
 // 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=--omit-implicit-checks
+// dart2jsOptions=--omit-implicit-checks
 
 import "package:expect/expect.dart";