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/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;