Restrict the number of transitive dependency edges traversable through attributes which use for_analysis_testing transitions.

Any cases exceeding this limit will result in a rule error being thrown on the analysis_test rule.

The limit is configurable via new flag --analysis_testing_deps_limit with a default limit of 500. The feature itself is still guarded behind --experimental_analysis_testing_improvements, so the former flag has no non-experimental meaning.

Progress toward #5574 and #6237.

RELNOTES: None.
PiperOrigin-RevId: 220144957
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
index 222125c..181f6d5 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
 import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
 import com.google.devtools.build.lib.analysis.constraints.ConstraintSemantics;
 import com.google.devtools.build.lib.analysis.constraints.EnvironmentCollection;
 import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironments;
@@ -49,6 +50,7 @@
 import com.google.devtools.build.lib.packages.TargetUtils;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Type;
+import com.google.devtools.build.lib.syntax.Type.LabelClass;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.TreeMap;
@@ -148,6 +150,18 @@
       addNativeDeclaredProvider(outputGroupInfo);
     }
 
+    if (ruleContext.getConfiguration().evaluatingForAnalysisTest()) {
+      if (ruleContext.getRule().isAnalysisTest()) {
+        ruleContext.ruleError(
+            String.format(
+                "analysis_test rule '%s' cannot be transitively "
+                    + "depended on by another analysis test rule",
+                ruleContext.getLabel()));
+        return null;
+      }
+      addProvider(new DepCountInfo(transitiveDepCount()));
+    }
+
     TransitiveInfoProviderMap providers = providersBuilder.build();
 
     if (ruleContext.getRule().isAnalysisTest()) {
@@ -163,7 +177,24 @@
       }
 
       AnalysisTestActionBuilder.writeAnalysisTestAction(ruleContext, testResultInfo);
+
+      int depCount = transitiveDepCount();
+      if (depCount > ruleContext.getConfiguration().analysisTestingDepsLimit()) {
+        ruleContext.ruleError(
+            String.format(
+                "analysis test rule excedeed maximum dependency edge count. "
+                    + "Count: %s. Limit is %s. This indicates either the analysis_test has too "
+                    + "many dependencies, or the underlying toolchains may be large. Try "
+                    + "decreasing the number of test dependencies. (Analysis tests should not be "
+                    + "very large!) If underlying toolchain size is to blame, it might be worth "
+                    + "considering increasing --analysis_testing_deps_limit. (Beware, however, "
+                    + "that large values of this flag can lead to no safeguards against giant "
+                    + "test suites that can lead to Out Of Memory exceptions in the build server.)",
+                depCount, ruleContext.getConfiguration().analysisTestingDepsLimit()));
+        return null;
+      }
     }
+
     AnalysisEnvironment analysisEnvironment = ruleContext.getAnalysisEnvironment();
     GeneratingActions generatingActions = Actions.filterSharedActionsAndThrowActionConflict(
           analysisEnvironment.getActionKeyContext(), analysisEnvironment.getRegisteredActions());
@@ -174,6 +205,22 @@
         generatingActions.getGeneratingActionIndex());
   }
 
