Refactor filesystem traversal failures

Only DanglingSymlinkException remains a subtype. It's the only one that
meaningfully appears in catch clauses and method throws lists.

This CL also equips SkyframeAwareAction exceptions with
DetailedExitCodes.

RELNOTES: None.
PiperOrigin-RevId: 315324370
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 679cdf7..a3c458e 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
@@ -48,6 +48,18 @@
   }
 
   public ActionExecutionException(
+      Throwable cause,
+      ActionAnalysisMetadata action,
+      boolean catastrophe,
+      DetailedExitCode detailedExitCode) {
+    super(cause.getMessage(), cause);
+    this.action = action;
+    this.detailedExitCode = detailedExitCode;
+    this.rootCauses = rootCausesFromAction(action, detailedExitCode);
+    this.catastrophe = catastrophe;
+  }
+
+  public ActionExecutionException(
       String message, Throwable cause, ActionAnalysisMetadata action, boolean catastrophe) {
     super(message, cause);
     this.action = action;
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
index c4e2eb0..b25b364 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
@@ -72,6 +72,7 @@
 import com.google.devtools.build.lib.skyframe.ArtifactNestedSetFunction.ArtifactNestedSetEvalException;
 import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ActionPostprocessing;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
@@ -1111,7 +1112,8 @@
       try {
         return skyframeAwareAction.processSkyframeValues(keys, values, env.valuesMissing());
       } catch (SkyframeAwareAction.ExceptionBase e) {
-        throw new ActionExecutionException(e, action, false);
+        throw new ActionExecutionException(
+            e, action, false, DetailedExitCode.of(e.getFailureDetail()));
       }
     }
     return null;
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index e1d21ef..77e0c68 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -2250,6 +2250,7 @@
     deps = [
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
index c611700..8b27387 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
@@ -73,34 +73,39 @@
   private static final FileInfo NON_EXISTENT_FILE_INFO =
       new FileInfo(FileType.NONEXISTENT, NON_EXISTENT_HAS_DIGEST, null, null);
 
-  /** Base class for exceptions that {@link RecursiveFilesystemTraversalFunctionException} wraps. */
-  public abstract static class RecursiveFilesystemTraversalException extends Exception {
-    protected RecursiveFilesystemTraversalException(String message) {
-      super(message);
-    }
-  }
+  /** The exception that {@link RecursiveFilesystemTraversalFunctionException} wraps. */
+  public static class RecursiveFilesystemTraversalException extends Exception {
 
-  /** Thrown when a generated directory's root-relative path conflicts with a package's path. */
-  public static final class GeneratedPathConflictException extends
-      RecursiveFilesystemTraversalException {
-    GeneratedPathConflictException(TraversalRequest traversal) {
-      super(
-          String.format(
-              "Generated directory %s conflicts with package under the same path. "
-                  + "Additional info: %s",
-              traversal.root.asRootedPath().getRootRelativePath().getPathString(),
-              traversal.errorInfo != null ? traversal.errorInfo : traversal.toString()));
-    }
-  }
+    /**
+     * Categories of errors that prevent normal {@link RecursiveFilesystemTraversalFunction}
+     * evaluation.
+     */
+    public enum Type {
+      /**
+       * The traversal encountered a subdirectory with a BUILD file but is not allowed to recurse
+       * into it. See {@code PackageBoundaryMode#REPORT_ERROR}.
+       */
+      CANNOT_CROSS_PACKAGE_BOUNDARY,
 
-  /**
-   * Thrown when the traversal encounters a subdirectory with a BUILD file but is not allowed to
-   * recurse into it. See {@code PackageBoundaryMode#REPORT_ERROR}.
-   */
-  public static final class CannotCrossPackageBoundaryException extends
-      RecursiveFilesystemTraversalException {
-    CannotCrossPackageBoundaryException(String message) {
+      /** A dangling symlink was dereferenced. */
+      DANGLING_SYMLINK,
+
+      /** A file operation failed. */
+      FILE_OPERATION_FAILURE,
+
+      /** A generated directory's root-relative path conflicts with a package's path. */
+      GENERATED_PATH_CONFLICT,
+    }
+
+    private final Type type;
+
+    RecursiveFilesystemTraversalException(String message, Type type) {
       super(message);
+      this.type = type;
+    }
+
+    public Type getType() {
+      return type;
     }
   }
 
@@ -111,29 +116,14 @@
    * and it's not easy to merge the two because of the dependency structure. The other one will
    * probably be removed along with the rest of the legacy Fileset code.
    */
-  public static final class DanglingSymlinkException extends RecursiveFilesystemTraversalException {
-    public final String path;
-    public final String unresolvedLink;
-
-    public DanglingSymlinkException(String path, String unresolvedLink) {
+  static final class DanglingSymlinkException extends RecursiveFilesystemTraversalException {
+    DanglingSymlinkException(String path, String unresolvedLink) {
       super(
           String.format(
-              "Found dangling symlink: %s, unresolved path: \"%s\"", path, unresolvedLink));
+              "Found dangling symlink: %s, unresolved path: \"%s\"", path, unresolvedLink),
+          Type.DANGLING_SYMLINK);
       Preconditions.checkArgument(path != null && !path.isEmpty());
       Preconditions.checkArgument(unresolvedLink != null && !unresolvedLink.isEmpty());
-      this.path = path;
-      this.unresolvedLink = unresolvedLink;
-    }
-
-    public String getPath() {
-      return path;
-    }
-  }
-
-  /** Thrown when we encounter errors from underlying File operations */
-  public static final class FileOperationException extends RecursiveFilesystemTraversalException {
-    public FileOperationException(String message) {
-      super(message);
     }
   }
 
@@ -189,8 +179,7 @@
       if (pkgLookupResult.isConflicting()) {
         // The traversal was requested for an output directory whose root-relative path conflicts
         // with a source package. We can't handle that, bail out.
-        throw new RecursiveFilesystemTraversalFunctionException(
-            new GeneratedPathConflictException(traversal));
+        throw createGeneratedPathConflictException(traversal);
       } else if (pkgLookupResult.isPackage() && !traversal.skipTestingForSubpackage) {
         // The traversal was requested for a directory that defines a package.
         String msg =
@@ -208,7 +197,8 @@
           case REPORT_ERROR:
             // We cannot traverse the subpackage and should complain loudly (display an error).
             throw new RecursiveFilesystemTraversalFunctionException(
-                new CannotCrossPackageBoundaryException(msg));
+                new RecursiveFilesystemTraversalException(
+                    msg, RecursiveFilesystemTraversalException.Type.CANNOT_CROSS_PACKAGE_BOUNDARY));
           default:
             throw new IllegalStateException(traversal.toString());
         }
@@ -221,13 +211,28 @@
           rootInfo,
           traverseChildren(env, dependentKeys, /*inline=*/ traversal.isRootGenerated));
     } catch (IOException e) {
+      String message = "Error while traversing fileset: " + e.getMessage();
       throw new RecursiveFilesystemTraversalFunctionException(
-          new FileOperationException("Error while traversing fileset: " + e.getMessage()));
+          new RecursiveFilesystemTraversalException(
+              message, RecursiveFilesystemTraversalException.Type.FILE_OPERATION_FAILURE));
     } catch (MissingDepException e) {
       return null;
     }
   }
 
