Remove null as an encoding of BUILD_FAILURE (exit code 1)

Future work will deliver details for failures attributable to the user.

RELNOTES: None.
PiperOrigin-RevId: 301809499
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
index c9becf1..a87b933 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
@@ -13,6 +13,8 @@
 // limitations under the License.
 package com.google.devtools.build.lib.actions;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.devtools.build.lib.causes.ActionFailed;
 import com.google.devtools.build.lib.causes.Cause;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
@@ -23,7 +25,6 @@
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.ExitCode;
-import javax.annotation.Nullable;
 
 /**
  * This exception gets thrown if {@link Action#execute(ActionExecutionContext)} is unsuccessful.
@@ -35,14 +36,14 @@
   private final Action action;
   private final NestedSet<Cause> rootCauses;
   private final boolean catastrophe;
-  @Nullable private final DetailedExitCode detailedExitCode;
+  private final DetailedExitCode detailedExitCode;
 
   public ActionExecutionException(Throwable cause, Action action, boolean catastrophe) {
     super(cause.getMessage(), cause);
     this.action = action;
     this.rootCauses = rootCausesFromAction(action);
     this.catastrophe = catastrophe;
-    this.detailedExitCode = null;
+    this.detailedExitCode = DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE);
   }
 
   public ActionExecutionException(String message,
@@ -51,7 +52,7 @@
     this.action = action;
     this.rootCauses = rootCausesFromAction(action);
     this.catastrophe = catastrophe;
-    this.detailedExitCode = null;
+    this.detailedExitCode = DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE);
   }
 
   public ActionExecutionException(
@@ -64,7 +65,7 @@
     this.action = action;
     this.rootCauses = rootCausesFromAction(action);
     this.catastrophe = catastrophe;
-    this.detailedExitCode = detailedExitCode;
+    this.detailedExitCode = checkNotNull(detailedExitCode);
   }
 
   public ActionExecutionException(String message, Action action, boolean catastrophe) {
@@ -72,7 +73,7 @@
     this.action = action;
     this.rootCauses = rootCausesFromAction(action);
     this.catastrophe = catastrophe;
-    this.detailedExitCode = null;
+    this.detailedExitCode = DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE);
   }
 
   public ActionExecutionException(String message, Action action, boolean catastrophe,
@@ -90,20 +91,7 @@
     this.action = action;
     this.rootCauses = rootCauses;
     this.catastrophe = catastrophe;
-    this.detailedExitCode = null;
-  }
-
-  public ActionExecutionException(
-      String message,
-      Throwable cause,
-      Action action,
-      NestedSet<Cause> rootCauses,
-      boolean catastrophe) {
-    super(message, cause);
-    this.action = action;
-    this.rootCauses = rootCauses;
-    this.catastrophe = catastrophe;
-    this.detailedExitCode = null;
+    this.detailedExitCode = DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE);
   }
 
   public ActionExecutionException(
@@ -117,7 +105,7 @@
     this.action = action;
     this.rootCauses = rootCauses;
     this.catastrophe = catastrophe;
-    this.detailedExitCode = detailedExitCode;
+    this.detailedExitCode = checkNotNull(detailedExitCode);
   }
 
   private static NestedSet<Cause> rootCausesFromAction(Action action) {
@@ -163,28 +151,15 @@
   /**
    * Returns the exit code to return from this Bazel invocation because of this action execution
    * failure.
-   *
-   * <p>Returns {@code null} if the exception is not intended to cause the invocation to fail, or if
-   * the failure is attributable to the user. (In the latter case, ExitCode.BUILD_FAILURE with
-   * numeric value 1 will be returned.)
    */
-  @Nullable
   public ExitCode getExitCode() {
-    return detailedExitCode == null ? null : detailedExitCode.getExitCode();
+    return detailedExitCode.getExitCode();
   }
 
   /**
    * Returns the pair of {@link ExitCode} and optional {@link FailureDetail} to return from this
    * Bazel invocation because of this action execution failure.
-   *
-   * <p>Returns {@code null} if the exception is not intended to cause the invocation to fail, or if
-   * the failure is attributable to the user. (In the latter case, ExitCode.BUILD_FAILURE with
-   * numeric value 1 will be returned for an exit code, and no FailureDetail will be returned.)
    */