+  private int transitiveDepCount() {
+    int depCount = 0;
+
+    for (String attributeName : ruleContext.attributes().getAttributeNames()) {
+      Type<?> attributeType =
+          ruleContext.attributes().getAttributeDefinition(attributeName).getType();
+      if (attributeType.getLabelClass() == LabelClass.DEPENDENCY) {
+        for (DepCountInfo depCountInfo :
+            ruleContext.getPrerequisites(attributeName, Mode.DONT_CHECK, DepCountInfo.class)) {
+          depCount += depCountInfo.getNumDepEdges() + 1;
+        }
+      }
+    }
+    return depCount;
+  }
+
   /**
    * Compute the artifacts to put into the {@link FilesToRunProvider} for this target. These are the
    * filesToBuild, any artifacts added by the rule with {@link #addFilesToRun}, and the runfiles'
@@ -440,4 +487,22 @@
     this.actionsWithoutExtraAction = actions;
     return this;
   }
+
+  /**
+   * Contains a count of transitive dependency edges traversed by the target which propagated this
+   * object.
+   *
+   * <p>This is automatically provided by all targets which are being evaluated in analysis testing.
+   */
+  private static class DepCountInfo implements TransitiveInfoProvider {
+    private final int numDepEdges;
+
+    public DepCountInfo(int numDepEdges) {
+      this.numDepEdges = numDepEdges;
+    }
+
+    public int getNumDepEdges() {
+      return numDepEdges;
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index 66dea6b..54d0f5f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -800,6 +800,29 @@
     public boolean allowAnalysisFailures;
 
     @Option(
+        name = "evaluating for analysis test",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+        effectTags = {OptionEffectTag.BAZEL_INTERNAL_CONFIGURATION},
+        metadataTags = {OptionMetadataTag.INTERNAL},
+        help =
+            "If true, targets in the current configuration are being analyzed only for purposes "
+                + "of an analysis test. This, for example, imposes the restriction described by "
+                + "--analysis_testing_deps_limit.")
+    public boolean evaluatingForAnalysisTest;
+
+    @Option(
+        name = "analysis_testing_deps_limit",
+        defaultValue = "500",
+        documentationCategory = OptionDocumentationCategory.TESTING,
+        effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
+        help =
+            "Sets the maximum number of transitive dependencies through a rule attribute with "
+                + "a for_analysis_testing configuration transition. "
+                + "Exceeding this limit will result in a rule error.")
+    public int analysisTestingDepsLimit;
+
+    @Option(
         name = "features",
         allowMultiple = true,
         defaultValue = "",
@@ -1791,6 +1814,14 @@
     return options.allowAnalysisFailures;
   }
 
+  public boolean evaluatingForAnalysisTest() {
+    return options.evaluatingForAnalysisTest;
+  }
+
+  public int analysisTestingDepsLimit() {
+    return options.analysisTestingDepsLimit;
+  }
+
   public List<Label> getActionListeners() {
     return options.actionListeners;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionSplitTransitionProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionSplitTransitionProvider.java
index bbfee98..4d11de7 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionSplitTransitionProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionSplitTransitionProvider.java
@@ -63,21 +63,27 @@
   private final EventHandler eventHandler;
   private final List<String> inputs;
   private final List<String> expectedOutputs;
+  private final boolean isForAnalysisTesting;
 
-  public FunctionSplitTransitionProvider(BaseFunction transitionFunction,
-      SkylarkSemantics semantics, EventHandler eventHandler,
-      List<String> inputs, List<String> expectedOutputs) {
+  public FunctionSplitTransitionProvider(
+      BaseFunction transitionFunction,
+      SkylarkSemantics semantics,
+      EventHandler eventHandler,
+      List<String> inputs,
+      List<String> expectedOutputs,
+      boolean isForAnalysisTesting) {
     this.transitionFunction = transitionFunction;
     this.semantics = semantics;
     this.eventHandler = eventHandler;
     this.inputs = inputs;
     this.expectedOutputs = expectedOutputs;
+    this.isForAnalysisTesting = isForAnalysisTesting;
   }
 
   @Override
   public SplitTransition apply(AttributeMap attributeMap) {
-    return new FunctionSplitTransition(transitionFunction, semantics, eventHandler, inputs,
-        expectedOutputs);
+    return new FunctionSplitTransition(
+        transitionFunction, semantics, eventHandler, inputs, expectedOutputs, isForAnalysisTesting);
   }
 
   private static class FunctionSplitTransition implements SplitTransition {
@@ -86,14 +92,21 @@
     private final EventHandler eventHandler;
     private final List<String> inputs;
     private final List<String> expectedOutputs;
+    private final boolean isForAnalysisTesting;
 
-    public FunctionSplitTransition(BaseFunction transitionFunction, SkylarkSemantics semantics,
-        EventHandler eventHandler, List<String> inputs, List<String> expectedOutputs) {
+    public FunctionSplitTransition(
+        BaseFunction transitionFunction,
+        SkylarkSemantics semantics,
+        EventHandler eventHandler,
+        List<String> inputs,
+        List<String> expectedOutputs,
+        boolean isForAnalysisTesting) {
       this.transitionFunction = transitionFunction;
       this.semantics = semantics;
       this.eventHandler = eventHandler;
       this.inputs = inputs;
       this.expectedOutputs = expectedOutputs;
+      this.isForAnalysisTesting = isForAnalysisTesting;
     }
 
     @Override
@@ -359,6 +372,10 @@
 
       BuildConfiguration.Options buildConfigOptions;
       buildConfigOptions = buildOptions.get(BuildConfiguration.Options.class);
+
+      if (isForAnalysisTesting) {
+        buildConfigOptions.evaluatingForAnalysisTest = true;
+      }
       updateOutputDirectoryNameFragment(buildConfigOptions, transition);
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java
index 8fea7ed..2163e5f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java
@@ -265,9 +265,14 @@
         } else {
           builder.hasStarlarkDefinedTransition();
         }
-        builder.cfg(new FunctionSplitTransitionProvider(
-            transImpl, env.getSemantics(), env.getEventHandler(),
-            starlarkDefinedTransition.getInputs(), starlarkDefinedTransition.getOutputs()));
+        builder.cfg(
+            new FunctionSplitTransitionProvider(
+                transImpl,
+                env.getSemantics(),
+                env.getEventHandler(),
+                starlarkDefinedTransition.getInputs(),
+                starlarkDefinedTransition.getOutputs(),
+                starlarkDefinedTransition.isForAnalysisTesting()));
       } else if (!trans.equals("target")) {
         throw new EvalException(ast.getLocation(),
             "cfg must be either 'data', 'host', or 'target'.");
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
index 3dc8d44..7d8d298 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
@@ -2282,7 +2282,6 @@
         + "with for_analysis_testing=True");
   }
 
-
   @Test
   public void testBuildSettingRule_flag() throws Exception {
     setSkylarkSemanticsOptions("--experimental_build_setting_api=true");
@@ -2386,6 +2385,127 @@
             + "by specifying --experimental_build_setting_api");
   }
 
+  @Test
+  public void testAnalysisTestCannotDependOnAnalysisTest() throws Exception {
+    setSkylarkSemanticsOptions(
+        "--experimental_analysis_testing_improvements=true",
+        "--experimental_starlark_config_transitions=true");
+
+    scratch.file(
+        "test/extension.bzl",
+        "",
+        "def analysis_test_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "def middle_rule_impl(ctx):",
+        "  return []",
+        "def inner_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "",
+        "def transition_func(settings):",
+        "  return { '//command_line_option:strict_java_deps' : 'WARN' }",
+        "my_transition = transition(",
+        "  implementation = transition_func,",
+        "  for_analysis_testing=True,",
+        "  inputs = [],",
+        "  outputs = ['//command_line_option:strict_java_deps'])",
+        "",
+        "inner_rule_test = rule(",
+        "  implementation = analysis_test_rule_impl,",
+        "  analysis_test = True,",
+        ")",
+        "middle_rule = rule(",
+        "  implementation = middle_rule_impl,",
+        "  attrs = {'dep':  attr.label()}",
+        ")",
+        "outer_rule_test = rule(",
+        "  implementation = analysis_test_rule_impl,",
+        "  analysis_test = True,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "  })");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'outer_rule_test', 'middle_rule', 'inner_rule_test')",
+        "",
+        "outer_rule_test(name = 'outer', dep = ':middle')",
+        "middle_rule(name = 'middle', dep = ':inner')",
+        "inner_rule_test(name = 'inner')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:outer");
+    assertContainsEvent(
+        "analysis_test rule '//test:inner' cannot be transitively "
+            + "depended on by another analysis test rule");
+  }
+
+  @Test
+  public void testAnalysisTestOverDepsLimit() throws Exception {
+    setupAnalysisTestDepsLimitTest(10, 12);
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent(
+        "analysis test rule excedeed maximum dependency edge count. " + "Count: 13. Limit is 10.");
+  }
+
+  @Test
+  public void testAnalysisTestUnderDepsLimit() throws Exception {
+    setupAnalysisTestDepsLimitTest(10, 8);
+
+    assertThat(getConfiguredTarget("//test:r")).isNotNull();
+  }
+
+  private void setupAnalysisTestDepsLimitTest(int limit, int dependencyChainSize) throws Exception {
+    setSkylarkSemanticsOptions(
+        "--experimental_analysis_testing_improvements=true",
+        "--experimental_starlark_config_transitions=true");
+    useConfiguration("--analysis_testing_deps_limit=" + limit);
+
+    scratch.file(
+        "test/extension.bzl",
+        "",
+        "def outer_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "def dep_rule_impl(ctx):",
+        "  return []",
+        "",
+        "def transition_func(settings):",
+        "  return { '//command_line_option:strict_java_deps' : 'WARN' }",
+        "my_transition = transition(",
+        "    implementation = transition_func,",
+        "    for_analysis_testing=True,",
+        "    inputs = [],",
+        "    outputs = ['//command_line_option:strict_java_deps'])",
+        "",
+        "dep_rule = rule(",
+        "  implementation = dep_rule_impl,",
+        "  attrs = {'dep':  attr.label()}",
+        ")",
+        "outer_rule_test = rule(",
+        "  implementation = outer_rule_impl,",
+        "  fragments = ['java'],",
+        "  analysis_test = True,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "  })");
+
+    // Create a chain of targets where 'innerN' depends on 'inner{N+1}' until the max length.
+    StringBuilder dependingRulesChain = new StringBuilder();
+    for (int i = 0; i < dependencyChainSize; i++) {
+      dependingRulesChain.append(
+          String.format("dep_rule(name = 'inner%s', dep = ':inner%s')\n", i, (i + 1)));
+    }
+    dependingRulesChain.append(String.format("dep_rule(name = 'inner%s')", dependencyChainSize));
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'dep_rule', 'outer_rule_test')",
+        "",
+        "outer_rule_test(name = 'r', dep = ':inner0')",
+        dependingRulesChain.toString());
+  }
+
   /**
    * Skylark integration test that forces inlining.
    */