Support jdk21 switches with missing classes/enum

Bug: b/336510513
Change-Id: I367ef9adbe9eb33c18b591da504fe680b40dc3bc
diff --git a/src/main/java/com/android/tools/r8/graph/UseRegistry.java b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
index 9d623da..b69adc0 100644
--- a/src/main/java/com/android/tools/r8/graph/UseRegistry.java
+++ b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
@@ -265,10 +265,12 @@
             throw new CompilationError(
                 "Unsupported const dynamic in call site " + arg, getContext().getOrigin());
           }
-          DexField dexField =
+          DexField enumField =
               TypeSwitchDesugaringHelper.extractEnumField(
                   arg.asDexValueConstDynamic(), getMethodContext(), appView);
-          registerStaticFieldRead(dexField);
+          if (enumField != null) {
+            registerStaticFieldRead(enumField);
+          }
           break;
         default:
           assert arg.isDexValueInt()
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaring.java
index 495b0aa..4961b64 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaring.java
@@ -233,8 +233,7 @@
             cfInstructions.add(new CfConstClass(bootstrapArg.asDexValueType().getValue()));
           } else if (bootstrapArg.isDexValueString()) {
             DexField enumField =
-                getEnumField(
-                    bootstrapArg.asDexValueString().getValue(), enumType, context, appView);
+                getEnumField(bootstrapArg.asDexValueString().getValue(), enumType, appView);
             pushEnumField(cfInstructions, enumField);
           } else {
             throw new CompilationError(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaringHelper.java b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaringHelper.java
index 5815e6b..8d20c19 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaringHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchDesugaringHelper.java
@@ -99,14 +99,15 @@
     DexString className = dexValueClassName.asDexValueString().getValue();
     DexType enumType =
         factory.createType(DescriptorUtils.javaTypeToDescriptor(className.toString()));
-    return getEnumField(fieldName, enumType, context, appView);
+    return getEnumField(fieldName, enumType, appView);
   }
 
-  public static DexField getEnumField(
-      DexString fieldName, DexType enumType, DexClassAndMethod context, AppView<?> appView) {
-    DexClass enumClass = appView.definitionFor(enumType);
+  public static DexField getEnumField(DexString fieldName, DexType enumType, AppView<?> appView) {
+    DexClass enumClass = appView.appInfo().definitionForWithoutExistenceAssert(enumType);
     if (enumClass == null) {
-      throw throwEnumFieldConstantDynamic("Missing enum class " + enumType, context);
+      // If the enum class is missing, the case is (interestingly) considered unreachable and
+      // effectively removed from the switch (base on jdk 21 behavior).
+      return null;
     }
     DexEncodedField dexEncodedField = enumClass.lookupUniqueStaticFieldWithName(fieldName);
     if (dexEncodedField == null) {
diff --git a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
index a25a3e3..7c11f12 100644
--- a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
@@ -311,8 +311,10 @@
       } else if (bootstrapArg.isDexValueConstDynamic()) {
         DexField enumField =
             extractEnumField(bootstrapArg.asDexValueConstDynamic(), getContext(), appView);
-        registerStaticFieldReadFromSwitchMethodHandle(enumField);
-        registerEnumMethods(enumField.getHolderType());
+        if (enumField != null) {
+          registerStaticFieldReadFromSwitchMethodHandle(enumField);
+          registerEnumMethods(enumField.getHolderType());
+        }
       }
     }
   }
@@ -324,7 +326,7 @@
         registerTypeReference(bootstrapArg.asDexValueType().value);
       } else if (bootstrapArg.isDexValueString()) {
         DexString fieldName = bootstrapArg.asDexValueString().value;
-        DexField enumField = getEnumField(fieldName, enumType, getContext(), appView);
+        DexField enumField = getEnumField(fieldName, enumType, appView);
         registerStaticFieldReadFromSwitchMethodHandle(enumField);
         registerEnumMethods(enumType);
       }
