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)