bazel syntax: break dependency on lib.events.Event

This change removes nearly all dependencies on lib.event.Event,
in preparation for reversing the lib.syntax->lib.event dependency.

SyntaxError is renamed to SyntaxError.Exception, and SyntaxError
itself is a simple event-like location+message type.

PiperOrigin-RevId: 304262913
diff --git a/src/main/java/com/google/devtools/build/lib/events/Event.java b/src/main/java/com/google/devtools/build/lib/events/Event.java
index 7a64c71..fbb3405 100644
--- a/src/main/java/com/google/devtools/build/lib/events/Event.java
+++ b/src/main/java/com/google/devtools/build/lib/events/Event.java
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.util.io.FileOutErr.OutputReference;
 import java.io.Serializable;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
@@ -227,6 +228,22 @@
     }
   }
 
+  /**
+   * An Eventable is an event-like value that can be converted to an event. This interface is a
+   * transitional hack until the lib.syntax-to-lib.events dependency can be reversed, at which point
+   * its use in {@link #replayEventsOn} will be replaced with a direct reference to {@code
+   * lib.syntax.SyntaxError}.
+   */
+  public interface Eventable {
+    Event toEvent();
+  }
+
+  public static void replayEventsOn(EventHandler eventHandler, List<? extends Eventable> events) {
+    for (Eventable event : events) {
+      eventHandler.handle(event.toEvent());
+    }
+  }
+
   public static Event of(EventKind kind, @Nullable Location location, String message) {
     return new Event(kind, location, message, null, null);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java
index b2f3f58..c8267ec 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java
@@ -280,7 +280,7 @@
     try {
       Object result = doEvaluate(thread, statement);
       return DebuggerSerialization.getValueProto(objectMap, "Evaluation result", result);
-    } catch (SyntaxError | EvalException | InterruptedException e) {
+    } catch (SyntaxError.Exception | EvalException | InterruptedException e) {
       throw new DebugRequestException(e.getMessage());
     }
   }
@@ -294,7 +294,7 @@
    * running.
    */
   private Object doEvaluate(StarlarkThread thread, String content)
