Encode build configuration failures with FailureDetails

Eliminates the second-to-last remaining use of DetailedExitCode without
FailureDetails.

Cleans up an incorrect use of BUILD_CONFIGURATION_UNKNOWN, a default
protobuf enum value.

RELNOTES: None.
PiperOrigin-RevId: 332872475
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
index 9534265..b0031a2 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
@@ -1697,7 +1697,10 @@
     name = "config/invalid_configuration_exception",
     srcs = ["config/InvalidConfigurationException.java"],
     deps = [
+        "//src/main/java/com/google/devtools/build/lib/skyframe:detailed_exceptions",
+        "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
         "//src/main/protobuf:failure_details_java_proto",
+        "//third_party:guava",
         "//third_party:jsr305",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationCollection.java
index e602252..7d1fbe2 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationCollection.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationCollection.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
 import java.util.HashMap;
 
 /**
@@ -45,7 +46,7 @@
       BuildConfiguration old = cacheKeyConflictDetector.put(config.checksum(), config);
       if (old != null) {
         throw new InvalidConfigurationException(
-            "Conflicting configurations: " + config + " & " + old);
+            "Conflicting configurations: " + config + " & " + old, Code.CONFLICTING_CONFIGURATIONS);
       }
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java b/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
index 7f2a7f4..25c08c7 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
@@ -13,39 +13,64 @@
 // limitations under the License.
 package com.google.devtools.build.lib.analysis.config;
 
+import com.google.common.base.Strings;
+import com.google.devtools.build.lib.server.FailureDetails;
 import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.skyframe.DetailedException;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import javax.annotation.Nullable;
 
 /**
  * Thrown if the configuration options lead to an invalid configuration, or if any of the
  * configuration labels cannot be loaded.
  */
-public class InvalidConfigurationException extends Exception {
+public class InvalidConfigurationException extends Exception implements DetailedException {
 
-  @Nullable private final Code detailedCode;
+  @Nullable private final DetailedExitCode detailedExitCode;
 
   public InvalidConfigurationException(String message) {
     super(message);
-    this.detailedCode = null;
+    this.detailedExitCode = null;
+  }
+
+  public InvalidConfigurationException(String message, Code code) {
+    super(message);
+    this.detailedExitCode = createDetailedExitCode(message, code);
   }
 
   public InvalidConfigurationException(String message, Throwable cause) {
     super(message, cause);
-    this.detailedCode = null;
+    this.detailedExitCode = null;
   }
 
   public InvalidConfigurationException(Throwable cause) {
     super(cause.getMessage(), cause);
-    this.detailedCode = null;
+    this.detailedExitCode = null;
   }
 
-  public InvalidConfigurationException(Code detailedCode, Exception cause) {
+  public InvalidConfigurationException(Code code, Throwable cause) {
     super(cause.getMessage(), cause);
-    this.detailedCode = detailedCode;
+    this.detailedExitCode = createDetailedExitCode(cause.getMessage(), code);
   }
 
-  @Nullable
-  public Code getDetailedCode() {
-    return detailedCode;
+  public InvalidConfigurationException(DetailedExitCode detailedExitCode, Throwable cause) {
+    super(cause.getMessage(), cause);
+    this.detailedExitCode = detailedExitCode;
+  }
+
+  @Override
+  public DetailedExitCode getDetailedExitCode() {
+    return detailedExitCode != null
+        ? detailedExitCode
+        : createDetailedExitCode(getMessage(), Code.INVALID_CONFIGURATION);
+  }
+
+  private static DetailedExitCode createDetailedExitCode(@Nullable String message, Code code) {
+    return DetailedExitCode.of(
+        FailureDetail.newBuilder()
+            .setMessage(Strings.nullToEmpty(message))
+            .setBuildConfiguration(FailureDetails.BuildConfiguration.newBuilder().setCode(code))
+            .build());
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
index fb00fbb..61e2a20 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
@@ -42,6 +42,7 @@
 import com.google.devtools.build.lib.profiler.SilentCloseable;
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.skyframe.BuildConfigurationValue;
 import com.google.devtools.build.lib.skyframe.BuildInfoCollectionFunction;
@@ -99,7 +100,7 @@
           buildOptions.get(CoreOptions.class).instrumentationFilter =
               new RegexFilter.RegexFilterConverter().convert(instrumentationFilter);
         } catch (OptionsParsingException e) {
-          throw new InvalidConfigurationException(e);
+          throw new InvalidConfigurationException(Code.HEURISTIC_INSTRUMENTATION_FILTER_INVALID, e);
         }
       }
     }
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
index 5eeed4b..079326f 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
@@ -23,7 +23,6 @@
 import com.google.devtools.build.lib.analysis.AnalysisOptions;
 import com.google.devtools.build.lib.analysis.OutputGroupInfo;
 import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
-import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
 import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
 import com.google.devtools.build.lib.exec.ExecutionOptions;
 import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
@@ -269,7 +268,7 @@
    *
    * @return list of warnings
    */
-  public List<String> validateOptions() throws InvalidConfigurationException {
+  public List<String> validateOptions() {
     List<String> warnings = new ArrayList<>();
 
     int localTestJobs = getExecutionOptions().localTestJobs;
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 7eb2896..53feadb 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
@@ -52,6 +52,7 @@
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.server.FailureDetails.ActionQuery;
+import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.Interrupted.Code;
 import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor;
@@ -153,7 +154,8 @@
         if (!"build".equals(request.getCommandName()) && !"test".equals(request.getCommandName())) {
           throw new InvalidConfigurationException(
               "The experimental setting to select multiple CPUs is only supported for 'build' and "
-              + "'test' right now!");
+                  + "'test' right now!",
+              BuildConfiguration.Code.MULTI_CPU_PREREQ_UNMET);
         }
       }
 
@@ -443,7 +445,7 @@
       detailedExitCode = DetailedExitCode.success();
       reportExceptionError(e);
     } catch (InvalidConfigurationException e) {
-      detailedExitCode = DetailedExitCode.justExitCode(ExitCode.COMMAND_LINE_ERROR);
+      detailedExitCode = e.getDetailedExitCode();
       reportExceptionError(e);
       // TODO(gregce): With "global configurations" we cannot tie a configuration creation failure
       // to a single target and have to halt the entire build. Once configurations are genuinely
@@ -570,7 +572,7 @@
    * settings that conflict.
    */
   @VisibleForTesting
-  public void validateOptions(BuildRequest request) throws InvalidConfigurationException {
+  public void validateOptions(BuildRequest request) {
     for (String issue : request.validateOptions()) {
       getReporter().handle(Event.warn(issue));
     }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
index 7ee7ea3..a3a8575 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
@@ -13,7 +13,6 @@
 // limitations under the License.
 package com.google.devtools.build.lib.runtime.commands;
 
-import com.google.common.base.Strings;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
@@ -61,7 +60,6 @@
 import com.google.devtools.build.lib.runtime.commands.info.UsedHeapSizeInfoItem;
 import com.google.devtools.build.lib.runtime.commands.info.WorkspaceInfoItem;
 import com.google.devtools.build.lib.server.FailureDetails;
-import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.Interrupted;
 import com.google.devtools.build.lib.util.AbruptExitException;
@@ -170,18 +168,7 @@
                         /*keepGoing=*/ true);
               } catch (InvalidConfigurationException e) {
                 env.getReporter().handle(Event.error(e.getMessage()));
-                throw new AbruptExitRuntimeException(
-                    DetailedExitCode.of(
-                        ExitCode.COMMAND_LINE_ERROR,
-                        FailureDetail.newBuilder()
-                            .setMessage(Strings.nullToEmpty(e.getMessage()))
-                            .setBuildConfiguration(
-                                FailureDetails.BuildConfiguration.newBuilder()
-                                    .setCode(
-                                        e.getDetailedCode() == null
-                                            ? Code.BUILD_CONFIGURATION_UNKNOWN
-                                            : e.getDetailedCode()))
-                            .build()));
+                throw new AbruptExitRuntimeException(e.getDetailedExitCode());
               } catch (AbruptExitException e) {
                 throw new AbruptExitRuntimeException(e.getDetailedExitCode());
               } catch (InterruptedException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareAnalysisPhaseFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareAnalysisPhaseFunction.java
index c95caf7..0d6f17c 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareAnalysisPhaseFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareAnalysisPhaseFunction.java
@@ -41,6 +41,7 @@
 import com.google.devtools.build.lib.events.ErrorSensingEventHandler;
 import com.google.devtools.build.lib.packages.NoSuchThingException;
 import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
 import com.google.devtools.build.lib.skyframe.PrepareAnalysisPhaseValue.PrepareAnalysisPhaseKey;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyFunction;
@@ -142,7 +143,8 @@
         .forEach(config -> config.reportInvalidOptions(nosyEventHandler));
     if (nosyEventHandler.hasErrors()) {
       throw new PrepareAnalysisPhaseFunctionException(
-          new InvalidConfigurationException("Build options are invalid"));
+          new InvalidConfigurationException(
+              "Build options are invalid", Code.INVALID_BUILD_OPTIONS));
     }
 
     // We get the list of labels from the TargetPatternPhaseValue, so we are reasonably certain that
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index c6863d3..ed87c6d 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -1516,7 +1516,8 @@
         ErrorSensingEventHandler.withoutPropertyValueTracking(eventHandler);
     topLevelTargetConfigs.forEach(config -> config.reportInvalidOptions(nosyEventHandler));
     if (nosyEventHandler.hasErrors()) {
-      throw new InvalidConfigurationException("Build options are invalid");
+      throw new InvalidConfigurationException(
+          "Build options are invalid", Code.INVALID_BUILD_OPTIONS);
     }
     return new BuildConfigurationCollection(topLevelTargetConfigs, hostConfig);
   }
@@ -1952,12 +1953,12 @@
       Throwable e = error.getException();
       // Wrap loading failed exceptions
       if (e instanceof NoSuchThingException) {
-        e = new InvalidConfigurationException(e);
+        e = new InvalidConfigurationException(((NoSuchThingException) e).getDetailedExitCode(), e);
       } else if (e == null && !error.getCycleInfo().isEmpty()) {
         getCyclesReporter().reportCycles(error.getCycleInfo(), firstError.getKey(), eventHandler);
         e =
             new InvalidConfigurationException(
-                "cannot load build configuration because of this cycle");
+                "cannot load build configuration because of this cycle", Code.CYCLE);
       }
       if (e != null) {
         Throwables.throwIfInstanceOf(e, InvalidConfigurationException.class);
@@ -2135,7 +2136,7 @@
           depFragments,
           BuildOptions.diffForReconstruction(defaultBuildOptions, toOption));
     } catch (OptionsParsingException e) {
-      throw new InvalidConfigurationException(e);
+      throw new InvalidConfigurationException(Code.INVALID_BUILD_OPTIONS, e);
     }
   }
 
@@ -2962,9 +2963,9 @@
         getCyclesReporter().reportCycles(errorInfo.getCycleInfo(), key, eventHandler);
         e =
             new InvalidConfigurationException(
-                "cannot load build configuration because of this cycle");
+                "cannot load build configuration because of this cycle", Code.CYCLE);
       } else if (e instanceof NoSuchThingException) {
-        e = new InvalidConfigurationException(e);
+        e = new InvalidConfigurationException(((NoSuchThingException) e).getDetailedExitCode(), e);
       }
       if (e != null) {
         Throwables.throwIfInstanceOf(e, InvalidConfigurationException.class);