Encapsulate the list of ignored subdirectories in an object.

This is the mechanical part of the change. It's not, strictly speaking, necessary, but given that the that set of path fragments will not be interpreted as a set of path fragments soon, I think it's better to be explicit.

An ancillary benefit is that now one can search for the new object to figure out which parts of the code handle the concept of "ignored subdirectories".

Progress towards #7093 .

RELNOTES: None.
PiperOrigin-RevId: 686046437
Change-Id: I4191297c9b1e5a6599d4c5f8eb2c80fae14ac220
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/BUILD b/src/main/java/com/google/devtools/build/lib/cmdline/BUILD
index c0b55a9..00d4764 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/BUILD
@@ -17,6 +17,7 @@
     srcs = [
         "BazelCompileContext.java",
         "BazelModuleContext.java",
+        "IgnoredSubdirectories.java",
         "Label.java",
         "LabelConstants.java",
         "LabelParser.java",
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/IgnoredSubdirectories.java b/src/main/java/com/google/devtools/build/lib/cmdline/IgnoredSubdirectories.java
new file mode 100644
index 0000000..d0458af
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/IgnoredSubdirectories.java
@@ -0,0 +1,94 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.cmdline;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * A set of subdirectories to ignore during target pattern matching or globbing.
+ *
+ * <p>This is currently just a prefix, but will eventually support glob-style wildcards.
+ */
+public final class IgnoredSubdirectories {
+  private final ImmutableSet<PathFragment> prefixes;
+
+  public IgnoredSubdirectories(ImmutableSet<PathFragment> prefixes) {
+    this.prefixes = prefixes;
+  }
+
+  public ImmutableSet<PathFragment> prefixes() {
+    return prefixes;
+  }
+
+  public static final IgnoredSubdirectories EMPTY = new IgnoredSubdirectories(ImmutableSet.of());
+
+  /**
+   * Checks whether every path in this instance can conceivably match something under {@code
+   * directory}.
+   */
+  public boolean allPathsAreUnder(PathFragment directory) {
+    for (PathFragment prefix : prefixes) {
+      if (!prefix.startsWith(directory)) {
+        return false;
+      }
+
+      if (prefix.equals(directory)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  /** Returns the entry that matches a given directory or {@code null} if none. */
+  @Nullable
+  public PathFragment matchingEntry(PathFragment directory) {
+    for (PathFragment prefix : prefixes) {
+      if (directory.startsWith(prefix)) {
+        return prefix;
+      }
+    }
+
+    return null;
+  }
+
+  /** Filters out entries that cannot match anything under {@code directory}. */
+  public IgnoredSubdirectories filterForDirectory(PathFragment directory) {
+    ImmutableSet<PathFragment> filteredPrefixes =
+        prefixes.stream().filter(p -> p.startsWith(directory)).collect(toImmutableSet());
+
+    return new IgnoredSubdirectories(filteredPrefixes);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof IgnoredSubdirectories)) {
+      return false;
+    }
+
+    IgnoredSubdirectories that = (IgnoredSubdirectories) other;
+    return Objects.equals(this.prefixes, that.prefixes);
+  }
+
+  @Override
+  public int hashCode() {
+    return prefixes.hashCode();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
index d9886b3..b836431 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
@@ -601,7 +601,7 @@
           getOriginalPattern(),
           directory.getPackageFragment().getPathString(),
           rulesOnly,
-          ignoredIntersection.ignoredPathFragments(),
+          new IgnoredSubdirectories(ignoredIntersection.ignoredPathFragments()),
           excludedSubdirectories,
           callback,
           exceptionClass);
@@ -635,7 +635,7 @@
           getOriginalPattern(),
           directory.getPackageFragment().getPathString(),
           rulesOnly,
-          ignoredIntersection.ignoredPathFragments(),
+          new IgnoredSubdirectories(ignoredIntersection.ignoredPathFragments()),
           excludedSubdirectories,
           callback,
           exceptionClass,
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPatternResolver.java b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPatternResolver.java
index 03fdb98..3be3928 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPatternResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPatternResolver.java
@@ -97,7 +97,7 @@
           String originalPattern,
           String directory,
           boolean rulesOnly,
-          ImmutableSet<PathFragment> forbiddenSubdirectories,
+          IgnoredSubdirectories forbiddenSubdirectories,
           ImmutableSet<PathFragment> excludedSubdirectories,
           BatchCallback<T, E> callback,
           Class<E> exceptionClass)
@@ -115,7 +115,7 @@
           String originalPattern,
           String directory,
           boolean rulesOnly,
-          ImmutableSet<PathFragment> forbiddenSubdirectories,
+          IgnoredSubdirectories forbiddenSubdirectories,
           ImmutableSet<PathFragment> excludedSubdirectories,
           BatchCallback<T, E> callback,
           Class<E> exceptionClass,
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/RecursivePackageProvider.java b/src/main/java/com/google/devtools/build/lib/pkgcache/RecursivePackageProvider.java
index a513f39..fd1f75b 100644
--- a/src/main/java/com/google/devtools/build/lib/pkgcache/RecursivePackageProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/RecursivePackageProvider.java
@@ -15,6 +15,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.cmdline.BatchCallback.SafeBatchCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -57,9 +58,11 @@
       ExtendedEventHandler eventHandler,
       RepositoryName repository,
       PathFragment directory,
-      ImmutableSet<PathFragment> ignoredSubdirectories,
+      IgnoredSubdirectories ignoredSubdirectories,
       ImmutableSet<PathFragment> excludedSubdirectories)
-      throws InterruptedException, QueryException, NoSuchPackageException,
+      throws InterruptedException,
+          QueryException,
+          NoSuchPackageException,
           ProcessPackageDirectoryException;
 
   /**
@@ -124,7 +127,7 @@
         ExtendedEventHandler eventHandler,
         RepositoryName repository,
         PathFragment directory,
-        ImmutableSet<PathFragment> ignoredSubdirectories,
+        IgnoredSubdirectories ignoredSubdirectories,
         ImmutableSet<PathFragment> excludedSubdirectories) {
       throw new UnsupportedOperationException();
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryFunction.java
index f73cb01..c2867d6 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryFunction.java
@@ -14,9 +14,9 @@
 package com.google.devtools.build.lib.skyframe;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.skyframe.ProcessPackageDirectory.ProcessPackageDirectorySkyFunctionException;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -65,7 +65,7 @@
     protected SkyKey getSkyKeyForSubdirectory(
         RepositoryName repository,
         RootedPath subdirectory,
-        ImmutableSet<PathFragment> excludedSubdirectoriesBeneathSubdirectory) {
+        IgnoredSubdirectories excludedSubdirectoriesBeneathSubdirectory) {
       return CollectPackagesUnderDirectoryValue.key(
           repository, subdirectory, excludedSubdirectoriesBeneathSubdirectory);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryValue.java
index 8f699c7..38ecc50 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryValue.java
@@ -16,13 +16,12 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
-import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.RootedPath;
 import com.google.devtools.build.skyframe.SkyFunctionName;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -228,7 +227,7 @@
   /** Create a collect packages under directory request. */
   @ThreadSafe
   public static SkyKey key(
-      RepositoryName repository, RootedPath rootedPath, ImmutableSet<PathFragment> excludedPaths) {
+      RepositoryName repository, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
     return Key.create(repository, rootedPath, excludedPaths);
   }
 
@@ -238,17 +237,13 @@
     private static final SkyKeyInterner<Key> interner = SkyKey.newInterner();
 
     private Key(
-        RepositoryName repositoryName,
-        RootedPath rootedPath,
-        ImmutableSet<PathFragment> excludedPaths) {
+        RepositoryName repositoryName, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
       super(repositoryName, rootedPath, excludedPaths);
     }
 
     @VisibleForSerialization
     static Key create(
-        RepositoryName repositoryName,
-        RootedPath rootedPath,
-        ImmutableSet<PathFragment> excludedPaths) {
+        RepositoryName repositoryName, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
       return interner.intern(new Key(repositoryName, rootedPath, excludedPaths));
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/EnvironmentBackedRecursivePackageProvider.java b/src/main/java/com/google/devtools/build/lib/skyframe/EnvironmentBackedRecursivePackageProvider.java
index bd08709..bac0580 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/EnvironmentBackedRecursivePackageProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/EnvironmentBackedRecursivePackageProvider.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.cmdline.BatchCallback.SafeBatchCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.Event;
@@ -166,7 +167,7 @@
       ExtendedEventHandler eventHandler,
       RepositoryName repository,
       PathFragment directory,
-      ImmutableSet<PathFragment> ignoredSubdirectories,
+      IgnoredSubdirectories ignoredSubdirectories,
       ImmutableSet<PathFragment> excludedSubdirectories)
       throws InterruptedException, NoSuchPackageException, ProcessPackageDirectoryException {
     PathPackageLocator packageLocator = PrecomputedValue.PATH_PACKAGE_LOCATOR.get(env);
@@ -194,9 +195,8 @@
       roots.add(Root.fromPath(repositoryValue.getPath()));
     }
 
-    ImmutableSet<PathFragment> filteredIgnoredSubdirectories =
-        ImmutableSet.copyOf(
-            Iterables.filter(ignoredSubdirectories, path -> path.startsWith(directory)));
+    IgnoredSubdirectories filteredIgnoredSubdirectories =
+        ignoredSubdirectories.filterForDirectory(directory);
 
     Iterable<RecursivePkgValue.Key> recursivePackageKeys =
         Iterables.transform(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GraphBackedRecursivePackageProvider.java b/src/main/java/com/google/devtools/build/lib/skyframe/GraphBackedRecursivePackageProvider.java
index 45e4a0c..119b8ee 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/GraphBackedRecursivePackageProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GraphBackedRecursivePackageProvider.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Sets.SetView;
 import com.google.common.flogger.GoogleLogger;
 import com.google.devtools.build.lib.cmdline.BatchCallback.SafeBatchCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
@@ -244,7 +245,7 @@
       ExtendedEventHandler eventHandler,
       RepositoryName repository,
       PathFragment directory,
-      ImmutableSet<PathFragment> ignoredSubdirectories,
+      IgnoredSubdirectories ignoredSubdirectories,
       ImmutableSet<PathFragment> excludedSubdirectories)
       throws InterruptedException, QueryException {
     rootPackageExtractor.streamPackagesFromRoots(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternFunction.java
index b69cab2..6058cfa 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternFunction.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.cmdline.BatchCallback;
 import com.google.devtools.build.lib.cmdline.BatchCallback.NullCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.QueryExceptionMarkerInterface;
@@ -302,7 +303,7 @@
         String originalPattern,
         String directory,
         boolean rulesOnly,
-        ImmutableSet<PathFragment> repositoryIgnoredSubdirectories,
+        IgnoredSubdirectories repositoryIgnoredSubdirectories,
         ImmutableSet<PathFragment> excludedSubdirectories,
         BatchCallback<Void, E> callback,
         Class<E> exceptionClass)
@@ -342,7 +343,7 @@
 
     private ImmutableList<SkyKey> getDeps(
         RepositoryName repository,
-        ImmutableSet<PathFragment> repositoryIgnoredSubdirectories,
+        IgnoredSubdirectories repositoryIgnoredSubdirectories,
         FilteringPolicy policy,
         RootedPath rootedPath) {
       List<SkyKey> keys = new ArrayList<>();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryValue.java
index 36c8ba7..5052749 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryValue.java
@@ -15,7 +15,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
@@ -23,7 +23,6 @@
 import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
-import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.RootedPath;
 import com.google.devtools.build.skyframe.SkyFunctionName;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -51,8 +50,8 @@
 
   /** Create a prepare deps of targets under directory request. */
   @ThreadSafe
-  public static SkyKey key(RepositoryName repository, RootedPath rootedPath,
-      ImmutableSet<PathFragment> excludedPaths) {
+  public static SkyKey key(
+      RepositoryName repository, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
     return key(repository, rootedPath, excludedPaths, FilteringPolicies.NO_FILTER);
   }
 
@@ -64,7 +63,7 @@
   public static PrepareDepsOfTargetsUnderDirectoryKey key(
       RepositoryName repository,
       RootedPath rootedPath,
-      ImmutableSet<PathFragment> excludedPaths,
+      IgnoredSubdirectories excludedPaths,
       FilteringPolicy filteringPolicy) {
     return PrepareDepsOfTargetsUnderDirectoryKey.create(
         new RecursivePkgKey(repository, rootedPath, excludedPaths), filteringPolicy);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ProcessPackageDirectory.java b/src/main/java/com/google/devtools/build/lib/skyframe/ProcessPackageDirectory.java
index e0f300f..aa8d138 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ProcessPackageDirectory.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ProcessPackageDirectory.java
@@ -13,14 +13,13 @@
 // limitations under the License.
 package com.google.devtools.build.lib.skyframe;
 
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -60,7 +59,7 @@
     SkyKey makeSkyKey(
         RepositoryName repository,
         RootedPath subdirectory,
-        ImmutableSet<PathFragment> excludedSubdirectoriesBeneathSubdirectory);
+        IgnoredSubdirectories excludedSubdirectoriesBeneathSubdirectory);
   }
 
   private final BlazeDirectories directories;
@@ -81,7 +80,7 @@
   public ProcessPackageDirectoryResult getPackageExistenceAndSubdirDeps(
       RootedPath rootedPath,
       RepositoryName repositoryName,
-      ImmutableSet<PathFragment> excludedPaths,
+      IgnoredSubdirectories excludedPaths,
       SkyFunction.Environment env)
       throws InterruptedException, ProcessPackageDirectorySkyFunctionException {
     PathFragment rootRelativePath = rootedPath.getRootRelativePath();
@@ -263,7 +262,7 @@
       DirectoryListingValue dirListingValue,
       RootedPath rootedPath,
       RepositoryName repositoryName,
-      ImmutableSet<PathFragment> excludedPaths,
+      IgnoredSubdirectories excludedPaths,
       boolean siblingRepositoryLayout) {
     Root root = rootedPath.getRoot();
     PathFragment rootRelativePath = rootedPath.getRootRelativePath();
@@ -290,7 +289,7 @@
       }
 
       // If this subdirectory is one of the excluded paths, don't recurse into it.
-      if (excludedPaths.contains(subdirectory)) {
+      if (excludedPaths.matchingEntry(subdirectory) != null) {
         continue;
       }
 
@@ -298,7 +297,7 @@
           skyKeyTransformer.makeSkyKey(
               repositoryName,
               RootedPath.toRootedPath(root, subdirectory),
-              getExcludedSubdirectoriesBeneathSubdirectory(subdirectory, excludedPaths)));
+              excludedPaths.filterForDirectory(subdirectory)));
     }
     return childDeps;
   }
