Add test_runner support for // [spec_parser] comments

This CL adds support to the test runner for the test outcome
expectation syntax that uses `// [...]` to indicate which diagnostic
we should expect with the given tool. Currently, `...` is `analyzer`
when testing the analyzer and `cfe` when testing the common front end.
After landing this CL, it also supports `// [spec_parser]`.

This change would allow us to migrate several tests from the multi-test
format (like `code; //# 01: compile-time error`) to the new format, in
particular tests with intentional syntax errors.

Change-Id: I5dd2560a873eb7433d3e07aa8f393e83c51b28e7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/501161
Reviewed-by: Bob Nystrom <rnystrom@google.com>
Commit-Queue: Erik Ernst <eernst@google.com>
diff --git a/pkg/test_runner/lib/src/command_output.dart b/pkg/test_runner/lib/src/command_output.dart
index 97f7ae5..7f16e86 100644
--- a/pkg/test_runner/lib/src/command_output.dart
+++ b/pkg/test_runner/lib/src/command_output.dart
@@ -812,7 +812,7 @@
   }
 }
 
-class SpecParseCommandOutput extends CommandOutput {
+class SpecParseCommandOutput extends CommandOutput with _StaticErrorOutput {
   SpecParseCommandOutput(
     Command command,
     int exitCode,
@@ -840,10 +840,54 @@
     if (hasTimedOut) return Expectation.timeout;
     if (hasNonUtf8) return Expectation.nonUtf8Error;
     if (truncatedOutput) return Expectation.truncatedOutput;
-    if (hasSyntaxError) return Expectation.syntaxError;
-    if (exitCode != 0) return Expectation.syntaxError;
+    if (testCase.testFile.isStaticErrorTest) {
+      return _validateExpectedErrors(testCase);
+    }
+    if (hasSyntaxError || exitCode != 0) return Expectation.syntaxError;
     return Expectation.pass;
   }
+
+  /// Parses the ANTLR parser's diagnostic output.
+  ///
+  /// For example: `void main() {(}` in 'foo.dart' yields
+  ///   Syntax error in foo.dart:
+  ///   line 1:14 no viable alternative at input '(}'
+  ///   Parsing failed
+  ///
+  /// The 'Syntax error' line is emitted once for each file with
+  /// a syntax error, and each error gets a 'line ...' message.
+  @override
+  void _parseErrors() {
+    var output = decodeUtf8(stderr);
+    var lines = output.split('\n');
+    String? currentPath;
+    var processCommand = command as ProcessCommand;
+    var fallbackPath = processCommand.arguments.firstWhere(
+      (arg) => arg.endsWith('.dart'),
+      orElse: () => '',
+    );
+    for (var line in lines) {
+      if (_pathRegExp.firstMatch(line) case var match?) {
+        currentPath = match[1];
+      } else if (_errorRegExp.firstMatch(line) case var match?) {
+        var lineNum = int.parse(match[1]!);
+        var column = int.parse(match[2]!) + 1; // ANTLR is 0-based.
+        var message = match[3]!;
+        addError(
+          StaticError(
+            ErrorSource.specParser,
+            message,
+            path: currentPath ?? fallbackPath,
+            line: lineNum,
+            column: column,
+          ),
+        );
+      }
+    }
+  }
+
+  static final RegExp _pathRegExp = .new(r"^Syntax error in (.*):$");
+  static final RegExp _errorRegExp = .new(r"^line (\d+):(\d+) (.*)$");
 }
 
 class VMCommandOutput extends CommandOutput with _UnittestSuiteMessagesMixin {
@@ -1537,6 +1581,7 @@
       Compiler.dart2wasm: ErrorSource.web,
       Compiler.ddc: ErrorSource.web,
       Compiler.fasta: ErrorSource.cfe,
+      Compiler.specParser: ErrorSource.specParser,
     }[testCase.configuration.compiler]!;
 
     var expected = testCase.testFile.expectedErrors.where(
diff --git a/pkg/test_runner/lib/src/static_error.dart b/pkg/test_runner/lib/src/static_error.dart
index 9036295..a818aa9 100644
--- a/pkg/test_runner/lib/src/static_error.dart
+++ b/pkg/test_runner/lib/src/static_error.dart
@@ -12,6 +12,7 @@
 class ErrorSource {
   static const analyzer = ErrorSource._("analyzer");
   static const cfe = ErrorSource._("CFE", marker: "cfe");
+  static const specParser = ErrorSource._("spec_parser");
   static const web = ErrorSource._("web");
 
   /// Pseudo-front end for context messages.
@@ -21,7 +22,7 @@
   ///
   /// The order is significant here. In static error tests, error expectations
   /// must be in this order for consistency.
-  static const all = [analyzer, cfe, web];
+  static const all = [analyzer, cfe, specParser, web];
 
   /// Gets the source whose lowercase name is [name] or `null` if no source
   /// with that name could be found.
@@ -453,6 +454,9 @@
         // TODO(rnystrom): If the web compilers report warnings, encode that in
         // the message somehow and then look for it here.
         return false;
+      case ErrorSource.specParser:
+        // The spec parser does not report warnings.
+        return false;
       default:
         break;
     }