Create a mode to propagate AnalysisFailureInfo for rule errors instead of failing a build

This new functionality is tied to --experimental_allow_analysis_failures : This feature is designed to facilitate in-build (analysis-phase) testing rules.

Progress toward #6237.

RELNOTES: None.
PiperOrigin-RevId: 215820356
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTargetFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTargetFactory.java
index 1542591..8464e3b 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTargetFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTargetFactory.java
@@ -35,6 +35,8 @@
 import com.google.devtools.build.lib.analysis.configuredtargets.OutputFileConfiguredTarget;
 import com.google.devtools.build.lib.analysis.configuredtargets.PackageGroupConfiguredTarget;
 import com.google.devtools.build.lib.analysis.skylark.SkylarkRuleConfiguredTargetUtil;
+import com.google.devtools.build.lib.analysis.test.AnalysisFailure;
+import com.google.devtools.build.lib.analysis.test.AnalysisFailureInfo;
 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;
@@ -282,7 +284,7 @@
             .setConstraintSemantics(ruleClassProvider.getConstraintSemantics())
             .build();
     if (ruleContext.hasErrors()) {
-      return null;
+      return erroredConfiguredTarget(ruleContext);
     }
     ConfigurationFragmentPolicy configurationFragmentPolicy =
         rule.getRuleClassObject().getConfigurationFragmentPolicy();
@@ -303,12 +305,14 @@
       }
       if (rule.getRuleClassObject().isSkylark()) {
         // TODO(bazel-team): maybe merge with RuleConfiguredTargetBuilder?
-        return SkylarkRuleConfiguredTargetUtil.buildRule(
+        ConfiguredTarget target = SkylarkRuleConfiguredTargetUtil.buildRule(
             ruleContext,
             rule.getRuleClassObject().getAdvertisedProviders(),
             rule.getRuleClassObject().getConfiguredTargetFunction(),
             rule.getLocation(),
             env.getSkylarkSemantics());
+
+        return target != null ? target : erroredConfiguredTarget(ruleContext);
       } else {
         RuleClass.ConfiguredTargetFactory<ConfiguredTarget, RuleContext, ActionConflictException>
             factory =
@@ -323,6 +327,36 @@
       // propagated, as this would show a nasty stack trace to users, and only provide info
       // on one specific failure with poor messaging. By returning null, the caller can
       // inspect ruleContext for multiple errors and output thorough messaging on each.
+      return erroredConfiguredTarget(ruleContext);
+    }
+  }
+
+  /**
+   * Returns a {@link ConfiguredTarget} which indicates that an analysis error occurred in
+   * processing the target. In most cases, this returns null, which signals to callers that
+   * the target failed to build and thus the build should fail. However, if analysis failures
+   * are allowed in this build, this returns a stub {@link ConfiguredTarget} which contains
+   * information about the failure.
+   */
+  @Nullable
+  private ConfiguredTarget erroredConfiguredTarget(RuleContext ruleContext)
+      throws ActionConflictException {
+    if (ruleContext.getConfiguration().allowAnalysisFailures()) {
+      ImmutableList.Builder<AnalysisFailure> analysisFailures = ImmutableList.builder();
+
+      for (String errorMessage : ruleContext.getSuppressedErrorMessages()) {
+        analysisFailures.add(new AnalysisFailure(ruleContext.getLabel(), errorMessage));
+      }
+      RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext);
+      builder.addNativeDeclaredProvider(
+          AnalysisFailureInfo.forAnalysisFailures(analysisFailures.build()));
+      builder.addProvider(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY));
+      return builder.build();
+    } else {
+      // Returning a null ConfiguredTarget is an indication a rule error occurred. Exceptions are
+      // not propagated, as this would show a nasty stack trace to users, and only provide info
+      // on one specific failure with poor messaging. By returning null, the caller can
+      // inspect ruleContext for multiple errors and output thorough messaging on each.
       return null;
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
index f1902f2..66d071e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -85,7 +85,6 @@
 import com.google.devtools.build.lib.packages.RequiredProviders;
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.packages.RuleClass;
-import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
 import com.google.devtools.build.lib.packages.RuleErrorConsumer;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.TargetUtils;
@@ -181,7 +180,7 @@
   private final BuildConfiguration hostConfiguration;
   private final ConfigurationFragmentPolicy configurationFragmentPolicy;
   private final ImmutableList<Class<? extends BuildConfiguration.Fragment>> universalFragments;
-  private final ErrorReporter reporter;
+  private final RuleErrorConsumer reporter;
   @Nullable private final ToolchainContext toolchainContext;
   private final ConstraintSemantics constraintSemantics;
 
@@ -294,6 +293,22 @@
   }
 
   /**
+   * If this target's configuration suppresses analysis failures, this returns a list
+   * of strings, where each string corresponds to a description of an error that occurred during
+   * processing this target.
+   *
+   * @throws IllegalStateException if this target's configuration does not suppress analysis
+   *     failures (if {@code getConfiguration().allowAnalysisFailures()} is false)
+   */
+  public List<String> getSuppressedErrorMessages() {
+    Preconditions.checkState(getConfiguration().allowAnalysisFailures(),
+        "Error messages can only be retrieved via RuleContext if allow_analysis_failures is true");
+    Preconditions.checkState(reporter instanceof SuppressingErrorReporter,
+        "Unexpected error reporter");
+    return ((SuppressingErrorReporter) reporter).getErrorMessages();
+  }
+
+  /**
    * If this <code>RuleContext</code> is for an aspect implementation, returns that aspect.
    * (it is the last aspect in the list of aspects applied to a target; all other aspects
    * are the ones main aspect sees as specified by its "required_aspect_providers")
@@ -357,13 +372,6 @@
     return getAnalysisEnvironment().hasErrors();
   }
 
-  @Override
-  public void assertNoErrors() throws RuleErrorException {
-    if (hasErrors()) {
-      throw new RuleErrorException();
-    }
-  }
-
   /**
    * Returns an immutable map from attribute name to list of configured targets for that attribute.
    */