-  // TODO(b/138456686): for detailed user failures, this must be able to return non-null for
-  //  user-attributable failures. The meaning of "null" must be changed in code paths handling this
-  //  returned value.
-  @Nullable
   public DetailedExitCode getDetailedExitCode() {
     return detailedExitCode;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java b/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
index d0aa35b..4fa494d 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.actions;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.devtools.build.lib.causes.Cause;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
@@ -40,31 +42,43 @@
 @ThreadSafe
 public class BuildFailedException extends Exception {
   private final boolean catastrophic;
-  private final Action action;
+  @Nullable private final Action action;
   private final NestedSet<Cause> rootCauses;
   private final boolean errorAlreadyShown;
-  @Nullable private final DetailedExitCode detailedExitCode;
+  private final DetailedExitCode detailedExitCode;
 
   public BuildFailedException() {
     this(null);
   }
 
   public BuildFailedException(String message) {
-    this(message, false, null, NestedSetBuilder.emptySet(Order.STABLE_ORDER), false, null);
+    this(
+        message,
+        /*catastrophic=*/ false,
+        /*action=*/ null,
+        NestedSetBuilder.emptySet(Order.STABLE_ORDER),
+        /*errorAlreadyShown=*/ false,
+        DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE));
   }
 
   public BuildFailedException(String message, DetailedExitCode detailedExitCode) {
     this(
         message,
-        false,
-        null,
+        /*catastrophic=*/ false,
+        /*action=*/ null,
         NestedSetBuilder.emptySet(Order.STABLE_ORDER),
-        false,
+        /*errorAlreadyShown=*/ false,
         detailedExitCode);
   }
 
   public BuildFailedException(String message, boolean catastrophic) {
-    this(message, catastrophic, null, NestedSetBuilder.emptySet(Order.STABLE_ORDER), false, null);
+    this(
+        message,
+        catastrophic,
+        /*action=*/ null,
+        NestedSetBuilder.emptySet(Order.STABLE_ORDER),
+        /*errorAlreadyShown=*/ false,
+        DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE));
   }
 
   public BuildFailedException(
@@ -73,19 +87,20 @@
       Action action,
       NestedSet<Cause> rootCauses,
       boolean errorAlreadyShown,
-      @Nullable DetailedExitCode detailedExitCode) {
+      DetailedExitCode detailedExitCode) {
     super(message);
     this.catastrophic = catastrophic;
     this.rootCauses = rootCauses;
     this.action = action;
     this.errorAlreadyShown = errorAlreadyShown;
-    this.detailedExitCode = detailedExitCode;
+    this.detailedExitCode = checkNotNull(detailedExitCode);
   }
 
   public boolean isCatastrophic() {
     return catastrophic;
   }
 
+  @Nullable
   public Action getAction() {
     return action;
   }
@@ -101,15 +116,7 @@
   /**
    * Returns the pair of {@link ExitCode} and optional {@link FailureDetail} to return from this
    * Bazel invocation.
-   *
-   * <p>Returns {@code null} if the failure is attributable to the user. (In this case,
-   * ExitCode.BUILD_FAILURE with numeric value 1 will be returned for an exit code, and no
-   * FailureDetail will be returned.)
    */
-  // TODO(b/138456686): for detailed user failures, this must be able to return non-null for
-  //  user-attributable failures. The meaning of "null" must be changed in code paths handling this
-  //  returned value.
-  @Nullable
   public DetailedExitCode getDetailedExitCode() {
     return detailedExitCode;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
index 45f7607..5f4258b 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -298,10 +298,7 @@
       if (e.isCatastrophic()) {
         result.setCatastrophe();
       }
-      detailedExitCode =
-          e.getDetailedExitCode() != null
-              ? e.getDetailedExitCode()
-              : DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE);
+      detailedExitCode = e.getDetailedExitCode();
     } catch (InterruptedException e) {
       // We may have been interrupted by an error, or the user's interruption may have raced with
       // an error, so check to see if we should report that error code instead.
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java b/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
index 0cb1821..62f996ac 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
@@ -14,7 +14,6 @@
 package com.google.devtools.build.lib.buildtool;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Range;
 import com.google.common.collect.Sets;
@@ -48,6 +47,7 @@
 import com.google.devtools.build.lib.skyframe.TopDownActionCache;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.DetailedExitCode;
+import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.LoggingUtil;
 import com.google.devtools.build.lib.vfs.ModifiedFileSet;
 import com.google.devtools.build.skyframe.CycleInfo;
@@ -168,7 +168,7 @@
               executionProgressReceiver,
               topLevelArtifactContext);
       // progressReceiver is finished, so unsynchronized access to builtTargets is now safe.
