Correctly handle failing finalizers; allow unexpanded finalizers in a package which contains errors

If the evaluation of a package threw an EvalException, it is possible and
legal for some finalizers to be left unexpanded. Moreover, we shouldn't try
to expand finalizers in a package that's already in error, just as we
already stop evaluating top-level code and non-finalizer macros after an
earlier step in package evaluation throws.

Fixes ISE introduced by https://github.com/bazelbuild/bazel/commit/11c38261667b9976b7bd08f9f05c8a735d65f8e2

PiperOrigin-RevId: 761997199
Change-Id: I2a1576b0f112ef20e1f71a0b6e3b774fba69b7c1
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java
index bbbc50f..0cd1ff8 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Package.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java
@@ -1538,7 +1538,8 @@
     }
 
     /**
-     * Ensures that all symbolic macros in the package have expanded.
+     * Ensures that all symbolic macros in an error-free package have expanded. No-op if the package
+     * already {@link #containsErrors}.
      *
      * <p>This does not run any macro that has already been evaluated. It *does* run macros that are
      * newly discovered during the operation of this method.
@@ -1555,8 +1556,9 @@
       // longer a concern, we will want to support delayed expansion of non-finalizer macros before
       // the finalizer expansion step.
 
-      // Finalizer expansion step.
-      if (!unexpandedMacros.isEmpty()) {
+      // Finalizer expansion step. Requires that the package not be in error (no point in finalizing
+      // a package that already threw an EvalException).
+      if (!containsErrors() && !unexpandedMacros.isEmpty()) {
         Preconditions.checkState(
             unexpandedMacros.stream()
                 .allMatch(id -> recorder.getMacroMap().get(id).getMacroClass().isFinalizer()),
@@ -1584,16 +1586,16 @@
     @Override
     @CanIgnoreReturnValue
     protected Builder beforeBuild() throws NoSuchPackageException {
-      // For correct semantics, we refuse to build a package that has declared symbolic macros that
-      // have not yet been expanded. (Currently finalizers are the only use case where this happens,
-      // but the Package logic is agnostic to that detail.)
+      // For correct semantics, we refuse to build a package that hasn't thrown any EvalExceptions
+      // but has declared symbolic macros that have not yet been expanded. (Currently finalizers are
+      // the only use case where this happens, but the Package logic is agnostic to that detail.)
       //
       // Production code should be calling expandAllRemainingMacros() to guarantee that nothing is
       // left unexpanded. Tests that do not declare any symbolic macros need not make the call.
       // Package deserialization doesn't have to do it either, since we shouldn't be evaluating
       // symbolic macros on the deserialized result of an already evaluated package.
       Preconditions.checkState(
-          unexpandedMacros.isEmpty(),
+          unexpandedMacros.isEmpty() || containsErrors(),
           "Cannot build a package with unexpanded symbolic macros; call"
               + " expandAllRemainingMacros()");
 
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RuleFinalizerTest.java b/src/test/java/com/google/devtools/build/lib/packages/RuleFinalizerTest.java
index ec25016..df37e4b 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/RuleFinalizerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/RuleFinalizerTest.java
@@ -262,4 +262,66 @@
     assertContainsEventWithFrequency(
         "native.existing_rules and native.existing_rule are as expected", 4);
   }
+
+  @Test
+  public void packageInError_notFinalized() throws Exception {
+    scratch.file(
+        "pkg/finalizers.bzl",
+        """
+        def _impl(name, visibility):
+            print("in my_finalizer")
+            native.cc_library(name = name + "_lib")
+
+        my_finalizer = macro(implementation = _impl, finalizer = True)
+        """);
+    scratch.file(
+        "pkg/BUILD",
+        """
+        load(":finalizers.bzl", "my_finalizer")
+        my_finalizer(name = "finalize")
+        cc_library(name = 1 // 0)  # causes EvalException
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    Package pkg = getPackage("pkg");
+    assertThat(pkg).isNotNull();
+    assertThat(pkg.containsErrors()).isTrue();
+    assertContainsEvent("division by zero");
+    assertDoesNotContainEvent("in my_finalizer");
+    assertThat(pkg.getTargets().keySet()).doesNotContain("finalize_lib");
+  }
+
+  // Regression test for b/419523258.
+  @Test
+  public void finalizerFailure_handledCleanly() throws Exception {
+    scratch.file(
+        "pkg/finalizers.bzl",
+        """
+        def _fail_impl(name, visibility):
+            fail("fail fail fail")
+
+        def _good_impl(name, visibility):
+            native.cc_library(name = name + "_lib")
+
+        fail_finalizer = macro(implementation = _fail_impl, finalizer = True)
+        good_finalizer = macro(implementation = _good_impl, finalizer = True)
+        """);
+    scratch.file(
+        "pkg/BUILD",
+        """
+        load(":finalizers.bzl", "fail_finalizer", "good_finalizer")
+        good_finalizer(name = "good_finalizer")
+        fail_finalizer(name = "bad_finalizer")
+        good_finalizer(name = "should_not_be_expanded")  # because it follows a failing one
+        cc_library(name = "unrelated_target")  # evaluated before any finalizers
+        """);
+
+    reporter.removeHandler(failFastHandler);
+    Package pkg = getPackage("pkg");
+    assertThat(pkg).isNotNull();
+    assertThat(pkg.containsErrors()).isTrue();
+    assertContainsEvent("fail fail fail");
+    assertThat(pkg.getTargets().keySet()).containsAtLeast("unrelated_target", "good_finalizer_lib");
+    assertThat(pkg.getTargets().keySet()).doesNotContain("should_not_be_expanded_lib");
+  }
 }