@@ -502,12 +510,6 @@
     reporter.ruleError(message);
   }
 
-  @Override
-  public RuleErrorException throwWithRuleError(String message) throws RuleErrorException {
-    reporter.ruleError(message);
-    throw new RuleErrorException();
-  }
-
   /**
    * Convenience function for subclasses to report non-attribute-specific
    * warnings in the current rule.
@@ -529,13 +531,6 @@
     reporter.attributeError(attrName, message);
   }
 
-  @Override
-  public RuleErrorException throwWithAttributeError(String attrName, String message)
-      throws RuleErrorException {
-    reporter.attributeError(attrName, message);
-    throw new RuleErrorException();
-  }
-
   /**
    * Like attributeError, but does not mark the configured target as errored.
    *
@@ -1436,7 +1431,7 @@
     private final BuildConfiguration configuration;
     private final BuildConfiguration hostConfiguration;
     private final PrerequisiteValidator prerequisiteValidator;
-    private final ErrorReporter reporter;
+    private final RuleErrorConsumer reporter;
     private OrderedSetMultimap<Attribute, ConfiguredTargetAndData> prerequisiteMap;
     private ImmutableMap<Label, ConfigMatchingProvider> configConditions;
     private NestedSet<PackageGroupContents> visibility;
@@ -1461,7 +1456,11 @@
       this.configuration = Preconditions.checkNotNull(configuration);
       this.hostConfiguration = Preconditions.checkNotNull(hostConfiguration);
       this.prerequisiteValidator = prerequisiteValidator;
-      reporter = new ErrorReporter(env, target.getAssociatedRule(), getRuleClassNameForLogging());
+      if (configuration.allowAnalysisFailures()) {
+        reporter = new SuppressingErrorReporter();
+      } else {
+        reporter = new ErrorReporter(env, target.getAssociatedRule(), getRuleClassNameForLogging());
+      }
     }
 
     @VisibleForTesting
@@ -1694,26 +1693,10 @@
     }
 
     @Override
-    public RuleErrorException throwWithRuleError(String message) throws RuleErrorException {
-      throw reporter.throwWithRuleError(message);
-    }
-
-    @Override
-    public RuleErrorException throwWithAttributeError(String attrName, String message)
-        throws RuleErrorException {
-      throw reporter.throwWithAttributeError(attrName, message);
-    }
-
-    @Override
     public boolean hasErrors() {
       return reporter.hasErrors();
     }
 
-    @Override
-    public void assertNoErrors() throws RuleErrorException {
-      reporter.assertNoErrors();
-    }
-
     private String badPrerequisiteMessage(
         ConfiguredTargetAndData prerequisite, String reason, boolean isWarning) {
       String msgReason = reason != null ? " (" + reason + ")" : "";
@@ -2026,6 +2009,41 @@
     protected Location getAttributeLocation(String attrName) {
       return rule.getAttributeLocation(attrName);
     }
+  }
 
+  /**
+   * Implementation of an error consumer which does not post any events, saves rule and attribute
+   * errors for future consumption, and drops warnings.
+   */
+  public static final class SuppressingErrorReporter implements RuleErrorConsumer {
+    private final List<String> errorMessages = Lists.newArrayList();
+
+    @Override
+    public void ruleWarning(String message) {}
+
+    @Override
+    public void ruleError(String message) {
+      errorMessages.add(message);
+    }
+
+    @Override
+    public void attributeWarning(String attrName, String message) {}
+
+    @Override
+    public void attributeError(String attrName, String message) {
+      errorMessages.add(message);
+    }
+
+    @Override
+    public boolean hasErrors() {
+      return !errorMessages.isEmpty();
+    }
+
+    /**
+     * Returns the error message strings reported to this error consumer.
+     */
+    public List<String> getErrorMessages() {
+      return errorMessages;
+    }
   }
 }
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 cb0c927..5f1b52b 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
@@ -786,6 +786,19 @@
     )
     public boolean isHost;
 