-      throws SyntaxError, EvalException, InterruptedException {
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     try {
       servicingEvalRequest.set(true);
 
@@ -388,7 +388,7 @@
     }
     try {
       return Starlark.truth(doEvaluate(thread, condition));
-    } catch (SyntaxError | EvalException | InterruptedException e) {
+    } catch (SyntaxError.Exception | EvalException | InterruptedException e) {
       throw new ConditionalBreakpointException(e.getMessage());
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
index 0fca831..6540f0a 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -741,10 +741,10 @@
    */
   public static void exec(
       ParserInput input, FileOptions options, Module module, StarlarkThread thread)
-      throws SyntaxError, EvalException, InterruptedException {
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     StarlarkFile file = parseAndValidate(input, options, module);
     if (!file.ok()) {
-      throw new SyntaxError(file.errors());
+      throw new SyntaxError.Exception(file.errors());
     }
     exec(file, module, thread);
   }
@@ -772,7 +772,7 @@
    */
   public static Object eval(
       ParserInput input, FileOptions options, Module module, StarlarkThread thread)
-      throws SyntaxError, EvalException, InterruptedException {
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     Expression expr = Expression.parse(input, options);
     ValidationEnvironment.validateExpr(expr, module, options);
 
@@ -802,11 +802,11 @@
   @Nullable
   public static Object execAndEvalOptionalFinalExpression(
       ParserInput input, FileOptions options, Module module, StarlarkThread thread)
-      throws SyntaxError, EvalException, InterruptedException {
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     StarlarkFile file = StarlarkFile.parse(input, options);
     ValidationEnvironment.validateFile(file, module);
     if (!file.ok()) {
-      throw new SyntaxError(file.errors());
+      throw new SyntaxError.Exception(file.errors());
     }
 
     // If the final statement is an expression, synthesize a return statement.
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Expression.java b/src/main/java/com/google/devtools/build/lib/syntax/Expression.java
index d730763..cb1cb3d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Expression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Expression.java
@@ -52,12 +52,13 @@
   public abstract Kind kind();
 
   /** Parses an expression with the default options. */
-  public static Expression parse(ParserInput input) throws SyntaxError {
+  public static Expression parse(ParserInput input) throws SyntaxError.Exception {
     return parse(input, FileOptions.DEFAULT);
   }
 
   /** Parses an expression. */
-  public static Expression parse(ParserInput input, FileOptions options) throws SyntaxError {
+  public static Expression parse(ParserInput input, FileOptions options)
+      throws SyntaxError.Exception {
     return Parser.parseExpression(input, options);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java b/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
index f7e7a86..0756b5e 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
@@ -17,7 +17,6 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
-import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import java.util.ArrayList;
@@ -73,7 +72,7 @@
   private int openParenStackDepth = 0;
 
   // List of errors appended to by Lexer and Parser.
-  private final List<Event> errors;
+  private final List<SyntaxError> errors;
 
   /**
    * True after a NEWLINE token.
@@ -84,7 +83,7 @@
   private int dents; // number of saved INDENT (>0) or OUTDENT (<0) tokens to return
 
   /** Constructs a lexer which tokenizes the parser input. Errors are appended to {@code errors}. */
-  Lexer(ParserInput input, FileOptions options, List<Event> errors) {
+  Lexer(ParserInput input, FileOptions options, List<SyntaxError> errors) {
     this.lnt = LineNumberTable.create(input.getContent(), input.getFile());
     this.options = options;
     this.buffer = input.getContent();
@@ -137,7 +136,7 @@
   }
 
   private void error(String message, int start, int end) {
-    errors.add(Event.error(createLocation(start, end), message));
+    errors.add(new SyntaxError(createLocation(start, end), message));
   }
 
   LexerLocation createLocation(int start, int end) {
@@ -416,13 +415,13 @@
             default:
               // unknown char escape => "\literal"
               if (options.restrictStringEscapes()) {
-                errors.add(
-                    Event.error(
-                        createLocation(pos - 1, pos),
-                        "invalid escape sequence: \\"
-                            + c
-                            + ". You can enable unknown escape sequences by passing the flag "
-                            + "--incompatible_restrict_string_escapes=false"));
+                error(
+                    "invalid escape sequence: \\"
+                        + c
+                        + ". You can enable unknown escape sequences by passing the flag"
+                        + " --incompatible_restrict_string_escapes=false",
+                    pos - 1,
+                    pos);
               }
               literal.append('\\');
               literal.append(c);
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ParamDescriptor.java b/src/main/java/com/google/devtools/build/lib/syntax/ParamDescriptor.java
index c0c8c65..2e9cd94 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ParamDescriptor.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ParamDescriptor.java
@@ -217,7 +217,7 @@
       x = EvalUtils.eval(ParserInput.fromLines(expr), FileOptions.DEFAULT, module, thread);
     } catch (InterruptedException ex) {
       throw new IllegalStateException(ex); // can't happen
-    } catch (SyntaxError | EvalException ex) {
+    } catch (SyntaxError.Exception | EvalException ex) {
       throw new IllegalArgumentException(
           String.format(
               "failed to evaluate default value '%s' of parameter '%s': %s",
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
index 01fe5d2..bbb8ed4 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
@@ -52,13 +51,13 @@
 
     // Errors encountered during scanning or parsing.
     // These lists are ultimately owned by StarlarkFile.
-    final List<Event> errors;
+    final List<SyntaxError> errors;
 
     ParseResult(
         List<Statement> statements,
         List<Comment> comments,
         Lexer.LexerLocation location,
-        List<Event> errors) {
+        List<SyntaxError> errors) {
       // No need to copy here; when the object is created, the parser instance is just about to go
       // out of scope and be garbage collected.
       this.statements = Preconditions.checkNotNull(statements);
@@ -106,7 +105,7 @@
   private static final boolean DEBUGGING = false;
 
   private final Lexer lexer;
-  private final List<Event> errors;
+  private final List<SyntaxError> errors;
 
   // TODO(adonovan): opt: compute this by subtraction.
   private static final Map<TokenKind, TokenKind> augmentedAssignments =
@@ -155,7 +154,7 @@
   // Intern string literals, as some files contain many literals for the same string.
   private final Map<String, String> stringInterner = new HashMap<>();
 
-  private Parser(Lexer lexer, List<Event> errors) {
+  private Parser(Lexer lexer, List<SyntaxError> errors) {
     this.lexer = lexer;
     this.errors = errors;
     nextToken();
@@ -179,7 +178,7 @@
 
   // Main entry point for parsing a file.
   static ParseResult parseFile(ParserInput input, FileOptions options) {
-    List<Event> errors = new ArrayList<>();
+    List<SyntaxError> errors = new ArrayList<>();
     Lexer lexer = new Lexer(input, options, errors);
     Parser parser = new Parser(lexer, errors);
     List<Statement> statements;
@@ -208,8 +207,9 @@
   }
 
   /** Parses an expression, possibly followed by newline tokens. */
-  static Expression parseExpression(ParserInput input, FileOptions options) throws SyntaxError {
-    List<Event> errors = new ArrayList<>();
+  static Expression parseExpression(ParserInput input, FileOptions options)
+      throws SyntaxError.Exception {
+    List<SyntaxError> errors = new ArrayList<>();
     Lexer lexer = new Lexer(input, options, errors);
     Parser parser = new Parser(lexer, errors);
     Expression result = parser.parseExpression();
@@ -218,7 +218,7 @@
     }
     parser.expect(TokenKind.EOF);
     if (!errors.isEmpty()) {
-      throw new SyntaxError(errors);
+      throw new SyntaxError.Exception(errors);
     }
     return result;
   }
@@ -252,7 +252,7 @@
     errorsCount++;
     // Limit the number of reported errors to avoid spamming output.
     if (errorsCount <= 5) {
-      errors.add(Event.error(location, message));
+      errors.add(new SyntaxError(location, message));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java b/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java
index 08d309c..7da88ee 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java
@@ -487,7 +487,7 @@
    */
   public static Module exec(
       StarlarkThread thread, ParserInput input, Map<String, Object> predeclared)
-      throws SyntaxError, EvalException, InterruptedException {
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     // Pseudocode:
     // file = StarlarkFile.parse(input)
     // validateFile(file, predeclared.keys, thread.semantics)
@@ -505,7 +505,7 @@
    * exception.
    */
   public static Object eval(StarlarkThread thread, ParserInput input, Map<String, Object> env)
-      throws SyntaxError, EvalException, InterruptedException {
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     // Pseudocode:
     // StarlarkFunction fn = exprFunc(input, env, thread.semantics)
     // return call(thread, fn)
@@ -517,8 +517,9 @@
    * it. If the final statement is an expression, return its value.
    *
    * <p>This complicated function, which combines exec and eval, is intended for use in a REPL or
-   * debugger. In case of parse of validation error, it throws SyntaxError. In case of execution
-   * error, the function returns partial results: the incomplete module plus the exception.
+   * debugger. In case of parse of validation error, it throws SyntaxError.Exception. In case of
+   * execution error, the function returns partial results: the incomplete module plus the
+   * exception.
    *
    * <p>Assignments in the input act as updates to a new module created by this function, which is
    * returned.
@@ -538,7 +539,7 @@
    */
   public static ModuleAndValue execAndEval(
       StarlarkThread thread, ParserInput input, Map<String, Object> predeclared)
-      throws SyntaxError {
+      throws SyntaxError.Exception {
     // Pseudocode:
     // file = StarlarkFile.parse(input)
     // validateFile(file, predeclared.keys, thread.semantics)
@@ -568,7 +569,7 @@
   /**
    * Parse the input as a file, validates it in the specified predeclared environment (a set of
    * names, optionally filtered by the semantics), and compiles it to a Program. It throws
-   * SyntaxError in case of scan/parse/validation error.
+   * SyntaxError.Exception in case of scan/parse/validation error.
    *
    * <p>In addition to the program, it returns the validated syntax tree. This permits clients such
    * as Bazel to inspect the syntax (for BUILD dialect checks, glob prefetching, etc.)
@@ -577,7 +578,7 @@
       ParserInput input, //
       Set<String> predeclared,
       StarlarkSemantics semantics)
-      throws SyntaxError {
+      throws SyntaxError.Exception {
     // Pseudocode:
     // file = StarlarkFile.parse(input)
     // validateFile(file, predeclared.keys, thread.semantics)
@@ -621,7 +622,7 @@
       ParserInput input, //
       Map<String, Object> env,
       StarlarkSemantics semantics)
-      throws SyntaxError {
+      throws SyntaxError.Exception {
     // Pseudocode:
     // expr = Expression.parse(input)
     // validateExpr(expr, env.keys, semantics)
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkFile.java b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkFile.java
index c009711..b5d222d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkFile.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkFile.java
@@ -15,7 +15,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.hash.HashCode;
-import com.google.devtools.build.lib.events.Event;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
@@ -32,14 +31,14 @@
   private final ImmutableList<Statement> statements;
   private final FileOptions options;
   private final ImmutableList<Comment> comments;
-  final List<Event> errors; // appended to by ValidationEnvironment
+  final List<SyntaxError> errors; // appended to by ValidationEnvironment
   @Nullable private final String contentHashCode;
 
   private StarlarkFile(
       ImmutableList<Statement> statements,
       FileOptions options,
       ImmutableList<Comment> comments,
-      List<Event> errors,
+      List<SyntaxError> errors,
       String contentHashCode,
       Lexer.LexerLocation location) {
     this.statements = statements;
@@ -81,7 +80,7 @@
    * Returns an unmodifiable view of the list of scanner, parser, and (perhaps) resolver errors
    * accumulated in this Starlark file.
    */
-  public List<Event> errors() {
+  public List<SyntaxError> errors() {
     return Collections.unmodifiableList(errors);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxError.java b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxError.java
index 112fcce..e7a0e35 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxError.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxError.java
@@ -14,54 +14,92 @@
 
 package com.google.devtools.build.lib.syntax;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
-import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Event; // TODO(adonovan): break dependency
+import com.google.devtools.build.lib.events.Location;
 import java.util.List;
 
 /**
- * An exception that indicates a static error associated with the syntax, such as scanner or parse
- * error, a structural problem, or a failure of identifier resolution. The exception records one or
- * more errors, each with a syntax location.
- *
- * <p>SyntaxError is thrown by operations such as {@link Expression#parse}, which are "all or
- * nothing". By contrast, {@link StarlarkFile#parse} does not throw an exception; instead, it
- * records the accumulated scanner, parser, and optionally validation errors within the syntax tree,
- * so that clients may obtain partial information from a damaged file.
- *
- * <p>Clients that fail abruptly when encountering parse errors are encouraged to use SyntaxError,
- * as in this example:
- *
- * <pre>
- * StarlarkFile file = StarlarkFile.parse(input);
- * if (!file.ok()) {
- *     throw new SyntaxError(file.errors());
- * }
- * </pre>
+ * A SyntaxError represents a static error associated with the syntax, such as a scanner or parse
+ * error, a structural problem, or a failure of identifier resolution. It records a description of
+ * the error and its location in the syntax.
  */
-public final class SyntaxError extends Exception {
+public final class SyntaxError implements Event.Eventable {
 
-  private final ImmutableList<Event> errors;
+  private final Location location;
+  private final String message;
 
-  /** Construct a SyntaxError from a non-empty list of errors. */
-  public SyntaxError(List<Event> errors) {
-    if (errors.isEmpty()) {
-      throw new IllegalArgumentException("no errors");
-    }
-    this.errors = ImmutableList.copyOf(errors);
+  public SyntaxError(Location location, String message) {
+    this.location = Preconditions.checkNotNull(location);
+    this.message = Preconditions.checkNotNull(message);
   }
 
-  /** Returns an immutable non-empty list of errors. */
-  public ImmutableList<Event> errors() {
-    return errors;
+  /** Returns the location of the error. */
+  public Location location() {
+    return location;
+  }
+
+  /** Returns a description of the error. */
+  public String message() {
+    return message;
+  }
+
+  /** Returns a string of the form {@code "foo.star:1:2: oops"}. */
+  @Override
+  public String toString() {
+    return location + ": " + message;
   }
 
   @Override
-  public String getMessage() {
-    String first = errors.get(0).getMessage();
-    if (errors.size() > 1) {
-      return String.format("%s (+ %d more)", first, errors.size() - 1);
-    } else {
-      return first;
+  public Event toEvent() {
+    return Event.error(location, message);
+  }
+
+  /**
+   * A SyntaxError.Exception is an exception holding one or more syntax errors.
+   *
+   * <p>SyntaxError.Exception is thrown by operations such as {@link Expression#parse}, which are
+   * "all or nothing". By contrast, {@link StarlarkFile#parse} does not throw an exception; instead,
+   * it records the accumulated scanner, parser, and optionally validation errors within the syntax
+   * tree, so that clients may obtain partial information from a damaged file.
+   *
+   * <p>Clients that fail abruptly when encountering parse errors are encouraged to throw
+   * SyntaxError.Exception, as in this example:
+   *
+   * <pre>
+   * StarlarkFile file = StarlarkFile.parse(input);
+   * if (!file.ok()) {
+   *     throw new SyntaxError.Exception(file.errors());
+   * }
+   * </pre>
+   */
+  public static final class Exception extends java.lang.Exception {
+
+    private final ImmutableList<SyntaxError> errors;
+
+    /** Construct a SyntaxError from a non-empty list of errors. */
+    public Exception(List<SyntaxError> errors) {
+      if (errors.isEmpty()) {
+        throw new IllegalArgumentException("no errors");
+      }
+      this.errors = ImmutableList.copyOf(errors);
+    }
+
+    /** Returns an immutable non-empty list of errors. */
+    public ImmutableList<SyntaxError> errors() {
+      return errors;
+    }
+
+    @Override
+    public String getMessage() {
+      String first = errors.get(0).message();
+      if (errors.size() > 1) {
+        // TODO(adonovan): say ("+ n more errors") to avoid ambiguity.
+        return String.format("%s (+ %d more)", first, errors.size() - 1);
+      } else {
+        return first;
+      }
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
index 3de6a0f..524ae6d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
@@ -15,7 +15,6 @@
 package com.google.devtools.build.lib.syntax;
 
 import com.google.common.base.Preconditions;
-import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.util.SpellChecker;
 import java.util.ArrayList;
@@ -40,9 +39,9 @@
  * nodes. (In the future, it will attach additional information to functions to support lexical
  * scope, and even compilation of the trees to bytecode.) Validation errors are reported in the
  * analogous manner to scan/parse errors: for a StarlarkFile, they are appended to {@code
- * StarlarkFile.errors}; for an expression they are reported by an SyntaxError exception. It is
- * legal to validate a file that already contains scan/parse errors, though it may lead to secondary
- * validation errors.
+ * StarlarkFile.errors}; for an expression they are reported by an SyntaxError.Exception exception.
+ * It is legal to validate a file that already contains scan/parse errors, though it may lead to
+ * secondary validation errors.
  */
 // TODO(adonovan): make this class private. Call it through the EvalUtils facade.
 public final class ValidationEnvironment extends NodeVisitor {
@@ -105,13 +104,13 @@
     }
   }
 
-  private final List<Event> errors;
+  private final List<SyntaxError> errors;
   private final FileOptions options;
   private final Module module;
   private Block block;
   private int loopCount;
 
-  private ValidationEnvironment(List<Event> errors, Module module, FileOptions options) {
+  private ValidationEnvironment(List<SyntaxError> errors, Module module, FileOptions options) {
     this.errors = errors;
     this.module = module;
     this.options = options;
@@ -122,7 +121,7 @@
   }
 
   void addError(Location loc, String message) {
-    errors.add(Event.error(loc, message));
+    errors.add(new SyntaxError(loc, message));
   }
 
   /**
@@ -486,14 +485,14 @@
    * defined by {@code module}. This operation mutates the Expression.
    */
   public static void validateExpr(Expression expr, Module module, FileOptions options)
-      throws SyntaxError {
-    List<Event> errors = new ArrayList<>();
+      throws SyntaxError.Exception {
+    List<SyntaxError> errors = new ArrayList<>();
     ValidationEnvironment venv = new ValidationEnvironment(errors, module, options);
 
     venv.visit(expr);
 
     if (!errors.isEmpty()) {
-      throw new SyntaxError(errors);
+      throw new SyntaxError.Exception(errors);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/starlark/cmd/BUILD b/src/main/java/com/google/devtools/starlark/cmd/BUILD
index e33f97f..5e28606 100644
--- a/src/main/java/com/google/devtools/starlark/cmd/BUILD
+++ b/src/main/java/com/google/devtools/starlark/cmd/BUILD
@@ -19,10 +19,7 @@
     ],
     main_class = "com.google.devtools.starlark.cmd.Starlark",
     visibility = ["//visibility:public"],
-    deps = [
-        "//src/main/java/com/google/devtools/build/lib:events",
-        "//src/main/java/com/google/devtools/build/lib:syntax",
-    ],
+    deps = ["//src/main/java/com/google/devtools/build/lib:syntax"],
 )
 
 filegroup(
diff --git a/src/main/java/com/google/devtools/starlark/cmd/Starlark.java b/src/main/java/com/google/devtools/starlark/cmd/Starlark.java
index 5cc353d..72375a4 100644
--- a/src/main/java/com/google/devtools/starlark/cmd/Starlark.java
+++ b/src/main/java/com/google/devtools/starlark/cmd/Starlark.java
@@ -13,7 +13,6 @@
 // limitations under the License.
 package com.google.devtools.starlark.cmd;
 
-import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
 import com.google.devtools.build.lib.syntax.FileOptions;
@@ -104,9 +103,9 @@
         if (result != null) {
           System.out.println(com.google.devtools.build.lib.syntax.Starlark.repr(result));
         }
-      } catch (SyntaxError ex) {
-        for (Event ev : ex.errors()) {
-          System.err.println(ev);
+      } catch (SyntaxError.Exception ex) {
+        for (SyntaxError error : ex.errors()) {
+          System.err.println(error);
         }
       } catch (EvalException ex) {
         System.err.println(ex.print());
@@ -133,9 +132,9 @@
     try {
       EvalUtils.exec(ParserInput.create(content, filename), options, module, thread);
       return 0;
-    } catch (SyntaxError ex) {
-      for (Event ev : ex.errors()) {
-        System.err.println(ev);
+    } catch (SyntaxError.Exception ex) {
+      for (SyntaxError error : ex.errors()) {
+        System.err.println(error);
       }
       return 1;
     } catch (EvalException ex) {
diff --git a/src/test/java/com/google/devtools/build/lib/packages/SelectTest.java b/src/test/java/com/google/devtools/build/lib/packages/SelectTest.java
index 5984f04..7bec95b 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/SelectTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/SelectTest.java
@@ -34,7 +34,8 @@
 @RunWith(JUnit4.class)
 public class SelectTest {
 
-  private static Object eval(String expr) throws SyntaxError, EvalException, InterruptedException {
+  private static Object eval(String expr)
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     ParserInput input = ParserInput.fromLines(expr);
     StarlarkThread thread =
         StarlarkThread.builder(Mutability.create("test"))
diff --git a/src/test/java/com/google/devtools/build/lib/profiler/memory/AllocationTrackerTest.java b/src/test/java/com/google/devtools/build/lib/profiler/memory/AllocationTrackerTest.java
index b1f30bc..8cb0d92 100644
--- a/src/test/java/com/google/devtools/build/lib/profiler/memory/AllocationTrackerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/profiler/memory/AllocationTrackerTest.java
@@ -188,7 +188,8 @@
     assertThat(rules).containsExactly("myrule", new RuleBytes("myrule").addBytes(128L));
   }
 
-  private void exec(String... lines) throws SyntaxError, EvalException, InterruptedException {
+  private void exec(String... lines)
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     ParserInput input = ParserInput.create(Joiner.on("\n").join(lines), "a.star");
     Mutability mu = Mutability.create("test");
     StarlarkThread thread =
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
index 62cf58e..f653a64 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
@@ -738,7 +738,7 @@
     Module module = thread.getGlobals();
     StarlarkFile file = EvalUtils.parseAndValidate(input, FileOptions.DEFAULT, module);
     if (!file.ok()) {
-      throw new SyntaxError(file.errors());
+      throw new SyntaxError.Exception(file.errors());
     }
     SkylarkImportLookupFunction.execAndExport(file, FAKE_LABEL, ev.getEventHandler(), thread);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
index 5ee621c..99c63be 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
@@ -17,9 +17,10 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.devtools.build.lib.events.EventCollector;
 import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -32,8 +33,9 @@
 
   @Test
   public void testExecutionStopsAtFirstError() throws Exception {
-    EventCollector printEvents = new EventCollector();
-    StarlarkThread thread = createStarlarkThread(StarlarkThread.makeDebugPrintHandler(printEvents));
+    List<String> printEvents = new ArrayList<>();
+    StarlarkThread thread =
+        createStarlarkThread(/*printHandler=*/ (_thread, msg) -> printEvents.add(msg));
     ParserInput input = ParserInput.fromLines("print('hello'); x = 1//0; print('goodbye')");
 
     Module module = thread.getGlobals();
@@ -41,14 +43,16 @@
         EvalException.class, () -> EvalUtils.exec(input, FileOptions.DEFAULT, module, thread));
 
     // Only expect hello, should have been an error before goodbye.
-    assertThat(printEvents).hasSize(1);
-    assertThat(printEvents.iterator().next().getMessage()).isEqualTo("hello");
+    assertThat(printEvents.toString()).isEqualTo("[hello]");
   }
 
   @Test
   public void testExecutionNotStartedOnInterrupt() throws Exception {
-    EventCollector printEvents = new EventCollector();
-    StarlarkThread thread = createStarlarkThread(StarlarkThread.makeDebugPrintHandler(printEvents));
+    StarlarkThread thread =
+        createStarlarkThread(
+            /*printHandler=*/ (_thread, msg) -> {
+              throw new AssertionError("print statement was reached");
+            });
     ParserInput input = ParserInput.fromLines("print('hello');");
     Module module = thread.getGlobals();
 
@@ -61,8 +65,6 @@
       // Reset interrupt bit in case the test failed to do so.
       Thread.interrupted();
     }
-
-    assertThat(printEvents).isEmpty();
   }
 
   @Test
@@ -564,7 +566,7 @@
   }
 
   private static void execBUILD(String... lines)
-      throws SyntaxError, EvalException, InterruptedException {
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     ParserInput input = ParserInput.fromLines(lines);
     StarlarkThread thread =
         StarlarkThread.builder(Mutability.create("test")).useDefaultSemantics().build();
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java b/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java
index 8d35d8f..f2f2f22 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java
@@ -16,7 +16,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
-import com.google.devtools.build.lib.testutil.MoreAsserts;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
@@ -40,18 +39,6 @@
   }
 
   @Test
-  public void testFunctionDefDuplicateArguments() throws Exception {
-    // TODO(adonovan): move to ParserTest.
-    ParserInput input =
-        ParserInput.fromLines(
-            "def func(a,b,a):", //
-            "  a = 1\n");
-    StarlarkFile file = StarlarkFile.parse(input);
-    MoreAsserts.assertContainsEvent(
-        file.errors(), "duplicate parameter name in function definition");
-  }
-
-  @Test
   public void testFunctionDefCallOuterFunc() throws Exception {
     List<Object> params = new ArrayList<>();
     createOuterFunction(params);
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LValueBoundNamesTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LValueBoundNamesTest.java
index df33118..425286a 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/LValueBoundNamesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LValueBoundNamesTest.java
@@ -14,7 +14,6 @@
 package com.google.devtools.build.lib.syntax;
 
 import com.google.common.truth.Truth;
-import com.google.devtools.build.lib.events.Event;
 import java.util.Arrays;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -54,8 +53,8 @@
   private static void assertBoundNames(String assignment, String... expectedBoundNames) {
     ParserInput input = ParserInput.fromLines(assignment);
     StarlarkFile file = StarlarkFile.parse(input);
-    for (Event error : file.errors()) {
-      throw new AssertionError(error);
+    if (!file.ok()) {
+      throw new AssertionError(new SyntaxError.Exception(file.errors()));
     }
     Expression lhs = ((AssignmentStatement) file.getStatements().get(0)).getLHS();
     Set<String> boundNames =
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java
index 3b727eb..831052c 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java
@@ -15,7 +15,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.devtools.build.lib.events.Event;
+import com.google.common.base.Joiner;
 import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
 import java.util.ArrayList;
 import java.util.List;
@@ -31,7 +31,7 @@
 
   // TODO(adonovan): make these these tests less unnecessarily stateful.
 
-  private final List<Event> errors = new ArrayList<>();
+  private final List<SyntaxError> errors = new ArrayList<>();
   private String lastError;
 
   /**
@@ -53,9 +53,8 @@
       result.add(tok.copy());
     } while (tok.kind != TokenKind.EOF);
 
-    for (Event error : errors) {
-      lastError =
-          error.getLocation().file() + ":" + error.getLocation().line() + ": " + error.getMessage();
+    for (SyntaxError error : errors) {
+      lastError = error.location().file() + ":" + error.location().line() + ": " + error.message();
     }
 
     return result;
@@ -516,4 +515,24 @@
   public void testLexerLocationCodec() throws Exception {
     new SerializationTester(createLexer("foo").createLocation(0, 2)).runTests();
   }
+
+  /**
+   * Returns the first error whose string form contains the specified substring, or throws an
+   * informative AssertionError if there is none.
+   *
+   * <p>Exposed for use by other frontend tests.
+   */
+  static SyntaxError assertContainsError(List<SyntaxError> errors, String substr) {
+    for (SyntaxError error : errors) {
+      if (error.toString().contains(substr)) {
+        return error;
+      }
+    }
+    if (errors.isEmpty()) {
+      throw new AssertionError("no errors, want '" + substr + "'");
+    } else {
+      throw new AssertionError(
+          "error '" + substr + "' not found, but got these:\n" + Joiner.on("\n").join(errors));
+    }
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/NodeVisitorTest.java b/src/test/java/com/google/devtools/build/lib/syntax/NodeVisitorTest.java
index 9ae38aa..2b7dcaa 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/NodeVisitorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/NodeVisitorTest.java
@@ -25,11 +25,11 @@
 @RunWith(JUnit4.class)
 public final class NodeVisitorTest {
 
-  private static StarlarkFile parse(String... lines) throws SyntaxError {
+  private static StarlarkFile parse(String... lines) throws SyntaxError.Exception {
     ParserInput input = ParserInput.fromLines(lines);
     StarlarkFile file = StarlarkFile.parse(input);
     if (!file.ok()) {
-      throw new SyntaxError(file.errors());
+      throw new SyntaxError.Exception(file.errors());
     }
     return file;
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
index f1a402b..3aebe9a 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
@@ -20,10 +20,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import com.google.devtools.build.lib.events.Event;
-import com.google.devtools.build.lib.events.EventCollector;
 import com.google.devtools.build.lib.events.Location;
-import com.google.devtools.build.lib.testutil.MoreAsserts;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -35,11 +32,11 @@
 @RunWith(JUnit4.class)
 public final class ParserTest {
 
-  private final EventCollector events = new EventCollector();
+  private final List<SyntaxError> events = new ArrayList<>();
   private boolean failFast = true;
 
-  private Event assertContainsError(String expectedMessage) {
-    return MoreAsserts.assertContainsEvent(events, expectedMessage);
+  private SyntaxError assertContainsError(String expectedMessage) {
+    return LexerTest.assertContainsError(events, expectedMessage);
   }
 
   private void setFailFast(boolean failFast) {
@@ -47,7 +44,7 @@
   }
 
   // Joins the lines, parse, and returns an expression.
-  private static Expression parseExpression(String... lines) throws SyntaxError {
+  private static Expression parseExpression(String... lines) throws SyntaxError.Exception {
     ParserInput input = ParserInput.fromLines(lines);
     return Expression.parse(input);
   }
@@ -59,32 +56,33 @@
     try {
       Expression.parse(input);
       throw new AssertionError("parseExpression(%s) succeeded unexpectedly: " + src);
-    } catch (SyntaxError ex) {
-      return ex.errors().get(0).getMessage();
+    } catch (SyntaxError.Exception ex) {
+      return ex.errors().get(0).message();
     }
   }
 
   // Joins the lines, parses, and returns a file.
   // Errors are added to this.events, or thrown if this.failFast;
-  private StarlarkFile parseFile(String... lines) throws SyntaxError {
+  private StarlarkFile parseFile(String... lines) throws SyntaxError.Exception {
     ParserInput input = ParserInput.fromLines(lines);
     StarlarkFile file = StarlarkFile.parse(input);
     if (!file.ok()) {
       if (failFast) {
-        throw new SyntaxError(file.errors());
+        throw new SyntaxError.Exception(file.errors());
       }
-      Event.replayEventsOn(events, file.errors());
+      // TODO(adonovan): return these, and eliminate a stateful field.
+      events.addAll(file.errors());
     }
     return file;
   }
 
   // Joins the lines, parses, and returns the sole statement.
-  private Statement parseStatement(String... lines) throws SyntaxError {
+  private Statement parseStatement(String... lines) throws SyntaxError.Exception {
     return Iterables.getOnlyElement(parseStatements(lines));
   }
 
   // Joins the lines, parses, and returns the statements.
-  private List<Statement> parseStatements(String... lines) throws SyntaxError {
+  private ImmutableList<Statement> parseStatements(String... lines) throws SyntaxError.Exception {
     return parseFile(lines).getStatements();
   }
 
@@ -341,7 +339,8 @@
     assertLocation(0, 14, slice);
   }
 
-  private static void evalSlice(String statement, Object... expectedArgs) throws SyntaxError {
+  private static void evalSlice(String statement, Object... expectedArgs)
+      throws SyntaxError.Exception {
     SliceExpression e = (SliceExpression) parseExpression(statement);
 
     // There is no way to evaluate the expression here, so we rely on string comparison.
@@ -401,7 +400,7 @@
   }
 
   @Test
-  public void testSecondaryLocation() throws SyntaxError {
+  public void testSecondaryLocation() throws SyntaxError.Exception {
     String expr = "f(1 % 2)";
     CallExpression call = (CallExpression) parseExpression(expr);
     Argument arg = call.getArguments().get(0);
@@ -409,7 +408,7 @@
   }
 
   @Test
-  public void testPrimaryLocation() throws SyntaxError {
+  public void testPrimaryLocation() throws SyntaxError.Exception {
     String expr = "f(1 + 2)";
     CallExpression call = (CallExpression) parseExpression(expr);
     Argument arg = call.getArguments().get(0);
@@ -621,7 +620,7 @@
     assertStatementLocationCorrect("def foo():\n  pass");
   }
 
-  private void assertStatementLocationCorrect(String stmtStr) throws SyntaxError {
+  private void assertStatementLocationCorrect(String stmtStr) throws SyntaxError.Exception {
     Statement stmt = parseStatement(stmtStr);
     assertThat(getText(stmtStr, stmt)).isEqualTo(stmtStr);
     // Also try it with another token at the end (newline), which broke the location in the past.
@@ -629,7 +628,7 @@
     assertThat(getText(stmtStr, stmt)).isEqualTo(stmtStr);
   }
 
-  private static void assertExpressionLocationCorrect(String exprStr) throws SyntaxError {
+  private static void assertExpressionLocationCorrect(String exprStr) throws SyntaxError.Exception {
     Expression expr = parseExpression(exprStr);
     assertThat(getText(exprStr, expr)).isEqualTo(exprStr);
     // Also try it with another token at the end (newline), which broke the location in the past.
@@ -866,10 +865,10 @@
             "  b = 2 * * 5", // parse error
             "");
 
-    assertThat(events).hasSize(3);
     assertContainsError("syntax error at 'for': expected newline");
     assertContainsError("syntax error at 'ada': expected newline");
     assertContainsError("syntax error at '*': expected expression");
+    assertThat(events).hasSize(3);
     assertThat(statements).hasSize(3);
   }
 
@@ -1182,7 +1181,7 @@
   }
 
   private void runLoadAliasTestForSymbols(String loadSymbolString, String... expectedSymbols)
-      throws SyntaxError {
+      throws SyntaxError.Exception {
     List<Statement> statements =
         parseStatements(String.format("load('//foo/bar:file.bzl', %s)\n", loadSymbolString));
     LoadStatement stmt = (LoadStatement) statements.get(0);
@@ -1335,6 +1334,15 @@
   }
 
   @Test
+  public void testFunctionDefDuplicateArguments() throws Exception {
+    setFailFast(false);
+    parseFile(
+        "def func(a,b,a):", //
+        "  a = 1\n");
+    assertContainsError("duplicate parameter name in function definition");
+  }
+
+  @Test
   public void testStringsAreDeduped() throws Exception {
     StarlarkFile file = parseFile("L1 = ['cat', 'dog', 'fish']", "L2 = ['dog', 'fish', 'cat']");
     Set<String> uniqueStringInstances = Sets.newIdentityHashSet();
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/PrettyPrintTest.java b/src/test/java/com/google/devtools/build/lib/syntax/PrettyPrintTest.java
index e2a9a0f..cf420bb 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/PrettyPrintTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/PrettyPrintTest.java
@@ -26,20 +26,20 @@
 @RunWith(JUnit4.class)
 public final class PrettyPrintTest {
 
-  private static StarlarkFile parseFile(String... lines) throws SyntaxError {
+  private static StarlarkFile parseFile(String... lines) throws SyntaxError.Exception {
     ParserInput input = ParserInput.fromLines(lines);
     StarlarkFile file = StarlarkFile.parse(input);
     if (!file.ok()) {
-      throw new SyntaxError(file.errors());
+      throw new SyntaxError.Exception(file.errors());
     }
     return file;
   }
 
-  private static Statement parseStatement(String... lines) throws SyntaxError {
+  private static Statement parseStatement(String... lines) throws SyntaxError.Exception {
     return parseFile(lines).getStatements().get(0);
   }
 
-  private static Expression parseExpression(String... lines) throws SyntaxError {
+  private static Expression parseExpression(String... lines) throws SyntaxError.Exception {
     return Expression.parse(ParserInput.fromLines(lines));
   }
 
@@ -50,24 +50,24 @@
   /**
    * Asserts that the given node's pretty print at a given indent level matches the given string.
    */
-  private void assertPrettyMatches(Node node, int indentLevel, String expected) {
+  private static void assertPrettyMatches(Node node, int indentLevel, String expected) {
     StringBuilder buf = new StringBuilder();
     new NodePrinter(buf, indentLevel).printNode(node);
     assertThat(buf.toString()).isEqualTo(expected);
   }
 
   /** Asserts that the given node's pretty print with no indent matches the given string. */
-  private void assertPrettyMatches(Node node, String expected) {
+  private static void assertPrettyMatches(Node node, String expected) {
     assertPrettyMatches(node, 0, expected);
   }
 
   /** Asserts that the given node's pretty print with one indent matches the given string. */
-  private void assertIndentedPrettyMatches(Node node, String expected) {
+  private static void assertIndentedPrettyMatches(Node node, String expected) {
     assertPrettyMatches(node, 1, expected);
   }
 
   /** Asserts that the given node's {@code toString} matches the given string. */
-  private void assertTostringMatches(Node node, String expected) {
+  private static void assertTostringMatches(Node node, String expected) {
     assertThat(node.toString()).isEqualTo(expected);
   }
 
@@ -75,7 +75,8 @@
    * Parses the given string as an expression, and asserts that its pretty print matches the given
    * string.
    */
-  private void assertExprPrettyMatches(String source, String expected) throws SyntaxError {
+  private static void assertExprPrettyMatches(String source, String expected)
+      throws SyntaxError.Exception {
       Expression node = parseExpression(source);
       assertPrettyMatches(node, expected);
   }
@@ -84,7 +85,8 @@
    * Parses the given string as an expression, and asserts that its {@code toString} matches the
    * given string.
    */
-  private void assertExprTostringMatches(String source, String expected) throws SyntaxError {
+  private static void assertExprTostringMatches(String source, String expected)
+      throws SyntaxError.Exception {
       Expression node = parseExpression(source);
       assertThat(node.toString()).isEqualTo(expected);
   }
@@ -93,7 +95,7 @@
    * Parses the given string as an expression, and asserts that both its pretty print and {@code
    * toString} return the original string.
    */
-  private void assertExprBothRoundTrip(String source) throws SyntaxError {
+  private static void assertExprBothRoundTrip(String source) throws SyntaxError.Exception {
     assertExprPrettyMatches(source, source);
     assertExprTostringMatches(source, source);
   }
@@ -102,7 +104,8 @@
    * Parses the given string as a statement, and asserts that its pretty print with one indent
    * matches the given string.
    */
-  private void assertStmtIndentedPrettyMatches(String source, String expected) throws SyntaxError {
+  private static void assertStmtIndentedPrettyMatches(String source, String expected)
+      throws SyntaxError.Exception {
     Statement node = parseStatement(source);
     assertIndentedPrettyMatches(node, expected);
   }
@@ -111,7 +114,8 @@
    * Parses the given string as an statement, and asserts that its {@code toString} matches the
    * given string.
    */
-  private void assertStmtTostringMatches(String source, String expected) throws SyntaxError {
+  private static void assertStmtTostringMatches(String source, String expected)
+      throws SyntaxError.Exception {
     Statement node = parseStatement(source);
     assertThat(node.toString()).isEqualTo(expected);
   }
@@ -119,14 +123,14 @@
   // Expressions.
 
   @Test
-  public void abstractComprehension() throws SyntaxError {
+  public void abstractComprehension() throws SyntaxError.Exception {
     // Covers DictComprehension and ListComprehension.
     assertExprBothRoundTrip("[z for y in x if True for z in y]");
     assertExprBothRoundTrip("{z: x for y in x if True for z in y}");
   }
 
   @Test
-  public void binaryOperatorExpression() throws SyntaxError {
+  public void binaryOperatorExpression() throws SyntaxError.Exception {
     assertExprPrettyMatches("1 + 2", "(1 + 2)");
     assertExprTostringMatches("1 + 2", "1 + 2");
 
@@ -135,22 +139,22 @@
   }
 
   @Test
-  public void conditionalExpression() throws SyntaxError {
+  public void conditionalExpression() throws SyntaxError.Exception {
     assertExprBothRoundTrip("1 if True else 2");
   }
 
   @Test
-  public void dictExpression() throws SyntaxError {
+  public void dictExpression() throws SyntaxError.Exception {
     assertExprBothRoundTrip("{1: \"a\", 2: \"b\"}");
   }
 
   @Test
-  public void dotExpression() throws SyntaxError {
+  public void dotExpression() throws SyntaxError.Exception {
     assertExprBothRoundTrip("o.f");
   }
 
   @Test
-  public void funcallExpression() throws SyntaxError {
+  public void funcallExpression() throws SyntaxError.Exception {
     assertExprBothRoundTrip("f()");
     assertExprBothRoundTrip("f(a)");
     assertExprBothRoundTrip("f(a, b = B, c = C, *d, **e)");
@@ -158,22 +162,22 @@
   }
 
   @Test
-  public void identifier() throws SyntaxError {
+  public void identifier() throws SyntaxError.Exception {
     assertExprBothRoundTrip("foo");
   }
 
   @Test
-  public void indexExpression() throws SyntaxError {
+  public void indexExpression() throws SyntaxError.Exception {
     assertExprBothRoundTrip("a[i]");
   }
 
   @Test
-  public void integerLiteral() throws SyntaxError {
+  public void integerLiteral() throws SyntaxError.Exception {
     assertExprBothRoundTrip("5");
   }
 
   @Test
-  public void listLiteralShort() throws SyntaxError {
+  public void listLiteralShort() throws SyntaxError.Exception {
     assertExprBothRoundTrip("[]");
     assertExprBothRoundTrip("[5]");
     assertExprBothRoundTrip("[5, 6]");
@@ -183,7 +187,7 @@
   }
 
   @Test
-  public void listLiteralLong() throws SyntaxError {
+  public void listLiteralLong() throws SyntaxError.Exception {
     // List literals with enough elements to trigger the abbreviated toString() format.
     assertExprPrettyMatches("[1, 2, 3, 4, 5, 6]", "[1, 2, 3, 4, 5, 6]");
     assertExprTostringMatches("[1, 2, 3, 4, 5, 6]", "[1, 2, 3, 4, <2 more arguments>]");
@@ -193,7 +197,7 @@
   }
 
   @Test
-  public void listLiteralNested() throws SyntaxError {
+  public void listLiteralNested() throws SyntaxError.Exception {
     // Make sure that the inner list doesn't get abbreviated when the outer list is printed using
     // prettyPrint().
     assertExprPrettyMatches(
@@ -205,7 +209,7 @@
   }
 
   @Test
-  public void sliceExpression() throws SyntaxError {
+  public void sliceExpression() throws SyntaxError.Exception {
     assertExprBothRoundTrip("a[b:c:d]");
     assertExprBothRoundTrip("a[b:c]");
     assertExprBothRoundTrip("a[b:]");
@@ -216,13 +220,13 @@
   }
 
   @Test
-  public void stringLiteral() throws SyntaxError {
+  public void stringLiteral() throws SyntaxError.Exception {
     assertExprBothRoundTrip("\"foo\"");
     assertExprBothRoundTrip("\"quo\\\"ted\"");
   }
 
   @Test
-  public void unaryOperatorExpression() throws SyntaxError {
+  public void unaryOperatorExpression() throws SyntaxError.Exception {
     assertExprPrettyMatches("not True", "not (True)");
     assertExprTostringMatches("not True", "not True");
     assertExprPrettyMatches("-5", "-(5)");
@@ -232,25 +236,25 @@
   // Statements.
 
   @Test
-  public void assignmentStatement() throws SyntaxError {
+  public void assignmentStatement() throws SyntaxError.Exception {
     assertStmtIndentedPrettyMatches("x = y", "  x = y\n");
     assertStmtTostringMatches("x = y", "x = y\n");
   }
 
   @Test
-  public void augmentedAssignmentStatement() throws SyntaxError {
+  public void augmentedAssignmentStatement() throws SyntaxError.Exception {
     assertStmtIndentedPrettyMatches("x += y", "  x += y\n");
     assertStmtTostringMatches("x += y", "x += y\n");
   }
 
   @Test
-  public void expressionStatement() throws SyntaxError {
+  public void expressionStatement() throws SyntaxError.Exception {
     assertStmtIndentedPrettyMatches("5", "  5\n");
     assertStmtTostringMatches("5", "5\n");
   }
 
   @Test
-  public void defStatement() throws SyntaxError {
+  public void defStatement() throws SyntaxError.Exception {
     assertStmtIndentedPrettyMatches(
         join("def f(x):",
              "  print(x)"),
@@ -287,7 +291,7 @@
   }
 
   @Test
-  public void flowStatement() throws SyntaxError {
+  public void flowStatement() throws SyntaxError.Exception {
     // The parser would complain if we tried to construct them from source.
     Node breakNode = new FlowStatement(TokenKind.BREAK);
     assertIndentedPrettyMatches(breakNode, "  break\n");
@@ -299,7 +303,7 @@
   }
 
   @Test
-  public void forStatement() throws SyntaxError {
+  public void forStatement() throws SyntaxError.Exception {
     assertStmtIndentedPrettyMatches(
         join("for x in y:",
              "  print(x)"),
@@ -324,7 +328,7 @@
   }
 
   @Test
-  public void ifStatement() throws SyntaxError {
+  public void ifStatement() throws SyntaxError.Exception {
     assertStmtIndentedPrettyMatches(
         join("if True:",
              "  print(x)"),
@@ -361,7 +365,7 @@
   }
 
   @Test
-  public void loadStatement() throws SyntaxError {
+  public void loadStatement() throws SyntaxError.Exception {
     // load("foo.bzl", a="A", "B")
     Node loadStatement =
         new LoadStatement(
@@ -378,7 +382,7 @@
   }
 
   @Test
-  public void returnStatement() throws SyntaxError {
+  public void returnStatement() throws SyntaxError.Exception {
     assertIndentedPrettyMatches(
         new ReturnStatement(new StringLiteral("foo")),
         "  return \"foo\"\n");
@@ -396,7 +400,7 @@
   // Miscellaneous.
 
   @Test
-  public void buildFileAST() throws SyntaxError {
+  public void buildFileAST() throws SyntaxError.Exception {
     Node node = parseFile("print(x)\nprint(y)");
     assertIndentedPrettyMatches(
         node,
@@ -407,16 +411,15 @@
   }
 
   @Test
-  public void comment() throws SyntaxError {
+  public void comment() throws SyntaxError.Exception {
     Comment node = new Comment("foo");
     assertIndentedPrettyMatches(node, "  # foo");
     assertTostringMatches(node, "foo");
   }
 
   /* Not tested explicitly because they're covered implicitly by tests for other nodes:
-   * - LValue
    * - DictExpression.Entry
-   * - passed arguments / formal parameters
-   * - ConditionalStatements
+   * - Argument / Parameter
+   * - IfStatements
    */
 }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
index 1472976..0dd862a 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -946,7 +946,7 @@
 
   // TODO(adonovan): move this and all tests that use it to Validation tests.
   private void assertValidationError(String expectedError, final String... lines) throws Exception {
-    SyntaxError error = assertThrows(SyntaxError.class, () -> exec(lines));
+    SyntaxError.Exception error = assertThrows(SyntaxError.Exception.class, () -> exec(lines));
     assertThat(error).hasMessageThat().contains(expectedError);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java
index 4cd28bc..7ffd09a 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFileTest.java
@@ -16,8 +16,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.base.Joiner;
-import com.google.devtools.build.lib.events.Event;
-import com.google.devtools.build.lib.testutil.MoreAsserts;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -86,25 +84,25 @@
   public void testFailsIfNewlinesAreMissing() throws Exception {
     StarlarkFile file = parseFile("foo() bar() something = baz() bar()");
 
-    Event event =
-        MoreAsserts.assertContainsEvent(file.errors(), "syntax error at \'bar\': expected newline");
-    assertThat(event.getLocation().toString()).isEqualTo("foo.star:1:7");
+    SyntaxError error =
+        LexerTest.assertContainsError(file.errors(), "syntax error at \'bar\': expected newline");
+    assertThat(error.location().toString()).isEqualTo("foo.star:1:7");
   }
 
   @Test
   public void testImplicitStringConcatenationFails() throws Exception {
     StarlarkFile file = parseFile("a = 'foo' 'bar'");
-    Event event =
-        MoreAsserts.assertContainsEvent(
+    SyntaxError error =
+        LexerTest.assertContainsError(
             file.errors(), "Implicit string concatenation is forbidden, use the + operator");
-    assertThat(event.getLocation().toString()).isEqualTo("foo.star:1:10");
+    assertThat(error.location().toString()).isEqualTo("foo.star:1:10");
   }
 
   @Test
   public void testImplicitStringConcatenationAcrossLinesIsIllegal() throws Exception {
     StarlarkFile file = parseFile("a = 'foo'\n  'bar'");
 
-    Event event = MoreAsserts.assertContainsEvent(file.errors(), "indentation error");
-    assertThat(event.getLocation().toString()).isEqualTo("foo.star:2:2");
+    SyntaxError error = LexerTest.assertContainsError(file.errors(), "indentation error");
+    assertThat(error.location().toString()).isEqualTo("foo.star:2:2");
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadDebuggingTest.java b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadDebuggingTest.java
index 053ea1a..c35ce1e 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadDebuggingTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadDebuggingTest.java
@@ -227,9 +227,9 @@
     Module module = thread.getGlobals();
     module.put("a", 1);
 
-    SyntaxError e =
+    SyntaxError.Exception e =
         assertThrows(
-            SyntaxError.class,
+            SyntaxError.Exception.class,
             () ->
                 EvalUtils.execAndEvalOptionalFinalExpression(
                     ParserInput.fromLines("b"), FileOptions.DEFAULT, module, thread));
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java
index 6b790bb..b8c8230 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java
@@ -58,7 +58,7 @@
   @Test
   public void testReference() throws Exception {
     setFailFast(false);
-    SyntaxError e = assertThrows(SyntaxError.class, () -> eval("foo"));
+    SyntaxError.Exception e = assertThrows(SyntaxError.Exception.class, () -> eval("foo"));
     assertThat(e).hasMessageThat().isEqualTo("name 'foo' is not defined");
     update("foo", "bar");
     assertThat(eval("foo")).isEqualTo("bar");
@@ -67,7 +67,7 @@
   // Test assign and reference through interpreter:
   @Test
   public void testAssignAndReference() throws Exception {
-    SyntaxError e = assertThrows(SyntaxError.class, () -> eval("foo"));
+    SyntaxError.Exception e = assertThrows(SyntaxError.Exception.class, () -> eval("foo"));
     assertThat(e).hasMessageThat().isEqualTo("name 'foo' is not defined");
     exec("foo = 'bar'");
     assertThat(eval("foo")).isEqualTo("bar");
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java
index f3776e6..402dff6 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java
@@ -14,10 +14,9 @@
 package com.google.devtools.build.lib.syntax;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.devtools.build.lib.testutil.MoreAsserts.assertContainsEvent;
+import static com.google.devtools.build.lib.syntax.LexerTest.assertContainsError;
 
-import com.google.devtools.build.lib.events.Event;
-import com.google.devtools.build.lib.events.EventCollector;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -29,36 +28,34 @@
   private final FileOptions.Builder options = FileOptions.builder();
 
   // Validates a file using the current options.
-  private StarlarkFile validateFile(String... lines) throws SyntaxError {
+  private StarlarkFile validateFile(String... lines) throws SyntaxError.Exception {
     ParserInput input = ParserInput.fromLines(lines);
     Module module = Module.createForBuiltins(Starlark.UNIVERSE);
     return EvalUtils.parseAndValidate(input, options.build(), module);
   }
 
   // Assertions that parsing and validation succeeds.
-  private void assertValid(String... lines) throws SyntaxError {
+  private void assertValid(String... lines) throws SyntaxError.Exception {
     StarlarkFile file = validateFile(lines);
     if (!file.ok()) {
-      throw new SyntaxError(file.errors());
+      throw new SyntaxError.Exception(file.errors());
     }
   }
 
   // Asserts that parsing of the program succeeds but validation fails
   // with at least the specified error.
-  private void assertInvalid(String expectedError, String... lines) throws SyntaxError {
-    EventCollector errors = getValidationErrors(lines);
-    assertContainsEvent(errors, expectedError);
+  private void assertInvalid(String expectedError, String... lines) throws SyntaxError.Exception {
+    List<SyntaxError> errors = getValidationErrors(lines);
+    assertContainsError(errors, expectedError);
   }
 
   // Returns the non-empty list of validation errors of the program.
-  private EventCollector getValidationErrors(String... lines) throws SyntaxError {
+  private List<SyntaxError> getValidationErrors(String... lines) throws SyntaxError.Exception {
     StarlarkFile file = validateFile(lines);
     if (file.ok()) {
       throw new AssertionError("validation succeeded unexpectedly");
     }
-    EventCollector errors = new EventCollector();
-    Event.replayEventsOn(errors, file.errors());
-    return errors;
+    return file.errors();
   }
 
   @Test
@@ -181,16 +178,16 @@
 
   @Test
   public void testNoGlobalReassign() throws Exception {
-    EventCollector errors = getValidationErrors("a = 1", "a = 2");
-    assertContainsEvent(errors, ":2:1: cannot reassign global 'a'");
-    assertContainsEvent(errors, ":1:1: 'a' previously declared here");
+    List<SyntaxError> errors = getValidationErrors("a = 1", "a = 2");
+    assertContainsError(errors, ":2:1: cannot reassign global 'a'");
+    assertContainsError(errors, ":1:1: 'a' previously declared here");
   }
 
   @Test
   public void testTwoFunctionsWithTheSameName() throws Exception {
-    EventCollector errors = getValidationErrors("def foo(): pass", "def foo(): pass");
-    assertContainsEvent(errors, ":2:5: cannot reassign global 'foo'");
-    assertContainsEvent(errors, ":1:5: 'foo' previously declared here");
+    List<SyntaxError> errors = getValidationErrors("def foo(): pass", "def foo(): pass");
+    assertContainsError(errors, ":2:5: cannot reassign global 'foo'");
+    assertContainsError(errors, ":1:5: 'foo' previously declared here");
   }
 
   @Test
@@ -308,18 +305,18 @@
 
   @Test
   public void testDollarErrorDoesNotLeak() throws Exception {
-    EventCollector errors =
+    List<SyntaxError> errors =
         getValidationErrors(
             "def GenerateMapNames():", //
             "  a = 2",
             "  b = [3, 4]",
             "  if a not b:",
             "    print(a)");
-    assertContainsEvent(errors, "syntax error at 'b': expected 'in'");
+    assertContainsError(errors, "syntax error at 'b': expected 'in'");
     // Parser uses "$error" symbol for error recovery.
     // It should not be used in error messages.
-    for (Event event : errors) {
-      assertThat(event.getMessage()).doesNotContain("$error$");
+    for (SyntaxError event : errors) {
+      assertThat(event.message()).doesNotContain("$error$");
     }
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java b/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
index f3314b6..5427604 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
@@ -110,7 +110,7 @@
   // and evaluation.
 
   /** Parses an expression. */
-  protected final Expression parseExpression(String... lines) throws SyntaxError {
+  protected final Expression parseExpression(String... lines) throws SyntaxError.Exception {
     return Expression.parse(ParserInput.fromLines(lines));
   }
 
@@ -132,7 +132,8 @@
   }
 
   /** Joins the lines, parses them as a file, and executes it. */
-  public final void exec(String... lines) throws SyntaxError, EvalException, InterruptedException {
+  public final void exec(String... lines)
+      throws SyntaxError.Exception, EvalException, InterruptedException {
     ParserInput input = ParserInput.fromLines(lines);
     EvalUtils.exec(input, FileOptions.DEFAULT, thread.getGlobals(), thread);
   }
@@ -141,7 +142,7 @@
     try {
       exec(input);
       fail("Expected error '" + msg + "' but got no error");
-    } catch (SyntaxError | EvalException | EventCollectionApparatus.FailFastException e) {
+    } catch (SyntaxError.Exception | EvalException | EventCollectionApparatus.FailFastException e) {
       assertThat(e).hasMessageThat().isEqualTo(msg);
     }
   }
@@ -150,7 +151,7 @@
     try {
       exec(input);
       fail("Expected error containing '" + msg + "' but got no error");
-    } catch (SyntaxError | EvalException | EventCollectionApparatus.FailFastException e) {
+    } catch (SyntaxError.Exception | EvalException | EventCollectionApparatus.FailFastException e) {
       assertThat(e).hasMessageThat().contains(msg);
     }
   }
@@ -158,7 +159,7 @@
   public void checkEvalErrorDoesNotContain(String msg, String... input) throws Exception {
     try {
       exec(input);
-    } catch (SyntaxError | EvalException | EventCollectionApparatus.FailFastException e) {
+    } catch (SyntaxError.Exception | EvalException | EventCollectionApparatus.FailFastException e) {
       assertThat(e).hasMessageThat().doesNotContain(msg);
     }
   }