+  private static RecursiveFilesystemTraversalFunctionException createGeneratedPathConflictException(
+      TraversalRequest traversal) {
+    String message =
+        String.format(
+            "Generated directory %s conflicts with package under the same path. "
+                + "Additional info: %s",
+            traversal.root.asRootedPath().getRootRelativePath().getPathString(),
+            traversal.errorInfo != null ? traversal.errorInfo : traversal.toString());
+    return new RecursiveFilesystemTraversalFunctionException(
+        new RecursiveFilesystemTraversalException(
+            message, RecursiveFilesystemTraversalException.Type.GENERATED_PATH_CONFLICT));
+  }
+
   @Override
   public String extractTag(SkyKey skyKey) {
     return null;
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeAwareAction.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeAwareAction.java
index 572abb3..13b88b4 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeAwareAction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeAwareAction.java
@@ -15,6 +15,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.graph.ImmutableGraph;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.ValueOrException;
@@ -46,12 +47,15 @@
 
   /** Wrapper and/or base class for exceptions raised in {@link #processSkyframeValues}. */
   class ExceptionBase extends Exception {
-    public ExceptionBase(String message) {
-      super(message);
+    private final FailureDetail failureDetail;
+
+    public ExceptionBase(Throwable cause, FailureDetail failureDetail) {
+      super(cause.getMessage(), cause);
+      this.failureDetail = failureDetail;
     }
 
-    public ExceptionBase(Throwable cause) {
-      super(cause.getMessage(), cause);
+    public FailureDetail getFailureDetail() {
+      return failureDetail;
     }
   }