+    // TODO(cparsons): Make this flag non-experimental when it is fully implemented.
+    @Option(
+        name = "experimental_allow_analysis_failures",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.TESTING,
+        effectTags = { OptionEffectTag.LOADING_AND_ANALYSIS },
+        metadataTags = { OptionMetadataTag.EXPERIMENTAL },
+        help = "If true, an analysis failure of a rule target results in the target's propagation "
+            + "of an instance of AnalysisFailureInfo containing the error description, instead of "
+            + "resulting in a build failure."
+    )
+    public boolean allowAnalysisFailures;
+
     @Option(
         name = "features",
         allowMultiple = true,
@@ -1779,6 +1792,10 @@
     return options.enforceConstraints;
   }
 
+  public boolean allowAnalysisFailures() {
+    return options.allowAnalysisFailures;
+  }
+
   public List<Label> getActionListeners() {
     return options.actionListeners;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/AnalysisFailureInfo.java b/src/main/java/com/google/devtools/build/lib/analysis/test/AnalysisFailureInfo.java
index 5ee6aea..ffbe86b 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/AnalysisFailureInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/AnalysisFailureInfo.java
@@ -13,11 +13,12 @@
 // limitations under the License.
 package com.google.devtools.build.lib.analysis.test;
 
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.packages.BuiltinProvider;
 import com.google.devtools.build.lib.packages.Info;
 import com.google.devtools.build.lib.skylarkbuildapi.test.AnalysisFailureInfoApi;
-import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
 
 /**
  * Implementation of {@link AnalysisFailureInfoApi}.
@@ -25,7 +26,7 @@
  * Encapsulates information about analysis-phase errors which would have occurred during a
  * build.
  */
-public class AnalysisFailureInfo extends Info implements AnalysisFailureInfoApi {
+public class AnalysisFailureInfo extends Info implements AnalysisFailureInfoApi<AnalysisFailure> {
 
   /**
    * Singleton provider instance for {@link AnalysisFailureInfo}.
@@ -33,16 +34,23 @@
   public static final AnalysisFailureInfoProvider SKYLARK_CONSTRUCTOR =
       new AnalysisFailureInfoProvider();
 
-  private final SkylarkNestedSet causes;
+  private final NestedSet<AnalysisFailure> causes;
 
-  public AnalysisFailureInfo(
-      SkylarkNestedSet causes) {
+  private AnalysisFailureInfo(NestedSet<AnalysisFailure> causes) {
     super(SKYLARK_CONSTRUCTOR, Location.BUILTIN);
     this.causes = causes;
   }
 
+  /**
+   * Constructs and returns an {@link AnalysisFailureInfo} object representing the given failures.
+   */
+  public static AnalysisFailureInfo forAnalysisFailures(Iterable<AnalysisFailure> failures) {
+    return new AnalysisFailureInfo(
+        NestedSetBuilder.<AnalysisFailure>stableOrder().addAll(failures).build());
+  }
+
   @Override
-  public SkylarkNestedSet getCauses() {
+  public NestedSet<AnalysisFailure> getCauses() {
     return causes;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/AnalysisFailureInfoApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/AnalysisFailureInfoApi.java
index d2f776e..2b1ea62 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/AnalysisFailureInfoApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/AnalysisFailureInfoApi.java
@@ -14,11 +14,11 @@
 
 package com.google.devtools.build.lib.skylarkbuildapi.test;
 
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.skylarkbuildapi.ProviderApi;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
-import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
 import com.google.devtools.build.lib.syntax.SkylarkSemantics.FlagIdentifier;
 
 /**
@@ -48,7 +48,8 @@
         + "<code>causes</code> equal to the union of the <code>causes</code> of the "
         + "dependencies.</li></ul>",
     documented = false)
-public interface AnalysisFailureInfoApi extends SkylarkValue {
+public interface AnalysisFailureInfoApi<AnalysisFailureApiT extends AnalysisFailureApi>
+    extends SkylarkValue {
 
   @SkylarkCallable(
       name = "causes",
@@ -58,7 +59,7 @@
       structField = true,
       enableOnlyWithFlag = FlagIdentifier.EXPERIMENTAL_ANALYSIS_TESTING_IMPROVEMENTS
   )
-  public SkylarkNestedSet getCauses();
+  public NestedSet<AnalysisFailureApiT> getCauses();
 
   /** Provider class for {@link AnalysisFailureInfoApi} objects. */
   @SkylarkModule(name = "Provider", documented = false, doc = "")
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/BUILD b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/BUILD
index 15c79f7..a030d48 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/test/BUILD
@@ -22,6 +22,7 @@
         "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
         "//src/main/java/com/google/devtools/build/lib:syntax",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/skylarkbuildapi",
         "//third_party:guava",
         "//third_party:jsr305",