-      Optional<DetailedExitCode> detailedExitCode =
+      DetailedExitCode detailedExitCode =
           processResult(
               reporter,
               result,
@@ -176,7 +176,7 @@
               skyframeExecutor);
 
       if (detailedExitCode != null) {
-        detailedExitCodes.add(detailedExitCode.orNull());
+        detailedExitCodes.add(detailedExitCode);
       }
 
       // Run exclusive tests: either tagged as "exclusive" or is run in an invocation with
@@ -209,7 +209,7 @@
             result);
 
         if (detailedExitCode != null) {
-          detailedExitCodes.add(detailedExitCode.orNull());
+          detailedExitCodes.add(detailedExitCode);
         }
       }
     } finally {
@@ -218,34 +218,36 @@
       statusReporter.unregisterFromEventBus();
     }
 
-    if (!detailedExitCodes.isEmpty()) {
-      if (options.getOptions(KeepGoingOption.class).keepGoing) {
-        // Use the exit code with the highest priority.
-        throw new BuildFailedException(
-            null, Collections.max(detailedExitCodes, DetailedExitCodeComparator.INSTANCE));
-      } else {
-        throw new BuildFailedException();
-      }
+    if (detailedExitCodes.isEmpty()) {
+      return;
+    }
+
+    if (options.getOptions(KeepGoingOption.class).keepGoing) {
+      // Use the exit code with the highest priority.
+      throw new BuildFailedException(
+          null, Collections.max(detailedExitCodes, DetailedExitCodeComparator.INSTANCE));
+    } else {
+      throw new BuildFailedException();
     }
   }
 
   /**
    * Process an {@link EvaluationResult}, taking into account the keepGoing setting.
    *
-   * <p>Returns a nullable optional {@link DetailedExitCode} value, as follows:
+   * <p>Returns a nullable {@link DetailedExitCode} value, as follows:
    *
    * <ol>
    *   <li>{@code null}, if {@code result} had no errors
-   *   <li>{@code Optional.absent()}, if result had errors but none of the errors specified a {@link
-   *       DetailedExitCode}
-   *   <li>{@code Optional.of(e)} if result had errors and one of them specified a {@link
-   *       DetailedExitCode} value {@code e}.
+   *   <li>{@code e} if result had errors and one of them specified a {@link DetailedExitCode} value
+   *       {@code e}
+   *   <li>{@code DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE)} if result had errors but
+   *       none specified a {@link DetailedExitCode} value
    * </ol>
    *
-   * <p>Throws on catastrophic failures.
+   * <p>Throws on catastrophic failures and, if !keepGoing, on any failure.
    */
   @Nullable
-  private static Optional<DetailedExitCode> processResult(
+  private static DetailedExitCode processResult(
       ExtendedEventHandler eventHandler,
       EvaluationResult<?> result,
       boolean keepGoing,
@@ -265,7 +267,7 @@
         // in the following order:
         //   1. First infrastructure error with non-null exit code
         //   2. First non-infrastructure error with non-null exit code
-        //   3. Null (later default to 1)
+        //   3. If the build fails but no interpretable error is specified, BUILD_FAILURE.
         DetailedExitCode detailedExitCode = null;
         for (Map.Entry<SkyKey, ErrorInfo> error : result.errorMap().entrySet()) {
           Throwable cause = error.getValue().getException();
@@ -280,7 +282,9 @@
           }
         }
 
-        return Optional.fromNullable(detailedExitCode);
+        return detailedExitCode != null
+            ? detailedExitCode
+            : DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE);
       }
       ErrorInfo errorInfo = Preconditions.checkNotNull(result.getError(), result);
       Exception exception = errorInfo.getException();
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java
index 15620d6..428ebef 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java
@@ -73,17 +73,13 @@
         message, this, action, isCatastrophic(), getDetailedExitCode());
   }
 
-  /** Return exit code depending on the spawn result. */
+  /** Return detailed exit code depending on the spawn result. */
   private DetailedExitCode getDetailedExitCode() {
-    if (result.status().isConsideredUserError()) {
-      if (result.failureDetail() != null) {
-        return DetailedExitCode.of(ExitCode.BUILD_FAILURE, result.failureDetail());
-      }
-      return null;
+    ExitCode exitCode =
+        result.status().isConsideredUserError() ? ExitCode.BUILD_FAILURE : ExitCode.REMOTE_ERROR;
+    if (result.failureDetail() == null) {
+      return DetailedExitCode.justExitCode(exitCode);
     }
-    if (result.failureDetail() != null) {
-      return DetailedExitCode.of(ExitCode.REMOTE_ERROR, result.failureDetail());
-    }
-    return DetailedExitCode.justExitCode(ExitCode.REMOTE_ERROR);
+    return DetailedExitCode.of(exitCode, result.failureDetail());
   }
 }