@@ -317,11 +316,9 @@
    * <p>TODO(bazel-team): Replace the excludedPaths set with a trie or a SortedSet for better
    * efficiency.
    */
-  public static ImmutableSet<PathFragment> getExcludedSubdirectoriesBeneathSubdirectory(
-      PathFragment subdirectory, ImmutableSet<PathFragment> excludedPaths) {
-    return excludedPaths.stream()
-        .filter(pathFragment -> pathFragment.startsWith(subdirectory))
-        .collect(toImmutableSet());
+  public static IgnoredSubdirectories getExcludedSubdirectoriesBeneathSubdirectory(
+      PathFragment subdirectory, IgnoredSubdirectories excludedPaths) {
+    return excludedPaths.filterForDirectory(subdirectory);
   }
 
   private static ProcessPackageDirectoryResult reportErrorAndReturn(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveDirectoryTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveDirectoryTraversalFunction.java
index 6700411..2100074 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveDirectoryTraversalFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveDirectoryTraversalFunction.java
@@ -15,9 +15,9 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.Event;
@@ -85,7 +85,7 @@
   protected abstract SkyKey getSkyKeyForSubdirectory(
       RepositoryName repository,
       RootedPath subdirectory,
-      ImmutableSet<PathFragment> excludedSubdirectoriesBeneathSubdirectory);
+      IgnoredSubdirectories excludedSubdirectoriesBeneathSubdirectory);
 
   /**
    * Called by {@link #visitDirectory} to compute the {@code TReturn} value it returns, as a
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePackageProviderBackedTargetPatternResolver.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePackageProviderBackedTargetPatternResolver.java
index a800de8..f56094e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePackageProviderBackedTargetPatternResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePackageProviderBackedTargetPatternResolver.java
@@ -30,6 +30,7 @@
 import com.google.common.util.concurrent.Uninterruptibles;
 import com.google.devtools.build.lib.cmdline.BatchCallback;
 import com.google.devtools.build.lib.cmdline.BatchCallback.SafeBatchCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.QueryExceptionMarkerInterface;
@@ -195,7 +196,7 @@
       final String originalPattern,
       String directory,
       boolean rulesOnly,
-      ImmutableSet<PathFragment> forbiddenSubdirectories,
+      IgnoredSubdirectories forbiddenSubdirectories,
       ImmutableSet<PathFragment> excludedSubdirectories,
       BatchCallback<Target, E> callback,
       Class<E> exceptionClass)
@@ -242,7 +243,7 @@
           String originalPattern,
           String directory,
           boolean rulesOnly,
-          ImmutableSet<PathFragment> forbiddenSubdirectories,
+          IgnoredSubdirectories forbiddenSubdirectories,
           ImmutableSet<PathFragment> excludedSubdirectories,
           BatchCallback<Target, E> callback,
           Class<E> exceptionClass,
@@ -286,12 +287,15 @@
           String pattern,
           String directory,
           boolean rulesOnly,
-          ImmutableSet<PathFragment> forbiddenSubdirectories,
+          IgnoredSubdirectories forbiddenSubdirectories,
           ImmutableSet<PathFragment> excludedSubdirectories,
           BatchCallback<Target, E> callback,
           ListeningExecutorService executor)
-          throws TargetParsingException, QueryException, InterruptedException,
-              ProcessPackageDirectoryException, NoSuchPackageException {
+          throws TargetParsingException,
+              QueryException,
+              InterruptedException,
+              ProcessPackageDirectoryException,
+              NoSuchPackageException {
     FilteringPolicy actualPolicy =
         rulesOnly ? FilteringPolicies.and(FilteringPolicies.RULES_ONLY, policy) : policy;
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java
index c569c59..0ca5985 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java
@@ -13,8 +13,8 @@
 // limitations under the License.
 package com.google.devtools.build.lib.skyframe;
 
-import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
@@ -66,7 +66,7 @@
     protected SkyKey getSkyKeyForSubdirectory(
         RepositoryName repository,
         RootedPath subdirectory,
-        ImmutableSet<PathFragment> excludedSubdirectoriesBeneathSubdirectory) {
+        IgnoredSubdirectories excludedSubdirectoriesBeneathSubdirectory) {
       return RecursivePkgValue.key(
           repository, subdirectory, excludedSubdirectoriesBeneathSubdirectory);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgKey.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgKey.java
index 3038da9..32e24ba 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgKey.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgKey.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
@@ -36,13 +37,11 @@
 public class RecursivePkgKey {
   @VisibleForSerialization final RepositoryName repositoryName;
   @VisibleForSerialization final RootedPath rootedPath;
-  @VisibleForSerialization final ImmutableSet<PathFragment> excludedPaths;
+  @VisibleForSerialization final IgnoredSubdirectories excludedPaths;
 
   public RecursivePkgKey(
-      RepositoryName repositoryName,
-      RootedPath rootedPath,
-      ImmutableSet<PathFragment> excludedPaths) {
-    PathFragment.checkAllPathsAreUnder(excludedPaths, rootedPath.getRootRelativePath());
+      RepositoryName repositoryName, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
+    Preconditions.checkArgument(excludedPaths.allPathsAreUnder(rootedPath.getRootRelativePath()));
     this.repositoryName = repositoryName;
     this.rootedPath = Preconditions.checkNotNull(rootedPath);
     this.excludedPaths = Preconditions.checkNotNull(excludedPaths);
@@ -56,7 +55,7 @@
     return rootedPath;
   }
 
-  public ImmutableSet<PathFragment> getExcludedPaths() {
+  public IgnoredSubdirectories getExcludedPaths() {
     return excludedPaths;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgSkyKey.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgSkyKey.java
index 8cbd4e8..6d9635e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgSkyKey.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgSkyKey.java
@@ -14,18 +14,15 @@
 
 package com.google.devtools.build.lib.skyframe;
 
-import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
-import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.RootedPath;
 import com.google.devtools.build.skyframe.SkyKey;
 
 /** Common parent class of SkyKeys that wrap a {@link RecursivePkgKey}. */
 public abstract class RecursivePkgSkyKey extends RecursivePkgKey implements SkyKey {
   public RecursivePkgSkyKey(
-      RepositoryName repositoryName,
-      RootedPath rootedPath,
-      ImmutableSet<PathFragment> excludedPaths) {
+      RepositoryName repositoryName, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
     super(repositoryName, rootedPath, excludedPaths);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java
index ab6dd20..9b5ccaf 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.skyframe;
 
-import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
@@ -23,7 +23,6 @@
 import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
-import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.RootedPath;
 import com.google.devtools.build.skyframe.SkyFunctionName;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -58,9 +57,7 @@
   /** Create a transitive package lookup request. */
   @ThreadSafe
   public static Key key(
-      RepositoryName repositoryName,
-      RootedPath rootedPath,
-      ImmutableSet<PathFragment> excludedPaths) {
+      RepositoryName repositoryName, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
     return Key.create(repositoryName, rootedPath, excludedPaths);
   }
 
@@ -78,16 +75,12 @@
     private static final SkyKeyInterner<Key> interner = SkyKey.newInterner();
 
     private Key(
-        RepositoryName repositoryName,
-        RootedPath rootedPath,
-        ImmutableSet<PathFragment> excludedPaths) {
+        RepositoryName repositoryName, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
       super(repositoryName, rootedPath, excludedPaths);
     }
 
     private static Key create(
-        RepositoryName repositoryName,
-        RootedPath rootedPath,
-        ImmutableSet<PathFragment> excludedPaths) {
+        RepositoryName repositoryName, RootedPath rootedPath, IgnoredSubdirectories excludedPaths) {
       return interner.intern(new Key(repositoryName, rootedPath, excludedPaths));
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValueRootPackageExtractor.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValueRootPackageExtractor.java
index 47b27ea..07d74bb 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValueRootPackageExtractor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValueRootPackageExtractor.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.cmdline.BatchCallback.SafeBatchCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
@@ -39,12 +40,11 @@
       ExtendedEventHandler eventHandler,
       RepositoryName repository,
       PathFragment directory,
-      ImmutableSet<PathFragment> ignoredSubdirectories,
+      IgnoredSubdirectories ignoredSubdirectories,
       ImmutableSet<PathFragment> excludedSubdirectories)
       throws InterruptedException, QueryException {
-    ImmutableSet<PathFragment> filteredIgnoredSubdirectories =
-        ImmutableSet.copyOf(
-            Iterables.filter(ignoredSubdirectories, path -> path.startsWith(directory)));
+    IgnoredSubdirectories filteredIgnoredSubdirectories =
+        ignoredSubdirectories.filterForDirectory(directory);
 
     for (Root root : roots) {
       RootedPath rootedPath = RootedPath.toRootedPath(root, directory);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RootPackageExtractor.java b/src/main/java/com/google/devtools/build/lib/skyframe/RootPackageExtractor.java
index 758546a..7088ab6 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RootPackageExtractor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RootPackageExtractor.java
@@ -15,6 +15,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.cmdline.BatchCallback.SafeBatchCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
@@ -51,7 +52,7 @@
       ExtendedEventHandler eventHandler,
       RepositoryName repository,
       PathFragment directory,
-      ImmutableSet<PathFragment> forbiddenSubdirectories,
+      IgnoredSubdirectories forbiddenSubdirectories,
       ImmutableSet<PathFragment> excludedSubdirectories)
       throws InterruptedException, QueryException;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TraversalInfoRootPackageExtractor.java b/src/main/java/com/google/devtools/build/lib/skyframe/TraversalInfoRootPackageExtractor.java
index 2e4ad9f..302dd7e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TraversalInfoRootPackageExtractor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TraversalInfoRootPackageExtractor.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.devtools.build.lib.cmdline.BatchCallback.SafeBatchCallback;
+import com.google.devtools.build.lib.cmdline.IgnoredSubdirectories;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.ParallelVisitor;
 import com.google.devtools.build.lib.cmdline.QueryExceptionMarkerInterface;
@@ -59,7 +60,7 @@
       ExtendedEventHandler eventHandler,
       RepositoryName repository,
       PathFragment directory,
-      ImmutableSet<PathFragment> forbiddenSubdirectories,
+      IgnoredSubdirectories forbiddenSubdirectories,
       ImmutableSet<PathFragment> excludedSubdirectories)
       throws InterruptedException {
     TreeSet<TraversalInfo> dirsToCheckForPackages = new TreeSet<>(TRAVERSAL_INFO_COMPARATOR);
@@ -175,10 +176,8 @@
           for (RootedPath subdirectory : subdirectoryTransitivelyContainsPackages.keySet()) {
             if (subdirectoryTransitivelyContainsPackages.get(subdirectory)) {
               PathFragment subdirectoryRelativePath = subdirectory.getRootRelativePath();
-              ImmutableSet<PathFragment> forbiddenSubdirectoriesBeneathThisSubdirectory =
-                  info.forbiddenSubdirectories.stream()
-                      .filter(pathFragment -> pathFragment.startsWith(subdirectoryRelativePath))
-                      .collect(toImmutableSet());
+              IgnoredSubdirectories forbiddenSubdirectoriesBeneathThisSubdirectory =
+                  info.forbiddenSubdirectories.filterForDirectory(subdirectoryRelativePath);
               ImmutableSet<PathFragment> excludedSubdirectoriesBeneathThisSubdirectory =
                   info.excludedSubdirectories.stream()
                       .filter(pathFragment -> pathFragment.startsWith(subdirectoryRelativePath))
@@ -218,7 +217,7 @@
     // CollectPackagesUnderDirectoryValue nodes whose keys have forbidden packages embedded in
     // them. Therefore, we need to be careful to request and use the same sort of keys here in our
     // traversal.
-    final ImmutableSet<PathFragment> forbiddenSubdirectories;
+    final IgnoredSubdirectories forbiddenSubdirectories;
     // Set of directories, targets under which should be excluded from the traversal results.
     // Excluded directory information isn't part of the graph keys in the prepopulated graph, so we
     // need to perform the filtering ourselves.
@@ -226,7 +225,7 @@
 
     private TraversalInfo(
         RootedPath rootedDir,
-        ImmutableSet<PathFragment> forbiddenSubdirectories,
+        IgnoredSubdirectories forbiddenSubdirectories,
         ImmutableSet<PathFragment> excludedSubdirectories) {
       this.rootedDir = rootedDir;
       this.forbiddenSubdirectories = forbiddenSubdirectories;