diff --git a/src/test/examplesJava21/switchpatternmatching/TypeSwitchMissingClassTest.java b/src/test/examplesJava21/switchpatternmatching/TypeSwitchMissingClassTest.java
new file mode 100644
index 0000000..4acb88f
--- /dev/null
+++ b/src/test/examplesJava21/switchpatternmatching/TypeSwitchMissingClassTest.java
@@ -0,0 +1,160 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package switchpatternmatching;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static switchpatternmatching.SwitchTestHelper.hasJdk21TypeSwitch;
+
+import com.android.tools.r8.JdkClassFileProvider;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+import org.gradle.internal.impldep.com.google.common.collect.ImmutableList;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TypeSwitchMissingClassTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public ClassHolder present;
+
+  @Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        ImmutableList.of(new ClassHolder(C.class), new ClassHolder(Color.class)));
+  }
+
+  // ClassHolder allows to correctly print parameters in the IntelliJ test IDE with {1}.
+  private static class ClassHolder {
+    public final Class<?> clazz;
+
+    private ClassHolder(Class<?> clazz) {
+      this.clazz = clazz;
+    }
+
+    @Override
+    public String toString() {
+      return clazz.getSimpleName();
+    }
+  }
+
+  public static String EXPECTED_OUTPUT =
+      StringUtils.lines("null", "String", "Array of int, length = 0", "Other");
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    CodeInspector inspector = new CodeInspector(ToolHelper.getClassFileForTestClass(Main.class));
+    assertTrue(
+        hasJdk21TypeSwitch(inspector.clazz(Main.class).uniqueMethodWithOriginalName("typeSwitch")));
+
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .apply(this::addModifiedProgramClasses)
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.getCfRuntime().isNewerThanOrEqual(CfVm.JDK21),
+            this::assertResult,
+            r -> r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class));
+  }
+
+  private void assertResult(TestRunResult<?> r) {
+    if (present.clazz.equals(C.class)) {
+      r.assertSuccessWithOutput(EXPECTED_OUTPUT);
+    } else {
+      r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
+    }
+  }
+
+  private <T extends TestBuilder<?, T>> void addModifiedProgramClasses(
+      TestBuilder<?, T> testBuilder) throws Exception {
+    testBuilder
+        .addProgramClassFileData(transformer(Main.class).clearNest().transform())
+        .addProgramClassFileData(transformer(present.clazz).clearNest().transform());
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .apply(this::addModifiedProgramClasses)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::assertResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Assume.assumeTrue(
+        parameters.isDexRuntime()
+            || (parameters.isCfRuntime()
+                && parameters.getCfRuntime().isNewerThanOrEqual(CfVm.JDK21)));
+    testForR8(parameters.getBackend())
+        .apply(this::addModifiedProgramClasses)
+        .applyIf(
+            parameters.isCfRuntime(),
+            b -> b.addLibraryProvider(JdkClassFileProvider.fromSystemJdk()))
+        .addIgnoreWarnings(present.clazz.equals(Color.class))
+        .allowDiagnosticWarningMessages(present.clazz.equals(Color.class))
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::assertResult);
+  }
+
+  // Enum will be missing at runtime.
+  enum Color {
+    RED,
+    GREEN,
+    BLUE;
+  }
+
+  // Class will be missing at runtime.
+  static class C {
+
+    @Override
+    public String toString() {
+      return "CCC";
+    }
+  }
+
+  static class Main {
+
+    static void typeSwitch(Object obj) {
+      switch (obj) {
+        case null -> System.out.println("null");
+        case Color.RED -> System.out.println("RED!!!");
+        case Color.BLUE -> System.out.println("BLUE!!!");
+        case Color.GREEN -> System.out.println("GREEN!!!");
+        case String string -> System.out.println("String");
+        case C c -> System.out.println(c.toString() + "!!!");
+        case int[] intArray -> System.out.println("Array of int, length = " + intArray.length);
+        default -> System.out.println("Other");
+      }
+    }
+
+    public static void main(String[] args) {
+      typeSwitch(null);
+      typeSwitch("s");
+      typeSwitch(new int[] {});
+      typeSwitch(new Object());
+    }
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 8ca1934..72bc22a 100644
--- a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -415,6 +415,23 @@
         });
   }
 
+  public ClassFileTransformer clearNest() {
+    return setMinVersion(CfVm.JDK11)
+        .addClassTransformer(
+            new ClassTransformer() {
+
+              @Override
+              public void visitNestHost(String nestHost) {
+                // Ignore/remove existing nest information.
+              }
+
+              @Override
+              public void visitNestMember(String nestMember) {
+                // Ignore/remove existing nest information.
+              }
+            });
+  }
+
   public ClassFileTransformer setNest(Class<?> host, Class<?>... members) {
     assert !Arrays.asList(members).contains(host);
     return setMinVersion(CfVm.JDK11)