Move verification of include paths to execution phase. Analysis time is
precious.

RELNOTES: None.
PiperOrigin-RevId: 219529252
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
index 4db107a..36f934c 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
@@ -45,6 +45,7 @@
 import com.google.devtools.build.lib.actions.extra.CppCompileInfo;
 import com.google.devtools.build.lib.actions.extra.EnvironmentVariable;
 import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.CollectionUtils;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
@@ -816,6 +817,40 @@
     return Iterables.concat(cxxSystemIncludeDirs, ccCompilationContext.getSystemIncludeDirs());
   }
 
+  @VisibleForTesting
+  void verifyActionIncludePaths() throws ActionExecutionException {
+    ImmutableSet<PathFragment> ignoredDirs = ImmutableSet.copyOf(getValidationIgnoredDirs());
+    // We currently do not check the output of:
+    // - getQuoteIncludeDirs(): those only come from includes attributes, and are checked in
+    //   CcCommon.getIncludeDirsFromIncludesAttribute().
+    // - getBuiltinIncludeDirs(): while in practice this doesn't happen, bazel can be configured
+    //   to use an absolute system root, in which case the builtin include dirs might be absolute.
+
+    Iterable<PathFragment> includePathsToVerify =
+        Iterables.concat(getIncludeDirs(), getSystemIncludeDirs());
+    for (PathFragment includePath : includePathsToVerify) {
+      // includePathsToVerify contains all paths that are added as -isystem directive on the command
+      // line, most of which are added for include directives in the CcCompilationContext and are
+      // thus also in ignoredDirs. The hash lookup prevents this from becoming O(N^2) for these.
+      if (ignoredDirs.contains(includePath)
+          || FileSystemUtils.startsWithAny(includePath, ignoredDirs)) {
+        continue;
+      }
+      // One starting ../ is okay for getting to a sibling repository.
+      if (includePath.startsWith(Label.EXTERNAL_PATH_PREFIX)) {
+        includePath = includePath.relativeTo(Label.EXTERNAL_PATH_PREFIX);
+      }
+      if (includePath.isAbsolute() || includePath.containsUplevelReferences()) {
+        throw new ActionExecutionException(
+            String.format(
+                "The include path '%s' references a path outside of the execution root.",
+                includePath),
+            this,
+            false);
+      }
+    }
+  }
+
   /**
    * Returns true if an included artifact is declared in a set of allowed include directories. The
    * simple case is that the artifact's parent directory is contained in the set, or is empty.
@@ -1031,6 +1066,10 @@
   @ThreadCompatible
   public ActionResult execute(ActionExecutionContext actionExecutionContext)
       throws ActionExecutionException, InterruptedException {
+    if (needsIncludeValidation) {
+      verifyActionIncludePaths();
+    }
+
     setModuleFileFlags();
     CppCompileActionContext.Reply reply;
     ShowIncludesFilter showIncludesFilterForStdout = null;
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
index 1e22172..9629b55 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
@@ -18,7 +18,6 @@
 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.common.collect.Iterables;
 import com.google.devtools.build.lib.actions.ActionEnvironment;
 import com.google.devtools.build.lib.actions.ActionOwner;
@@ -26,14 +25,12 @@
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
-import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
 import com.google.devtools.build.lib.rules.cpp.CcCommon.CoptsFilter;
 import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
 import com.google.devtools.build.lib.rules.cpp.CppCompileAction.DotdFile;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -356,10 +353,6 @@
               builtinIncludeDirectories,
               grepIncludes);
     }
-
-    if (cppSemantics.needsIncludeValidation()) {
-      verifyActionIncludePaths(action, errorCollector);
-    }
     return action;
   }
 
@@ -408,37 +401,6 @@
     return shouldScanIncludes && useHeaderModules();
   }
 
-  private void verifyActionIncludePaths(CppCompileAction action, Consumer<String> errorReporter) {
-    ImmutableSet<PathFragment> ignoredDirs = ImmutableSet.copyOf(action.getValidationIgnoredDirs());
-    // We currently do not check the output of:
-    // - getQuoteIncludeDirs(): those only come from includes attributes, and are checked in
-    //   CcCommon.getIncludeDirsFromIncludesAttribute().
-    // - getBuiltinIncludeDirs(): while in practice this doesn't happen, bazel can be configured
-    //   to use an absolute system root, in which case the builtin include dirs might be absolute.
-
-    Iterable<PathFragment> includePathsToVerify =
-        Iterables.concat(action.getIncludeDirs(), action.getSystemIncludeDirs());
-    for (PathFragment includePath : includePathsToVerify) {
-      // includePathsToVerify contains all paths that are added as -isystem directive on the command
-      // line, most of which are added for include directives in the CcCompilationContext and are
-      // thus also in ignoredDirs. The hash lookup prevents this from becoming O(N^2) for these.
-      if (ignoredDirs.contains(includePath)
-          || FileSystemUtils.startsWithAny(includePath, ignoredDirs)) {
-        continue;
-      }
-      // One starting ../ is okay for getting to a sibling repository.
-      if (includePath.startsWith(Label.EXTERNAL_PATH_PREFIX)) {
-        includePath = includePath.relativeTo(Label.EXTERNAL_PATH_PREFIX);
-      }
-      if (includePath.isAbsolute() || includePath.containsUplevelReferences()) {
-        errorReporter.accept(
-            String.format(
-                "The include path '%s' references a path outside of the execution root.",
-                includePath));
-      }
-    }
-  }
-
   /**
    * Set action name that is used to pick the right action_config and features from {@link
    * FeatureConfiguration}. By default the action name is decided from the source filetype.
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
index f50c2a4..48f091e 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
@@ -22,6 +22,7 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.FailAction;
 import com.google.devtools.build.lib.actions.extra.CppLinkInfo;
@@ -1278,38 +1279,69 @@
 
   @Test
   public void testIncludePathsOutsideExecutionRoot() throws Exception {
-    checkError(
+    scratchRule(
         "root",
         "a",
-        "The include path '../somewhere' references a path outside of the execution root.",
         "cc_library(name='a', srcs=['a.cc'], copts=['-Id/../../somewhere'])");
+    CppCompileAction compileAction = getCppCompileAction("//root:a");
+    try {
+      compileAction.verifyActionIncludePaths();
+    } catch (ActionExecutionException exception) {
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              "The include path '../somewhere' references a path outside of the execution root.");
+    }
   }
 
   @Test
   public void testAbsoluteIncludePathsOutsideExecutionRoot() throws Exception {
-    checkError(
+    scratchRule(
         "root",
         "a",
-        "The include path '/somewhere' references a path outside of the execution root.",
         "cc_library(name='a', srcs=['a.cc'], copts=['-I/somewhere'])");
+    CppCompileAction compileAction = getCppCompileAction("//root:a");
+    try {
+      compileAction.verifyActionIncludePaths();
+    } catch (ActionExecutionException exception) {
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              "The include path '/somewhere' references a path outside of the execution root.");
+    }
   }
 
   @Test
   public void testSystemIncludePathsOutsideExecutionRoot() throws Exception {
-    checkError(
+    scratchRule(
         "root",
         "a",
-        "The include path '../system' references a path outside of the execution root.",
         "cc_library(name='a', srcs=['a.cc'], copts=['-isystem../system'])");
+    CppCompileAction compileAction = getCppCompileAction("//root:a");
+    try {
+      compileAction.verifyActionIncludePaths();
+    } catch (ActionExecutionException exception) {
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              "The include path '../system' references a path outside of the execution root.");
+    }
   }
 
   @Test
   public void testAbsoluteSystemIncludePathsOutsideExecutionRoot() throws Exception {
-    checkError(
+    scratchRule(
         "root",
         "a",
-        "The include path '/system' references a path outside of the execution root.",
         "cc_library(name='a', srcs=['a.cc'], copts=['-isystem/system'])");
+    CppCompileAction compileAction = getCppCompileAction("//root:a");
+    try {
+      compileAction.verifyActionIncludePaths();
+    } catch (ActionExecutionException exception) {
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo("The include path '/system' references a path outside of the execution root.");
+    }
   }
 
   /**