Extend and disable-by-default Enum reflection tracing
Enum.valueOf and Enum collections will no longer cause Enum.values()
methods to be kept by default.
The logic is now guarded by a system property:
com.android.tools.r8.experimentalTraceEnumReflection
It now also recognizes:
* Bundle.getSerializable()
* Parcel.readSerializable()
* Intent.getSerializableExtra()
* ObjectInputStream.readObject()
Some limitations:
* For the overloads that accept a Class, it must be a const-class
* For other overloads, the return value must be directly used by a
checked-cast of the Enum type.
Bug: b/204939965
Change-Id: Icd7b273ebdea62c19917e9e5ecac6089849a536e
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 27f31fa..1aa0120 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -640,9 +640,6 @@
createStaticallyKnownType("Ljava/util/logging/Level;");
public final DexType javaUtilLoggingLoggerType =
createStaticallyKnownType("Ljava/util/logging/Logger;");
- public final DexType javaUtilEnumMapType = createStaticallyKnownType("Ljava/util/EnumMap;");
- public final DexType javaUtilEnumSetType = createStaticallyKnownType("Ljava/util/EnumSet;");
-
public final DexType androidAppActivity = createStaticallyKnownType("Landroid/app/Activity;");
public final DexType androidAppFragment = createStaticallyKnownType("Landroid/app/Fragment;");
public final DexType androidAppZygotePreload =
@@ -764,8 +761,6 @@
public final JavaUtilLocaleMembers javaUtilLocaleMembers = new JavaUtilLocaleMembers();
public final JavaUtilLoggingLevelMembers javaUtilLoggingLevelMembers =
new JavaUtilLoggingLevelMembers();
- public final JavaUtilEnumMapMembers javaUtilEnumMapMembers = new JavaUtilEnumMapMembers();
- public final JavaUtilEnumSetMembers javaUtilEnumSetMembers = new JavaUtilEnumSetMembers();
public final List<LibraryMembers> libraryMembersCollection =
ImmutableList.of(
@@ -1614,28 +1609,6 @@
}
}
- public class JavaUtilEnumMapMembers {
- public final DexMethod constructor =
- createMethod(javaUtilEnumMapType, createProto(voidType, classType), constructorMethodName);
- }
-
- public class JavaUtilEnumSetMembers {
- private final DexString allOfString = createString("allOf");
- private final DexString noneOfString = createString("noneOf");
- private final DexString rangeString = createString("range");
-
- public boolean isFactoryMethod(DexMethod invokedMethod) {
- if (!invokedMethod.getHolderType().equals(javaUtilEnumSetType)) {
- return false;
- }
- DexString name = invokedMethod.getName();
- return name.isIdenticalTo(allOfString)
- || name.isIdenticalTo(noneOfString)
- || name.isIdenticalTo(ofMethodName)
- || name.isIdenticalTo(rangeString);
- }
- }
-
public class LongMembers extends BoxedPrimitiveMembers {
public final DexField TYPE = createField(boxedLongType, classType, "TYPE");
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index fdc7fb5..d179979 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -99,9 +99,7 @@
import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteBuilderShrinker;
import com.android.tools.r8.ir.analysis.proto.ProtoEnqueuerUseRegistry;
import com.android.tools.r8.ir.analysis.proto.schema.ProtoEnqueuerExtension;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
import com.android.tools.r8.ir.code.ArrayPut;
-import com.android.tools.r8.ir.code.ConstClass;
import com.android.tools.r8.ir.code.ConstantValueUtils;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
@@ -553,6 +551,10 @@
ProtoEnqueuerExtension.register(appView, analysesBuilder);
ResourceAccessAnalysis.register(appView, this, analysesBuilder);
RuntimeTypeCheckInfo.register(runtimeTypeCheckInfoBuilder, analysesBuilder);
+ if (options.experimentalTraceEnumReflection) {
+ EnqueuerEnumReflectionSupport.register(
+ appView, this::markEnumValuesAsReachable, analysesBuilder);
+ }
}
analyses = analysesBuilder.build();
@@ -1548,11 +1550,6 @@
MethodResolutionResult resolutionResult =
handleInvokeOfDirectTarget(invokedMethod, context, reason);
analyses.traceInvokeDirect(invokedMethod, resolutionResult, context);
-
- if (invokedMethod.equals(appView.dexItemFactory().javaUtilEnumMapMembers.constructor)) {
- // EnumMap uses reflection.
- pendingReflectiveUses.add(context);
- }
}
void traceInvokeInterface(
@@ -1607,10 +1604,6 @@
identifierNameStrings.add(invokedMethod);
// Revisit the current method to implicitly add -keep rule for items with reflective access.
pendingReflectiveUses.add(context);
- } else if (invokedMethod == dexItemFactory.enumMembers.valueOf
- || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
- // See comment in handleEnumValueOfOrCollectionInstantiation.
- pendingReflectiveUses.add(context);
} else if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
pendingReflectiveUses.add(context);
} else if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) {
@@ -1647,7 +1640,6 @@
invokedMethod, context, registry, KeepReason.invokedFromLambdaCreatedIn(context));
}
- @SuppressWarnings("ReferenceEquality")
private void traceInvokeVirtual(
DexMethod invokedMethod,
ProgramMethod context,
@@ -1656,10 +1648,11 @@
if (registry != null && !registry.markInvokeVirtualAsSeen(invokedMethod)) {
return;
}
- if (invokedMethod == appView.dexItemFactory().classMethods.newInstance
- || invokedMethod == appView.dexItemFactory().constructorMethods.newInstance) {
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.newInstance)
+ || invokedMethod.isIdenticalTo(dexItemFactory.constructorMethods.newInstance)) {
pendingReflectiveUses.add(context);
- } else if (appView.dexItemFactory().classMethods.isReflectiveMemberLookup(invokedMethod)) {
+ } else if (dexItemFactory.classMethods.isReflectiveMemberLookup(invokedMethod)) {
// Implicitly add -identifiernamestring rule for the Java reflection in use.
identifierNameStrings.add(invokedMethod);
// Revisit the current method to implicitly add -keep rule for items with reflective access.
@@ -5224,33 +5217,24 @@
InstructionIterator iterator = code.instructionIterator();
while (iterator.hasNext()) {
Instruction instruction = iterator.next();
- handleReflectiveBehavior(method, instruction);
+ if (instruction.isInvokeMethod()) {
+ handleReflectiveBehavior(method, instruction.asInvokeMethod());
+ }
}
}
- @SuppressWarnings("ReferenceEquality")
- private void handleReflectiveBehavior(ProgramMethod method, Instruction instruction) {
- if (!instruction.isInvokeMethod()) {
- return;
- }
- InvokeMethod invoke = instruction.asInvokeMethod();
+ private void handleReflectiveBehavior(ProgramMethod method, InvokeMethod invoke) {
DexMethod invokedMethod = invoke.getInvokedMethod();
DexItemFactory dexItemFactory = appView.dexItemFactory();
- if (invokedMethod == dexItemFactory.classMethods.newInstance) {
+ if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.newInstance)) {
handleJavaLangClassNewInstance(method, invoke);
return;
}
- if (invokedMethod == dexItemFactory.constructorMethods.newInstance) {
+ if (invokedMethod.isIdenticalTo(dexItemFactory.constructorMethods.newInstance)) {
handleJavaLangReflectConstructorNewInstance(method, invoke);
return;
}
- if (invokedMethod == dexItemFactory.enumMembers.valueOf
- || invokedMethod == dexItemFactory.javaUtilEnumMapMembers.constructor
- || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
- handleEnumValueOfOrCollectionInstantiation(method, invoke);
- return;
- }
- if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
+ if (invokedMethod.isIdenticalTo(dexItemFactory.proxyMethods.newProxyInstance)) {
handleJavaLangReflectProxyNewProxyInstance(method, invoke);
return;
}
@@ -5573,47 +5557,6 @@
}
}
- private void handleEnumValueOfOrCollectionInstantiation(
- ProgramMethod context, InvokeMethod invoke) {
- if (invoke.inValues().isEmpty()) {
- // Should never happen.
- return;
- }
-
- // The use of java.lang.Enum.valueOf(java.lang.Class, java.lang.String) will indirectly
- // access the values() method of the enum class passed as the first argument. The method
- // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will
- // call this method.
- // Likewise, EnumSet and EnumMap call values() on the passed in Class.
- Value firstArg = invoke.getFirstNonReceiverArgument();
- if (firstArg.isPhi()) {
- return;
- }
- DexType type;
- if (invoke
- .getInvokedMethod()
- .getParameter(0)
- .isIdenticalTo(appView.dexItemFactory().classType)) {
- // EnumMap.<init>(), EnumSet.noneOf(), EnumSet.allOf(), Enum.valueOf().
- ConstClass constClass = firstArg.definition.asConstClass();
- if (constClass == null || !constClass.getType().isClassType()) {
- return;
- }
- type = constClass.getType();
- } else {
- // EnumSet.of(), EnumSet.range()
- ClassTypeElement typeElement = firstArg.getType().asClassType();
- if (typeElement == null) {
- return;
- }
- type = typeElement.getClassType();
- }
- DexProgramClass clazz = getProgramClassOrNull(type, context);
- if (clazz != null && clazz.isEnum()) {
- markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(context));
- }
- }
-
private void handleServiceLoaderInvocation(ProgramMethod method, InvokeMethod invoke) {
if (invoke.inValues().isEmpty()) {
// Should never happen.
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionSupport.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionSupport.java
new file mode 100644
index 0000000..376a723
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionSupport.java
@@ -0,0 +1,336 @@
+// Copyright (c) 2025, 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 com.android.tools.r8.shaking;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.analysis.EnqueuerAnalysisCollection;
+import com.android.tools.r8.graph.analysis.FixpointEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.TraceInvokeEnqueuerAnalysis;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
+
+/**
+ * Finds Enum.valueOf() methods that must be kept due to uses of OS APIs that use it via reflection.
+ *
+ * <p>Looks for:
+ *
+ * <ul>
+ * <li>Enum.valueOf()
+ * <li>EnumSet.allOf()
+ * <li>EnumSet.noneOf()
+ * <li>EnumSet.range()
+ * <li>new EnumMap()
+ * <li>Parcel.readSerializable()
+ * <li>Bundle.getSerializable()
+ * <li>Intent.getSerializableExtra()
+ * <li>ObjectInputStream.readObject()
+ * </ul>
+ *
+ * <p>Similar to non-enum reflection calls, tracing is best-effort. E.g.:
+ *
+ * <ul>
+ * <li>For the overloads that accept a Class, it must be a const-class
+ * <li>For others, the return value must be directly used by a checked-cast of the Enum type.
+ * </ul>
+ */
+public class EnqueuerEnumReflectionSupport
+ implements TraceInvokeEnqueuerAnalysis, FixpointEnqueuerAnalysis {
+ private final AppView<? extends AppInfoWithClassHierarchy> appView;
+ private final BiConsumer<DexProgramClass, KeepReason> markAsReachableCallback;
+
+ private final DexString enumSetAllOfString;
+ private final DexString enumSetNoneOfString;
+ private final DexString enumSetRangeString;
+ private final DexString enumSetOfString;
+ private final DexType androidContentIntentType;
+ private final DexType androidOsParcelType;
+ private final DexType javaIoObjectInputStreamType;
+ private final DexType javaUtilEnumMapType;
+ private final DexType javaUtilEnumSetType;
+
+ private final DexMethod intentGetSerializableExtra1;
+ private final DexMethod intentGetSerializableExtra2;
+ private final DexMethod bundleGetSerializable1;
+ private final DexMethod bundleGetSerializable2;
+ private final DexMethod parcelReadSerializable1;
+ private final DexMethod parcelReadSerializable2;
+
+ private final DexMethod objectInputStreamReadObject;
+ private final DexMethod enumValueOfMethod;
+ private final DexMethod enumMapConstructor;
+ private final ProgramMethodSet pendingReflectiveUses = ProgramMethodSet.createLinked();
+
+ public EnqueuerEnumReflectionSupport(
+ AppView<? extends AppInfoWithClassHierarchy> appView,
+ BiConsumer<DexProgramClass, KeepReason> markAsReachableCallback) {
+ this.appView = appView;
+ this.markAsReachableCallback = markAsReachableCallback;
+
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+
+ enumSetAllOfString = dexItemFactory.createString("allOf");
+ enumSetNoneOfString = dexItemFactory.createString("noneOf");
+ enumSetRangeString = dexItemFactory.createString("range");
+ enumSetOfString = dexItemFactory.ofMethodName;
+
+ androidContentIntentType = dexItemFactory.createType("Landroid/content/Intent;");
+ androidOsParcelType = dexItemFactory.createType("Landroid/os/Parcel;");
+ javaIoObjectInputStreamType = dexItemFactory.createType("Ljava/io/ObjectInputStream;");
+ javaUtilEnumMapType = dexItemFactory.createType("Ljava/util/EnumMap;");
+ javaUtilEnumSetType = dexItemFactory.createType("Ljava/util/EnumSet;");
+
+ intentGetSerializableExtra1 =
+ dexItemFactory.createMethod(
+ androidContentIntentType,
+ dexItemFactory.createProto(dexItemFactory.serializableType, dexItemFactory.stringType),
+ "getSerializableExtra");
+ intentGetSerializableExtra2 =
+ dexItemFactory.createMethod(
+ androidContentIntentType,
+ dexItemFactory.createProto(
+ dexItemFactory.serializableType,
+ dexItemFactory.stringType,
+ dexItemFactory.classType),
+ "getSerializableExtra");
+ bundleGetSerializable1 =
+ dexItemFactory.createMethod(
+ dexItemFactory.androidOsBundleType,
+ dexItemFactory.createProto(dexItemFactory.serializableType, dexItemFactory.stringType),
+ "getSerializable");
+ bundleGetSerializable2 =
+ dexItemFactory.createMethod(
+ dexItemFactory.androidOsBundleType,
+ dexItemFactory.createProto(
+ dexItemFactory.serializableType,
+ dexItemFactory.stringType,
+ dexItemFactory.classType),
+ "getSerializable");
+ parcelReadSerializable1 =
+ dexItemFactory.createMethod(
+ androidOsParcelType,
+ dexItemFactory.createProto(dexItemFactory.serializableType),
+ "readSerializable");
+ parcelReadSerializable2 =
+ dexItemFactory.createMethod(
+ androidOsParcelType,
+ dexItemFactory.createProto(
+ dexItemFactory.serializableType,
+ dexItemFactory.classLoaderType,
+ dexItemFactory.classType),
+ "readSerializable");
+ objectInputStreamReadObject =
+ dexItemFactory.createMethod(
+ javaIoObjectInputStreamType,
+ dexItemFactory.createProto(dexItemFactory.objectType),
+ "readObject");
+ enumValueOfMethod = dexItemFactory.enumMembers.valueOf;
+ enumMapConstructor =
+ dexItemFactory.createMethod(
+ javaUtilEnumMapType,
+ dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.classType),
+ dexItemFactory.constructorMethodName);
+ }
+
+ public static void register(
+ AppView<? extends AppInfoWithClassHierarchy> appView,
+ BiConsumer<DexProgramClass, KeepReason> markAsReachableCallback,
+ EnqueuerAnalysisCollection.Builder builder) {
+ EnqueuerEnumReflectionSupport instance =
+ new EnqueuerEnumReflectionSupport(appView, markAsReachableCallback);
+ builder.addTraceInvokeAnalysis(instance);
+ builder.addFixpointAnalysis(instance);
+ }
+
+ private DexProgramClass maybeGetProgramEnumType(DexType type, boolean checkSuper) {
+ // Arrays can be used for serialization-related methods.
+ if (type.isArrayType()) {
+ type = type.toBaseType(appView.dexItemFactory());
+ if (!type.isClassType()) {
+ return null;
+ }
+ }
+
+ DexClass dexClass = appView.definitionFor(type);
+ if (dexClass == null || !dexClass.isProgramClass() || !dexClass.hasSuperType()) {
+ return null;
+ }
+ DexType superType = dexClass.getSuperType();
+ if (superType.isIdenticalTo(appView.dexItemFactory().enumType)) {
+ return dexClass.asProgramClass();
+ }
+ // Cannot have a sub-sub-type of an Enum.
+ return checkSuper ? maybeGetProgramEnumType(superType, false) : null;
+ }
+
+ private void handleEnumCastFromSerializable(ProgramMethod context, Value value) {
+ if (value == null || value.isPhi()) {
+ return;
+ }
+ for (Instruction user : value.aliasedUsers()) {
+ CheckCast castInstr = user.asCheckCast();
+ if (castInstr == null) {
+ continue;
+ }
+ DexProgramClass enumClass = maybeGetProgramEnumType(castInstr.getType(), true);
+ if (enumClass != null) {
+ markAsReachableCallback.accept(enumClass, KeepReason.invokedFrom(context));
+ }
+ }
+ }
+
+ private void handleReflectiveEnumInvoke(
+ ProgramMethod context, InvokeMethod invoke, int classParamIndex) {
+ if (invoke.inValues().size() <= classParamIndex) {
+ // Should never happen.
+ return;
+ }
+
+ // The use of java.lang.Enum.valueOf(java.lang.Class, java.lang.String) will indirectly
+ // access the values() method of the enum class passed as the first argument. The method
+ // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will
+ // call this method.
+ // Likewise, EnumSet and EnumMap call values() on the passed in Class.
+ Value classArg = invoke.getArgumentForParameter(classParamIndex);
+ if (classArg.isPhi()) {
+ return;
+ }
+ DexType type;
+ if (invoke
+ .getInvokedMethod()
+ .getParameter(classParamIndex)
+ .isIdenticalTo(appView.dexItemFactory().classType)) {
+ // EnumMap.<init>(), EnumSet.noneOf(), EnumSet.allOf(), Enum.valueOf().
+ ConstClass constClass = classArg.definition.asConstClass();
+ if (constClass == null) {
+ return;
+ }
+ type = constClass.getType();
+ } else {
+ // EnumSet.of(), EnumSet.range()
+ ClassTypeElement typeElement = classArg.getType().asClassType();
+ if (typeElement == null) {
+ return;
+ }
+ type = typeElement.getClassType();
+ }
+ // Arrays can be used for serialization-related methods.
+ if (type.isArrayType()) {
+ type = type.toBaseType(appView.dexItemFactory());
+ }
+ DexClass clazz = appView.definitionFor(type, context);
+ if (clazz != null && clazz.isProgramClass() && clazz.isEnum()) {
+ markAsReachableCallback.accept(clazz.asProgramClass(), KeepReason.invokedFrom(context));
+ }
+ }
+
+ private void handleReflectiveBehavior(ProgramMethod method) {
+ IRCode code = method.buildIR(appView, MethodConversionOptions.nonConverting());
+ InstructionIterator iterator = code.instructionIterator();
+ while (iterator.hasNext()) {
+ Instruction instruction = iterator.next();
+ if (!instruction.isInvokeMethod()) {
+ continue;
+ }
+ InvokeMethod invoke = instruction.asInvokeMethod();
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ if (invokedMethod.isIdenticalTo(enumValueOfMethod)
+ || invokedMethod.isIdenticalTo(enumMapConstructor)
+ || isEnumSetFactoryMethod(invokedMethod)) {
+ handleReflectiveEnumInvoke(method, invoke, 0);
+ } else if (invokedMethod.isIdenticalTo(bundleGetSerializable2)
+ || invokedMethod.isIdenticalTo(parcelReadSerializable2)
+ || invokedMethod.isIdenticalTo(intentGetSerializableExtra2)) {
+ handleReflectiveEnumInvoke(method, invoke, 1);
+ } else if (invokedMethod.isIdenticalTo(bundleGetSerializable1)
+ || invokedMethod.isIdenticalTo(parcelReadSerializable1)
+ || invokedMethod.isIdenticalTo(intentGetSerializableExtra1)
+ || invokedMethod.isIdenticalTo(objectInputStreamReadObject)) {
+ // These forms do not take a Class parameter, but are often followed by a checked-cast.
+ // Get the enum type from the checked-cast.
+ // There is no checked-cast when the return value is used as an Object (e.g. toString(), or
+ // inserted into a List<Object>).
+ // This also fails to identify when the above methods are wrapped in a helper method
+ // (e.g. a "IntentUtils.safeGetSerializableExtra()").
+ handleEnumCastFromSerializable(method, invoke.outValue());
+ }
+ }
+ }
+
+ private boolean isEnumSetFactoryMethod(DexMethod invokedMethod) {
+ if (!invokedMethod.getHolderType().equals(javaUtilEnumSetType)) {
+ return false;
+ }
+ DexString name = invokedMethod.getName();
+ return name.isIdenticalTo(enumSetAllOfString)
+ || name.isIdenticalTo(enumSetNoneOfString)
+ || name.isIdenticalTo(enumSetOfString)
+ || name.isIdenticalTo(enumSetRangeString);
+ }
+
+ @Override
+ public void traceInvokeStatic(
+ DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+ if (invokedMethod.isIdenticalTo(enumValueOfMethod) || isEnumSetFactoryMethod(invokedMethod)) {
+ pendingReflectiveUses.add(context);
+ }
+ }
+
+ @Override
+ public void traceInvokeDirect(
+ DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+ // EnumMap uses reflection.
+ if (invokedMethod.isIdenticalTo(enumMapConstructor)) {
+ pendingReflectiveUses.add(context);
+ }
+ }
+
+ @Override
+ public void traceInvokeVirtual(
+ DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+ if (invokedMethod.isIdenticalTo(bundleGetSerializable1)
+ || invokedMethod.isIdenticalTo(bundleGetSerializable2)
+ || invokedMethod.isIdenticalTo(parcelReadSerializable1)
+ || invokedMethod.isIdenticalTo(parcelReadSerializable2)
+ || invokedMethod.isIdenticalTo(intentGetSerializableExtra1)
+ || invokedMethod.isIdenticalTo(intentGetSerializableExtra2)
+ || invokedMethod.isIdenticalTo(objectInputStreamReadObject)) {
+ pendingReflectiveUses.add(context);
+ }
+ }
+
+ @Override
+ public void notifyFixpoint(
+ Enqueuer enqueuer,
+ EnqueuerWorklist worklist,
+ ExecutorService executorService,
+ Timing timing) {
+ if (!pendingReflectiveUses.isEmpty()) {
+ timing.begin("Handle reflective Enum operations");
+ pendingReflectiveUses.forEach(this::handleReflectiveBehavior);
+ pendingReflectiveUses.clear();
+ timing.end();
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index ef25826..dd20685 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -770,6 +770,8 @@
System.getProperty("com.android.tools.r8.ignoreBootClasspathEnumsForMaindexTracing") != null;
public boolean pruneNonVissibleAnnotationClasses =
System.getProperty("com.android.tools.r8.pruneNonVissibleAnnotationClasses") != null;
+ public boolean experimentalTraceEnumReflection =
+ System.getProperty("com.android.tools.r8.experimentalTraceEnumReflection") != null;
// Flag to turn on/offLoad/store optimization in the Cf back-end.
public boolean enableLoadStoreOptimization = true;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
index 01a5e94..dfd0f1a 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
@@ -51,7 +51,14 @@
.addInnerClasses(FailingMethodEnumUnboxingTest.class)
.addKeepMainRules(TESTS)
.addKeepRules(enumKeepRules.getKeepRules())
- .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .addOptionsModification(
+ opt -> {
+ enableEnumOptions(opt, enumValueOptimization);
+ if (enumKeepRules == EnumKeepRules.NONE) {
+ // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+ opt.experimentalTraceEnumReflection = true;
+ }
+ })
.addEnumUnboxingInspector(
inspector ->
inspector.assertNotUnboxed(
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java
index e75d109..4662a90 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java
@@ -39,7 +39,14 @@
.addEnumUnboxingInspector(inspector -> inspector.assertNotUnboxed(Main.Enum.class))
.enableNeverClassInliningAnnotations()
.addKeepRules(enumKeepRules.getKeepRules())
- .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .addOptionsModification(
+ opt -> {
+ enableEnumOptions(opt, enumValueOptimization);
+ if (enumKeepRules == EnumKeepRules.NONE) {
+ // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+ opt.experimentalTraceEnumReflection = true;
+ }
+ })
.setMinApi(parameters)
.compile()
.run(parameters.getRuntime(), success)
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
index f976adb..e73cda0 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
@@ -46,7 +46,14 @@
inspector -> inspector.assertUnboxed(EnumValueOf.MyEnum.class))
.enableNeverClassInliningAnnotations()
.addKeepRules(enumKeepRules.getKeepRules())
- .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .addOptionsModification(
+ opt -> {
+ enableEnumOptions(opt, enumValueOptimization);
+ if (enumKeepRules == EnumKeepRules.NONE) {
+ // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+ opt.experimentalTraceEnumReflection = true;
+ }
+ })
.setMinApi(parameters)
.compile();
for (Class<?> main : TESTS) {
diff --git a/src/test/java/com/android/tools/r8/naming/EnumMinification.java b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
index 5b1d6f5..c72b68e 100644
--- a/src/test/java/com/android/tools/r8/naming/EnumMinification.java
+++ b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
@@ -17,6 +17,7 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase.EnumKeepRules;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import org.junit.Test;
@@ -50,6 +51,7 @@
.addProgramClassFileData(enumClassFile)
.addKeepMainRule(mainClass)
.addKeepRules("-neverinline enum * extends java.lang.Enum { valueOf(...); }")
+ .addKeepRules(EnumKeepRules.STUDIO.getKeepRules())
.enableProguardTestOptions()
.setMinApi(parameters)
.compile();
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
index fe575ed..117154a 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
@@ -49,6 +49,13 @@
.addInnerClasses(EnumValueOfOptimizationTest.class)
.addKeepMainRule(Main.class)
.addKeepRules(enumKeepRules.getKeepRules())
+ .addOptionsModification(
+ opt -> {
+ if (enumKeepRules == EnumKeepRules.NONE) {
+ // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+ opt.experimentalTraceEnumReflection = true;
+ }
+ })
.applyIf(
enableNoVerticalClassMergingAnnotations,
R8TestBuilder::enableNoVerticalClassMergingAnnotations,
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
deleted file mode 100644
index 2a9872c..0000000
--- a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
+++ /dev/null
@@ -1,208 +0,0 @@
-// 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 com.android.tools.r8.shaking.enums;
-
-import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import java.util.Arrays;
-import java.util.EnumMap;
-import java.util.EnumSet;
-import java.util.List;
-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 EnumCollectionsTest extends TestBase {
-
- @Parameter(0)
- public TestParameters parameters;
-
- @Parameters(name = "{0}")
- public static TestParametersCollection data() {
- return getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build();
- }
-
- private static final List<String> EXPECTED_OUTPUT =
- Arrays.asList(
- "none: [A, B]",
- "all: [B, C]",
- "of: [C]",
- "of: [D, E]",
- "of: [E, F, G]",
- "of: [F, G, H, I]",
- "of: [G, H, I, J, K]",
- "of: [H, I, J, K, L, M]",
- "range: [I, J]",
- "map: {J=1}",
- "valueOf: K",
- "phi: [B]");
-
- public static class TestMain {
- public enum EnumA {
- A,
- B
- }
-
- public enum EnumB {
- B,
- C
- }
-
- public enum EnumC {
- C,
- D
- }
-
- public enum EnumD {
- D,
- E
- }
-
- public enum EnumE {
- E,
- F,
- G
- }
-
- public enum EnumF {
- F,
- G,
- H,
- I
- }
-
- public enum EnumG {
- G,
- H,
- I,
- J,
- K
- }
-
- public enum EnumH {
- H,
- I,
- J,
- K,
- L,
- M
- }
-
- public enum EnumI {
- I,
- J
- }
-
- public enum EnumJ {
- J,
- K
- }
-
- public enum EnumK {
- K,
- L
- }
-
- @NeverInline
- private static void noneOf() {
- System.out.println("none: " + EnumSet.complementOf(EnumSet.noneOf(EnumA.class)));
- }
-
- @NeverInline
- private static void allOf() {
- System.out.println("all: " + EnumSet.allOf(EnumB.class));
- }
-
- @NeverInline
- private static void of1() {
- System.out.println("of: " + EnumSet.of(EnumC.C));
- }
-
- @NeverInline
- private static void of2() {
- System.out.println("of: " + EnumSet.of(EnumD.D, EnumD.E));
- }
-
- @NeverInline
- private static void of3() {
- System.out.println("of: " + EnumSet.of(EnumE.E, EnumE.F, EnumE.G));
- }
-
- @NeverInline
- private static void of4() {
- System.out.println("of: " + EnumSet.of(EnumF.F, EnumF.G, EnumF.H, EnumF.I));
- }
-
- @NeverInline
- private static void of5() {
- System.out.println("of: " + EnumSet.of(EnumG.G, EnumG.H, EnumG.I, EnumG.J, EnumG.K));
- }
-
- @NeverInline
- private static void ofVarArgs() {
- System.out.println("of: " + EnumSet.of(EnumH.H, EnumH.I, EnumH.J, EnumH.K, EnumH.L, EnumH.M));
- }
-
- @NeverInline
- private static void range() {
- System.out.println("range: " + EnumSet.range(EnumI.I, EnumI.J));
- }
-
- @NeverInline
- private static void map() {
- EnumMap<EnumJ, Integer> map = new EnumMap<>(EnumJ.class);
- map.put(EnumJ.J, 1);
- System.out.println("map: " + map);
- }
-
- @NeverInline
- private static void valueOf() {
- System.out.println("valueOf: " + EnumK.valueOf("K"));
- }
-
- public static void main(String[] args) {
- // Use different methods to ensure Enqueuer.traceInvokeStatic() triggers for each one.
- noneOf();
- allOf();
- of1();
- of2();
- of3();
- of4();
- of5();
- ofVarArgs();
- range();
- map();
- valueOf();
- // Ensure phi as argument does not cause issues.
- System.out.println(
- "phi: " + EnumSet.of((Enum) (args.length > 10 ? (Object) EnumA.A : (Object) EnumB.B)));
- }
- }
-
- @Test
- public void testRuntime() throws Exception {
- testForRuntime(parameters)
- .addProgramClassesAndInnerClasses(TestMain.class)
- .run(parameters.getRuntime(), TestMain.class)
- .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
- }
-
- @Test
- public void testR8() throws Exception {
- testForR8(parameters.getBackend())
- .setMinApi(parameters)
- .addProgramClassesAndInnerClasses(TestMain.class)
- .enableInliningAnnotations()
- .addKeepMainRule(TestMain.class)
- .compile()
- .run(parameters.getRuntime(), TestMain.class)
- .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
- }
-}
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionTest.java
new file mode 100644
index 0000000..9e9b297
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionTest.java
@@ -0,0 +1,505 @@
+// Copyright (c) 2025, 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 com.android.tools.r8.shaking.enums;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumA;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumB;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumC;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumD;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumE;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumF;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumG;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumH;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumI;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumJ;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumK;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumL;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumM;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumN;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumO;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumP;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumQ;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumR;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumS;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumT;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.List;
+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;
+
+/** Tests for the various APIs that cause an Enum type's values() method to be kept. */
+@RunWith(Parameterized.class)
+public class EnumReflectionTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build();
+ }
+
+ public static class FakeParcel {
+ // Deserializing arrays yields UnsatisfiedLinkError for com.android.org.conscrypt.NativeCrypto
+ // when running under ART.
+ private Class arrayType;
+ private ObjectInputStream objectInputStream;
+
+ public static byte[] toBytes(Serializable thing) {
+ try {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
+ objectOutputStream.writeObject(thing);
+ return byteArrayOutputStream.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static FakeParcel createWithSingleSerializable(Serializable thing) {
+ try {
+ FakeParcel ret = new FakeParcel();
+ if (thing.getClass().isArray()) {
+ ret.arrayType = thing.getClass();
+ if (Array.getLength(thing) != 1) {
+ throw new IllegalArgumentException();
+ }
+ thing = (Serializable) Array.get(thing, 0);
+ }
+ ret.objectInputStream = new ObjectInputStream(new ByteArrayInputStream(toBytes(thing)));
+ return ret;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Serializable readSerializable() {
+ try {
+ Serializable ret = (Serializable) objectInputStream.readObject();
+ if (arrayType != null) {
+ Object array = Array.newInstance(arrayType.getComponentType(), 1);
+ Array.set(array, 0, ret);
+ ret = (Serializable) array;
+ }
+ return ret;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public <T extends Serializable> T readSerializable(ClassLoader loader, Class<T> clazz) {
+ return clazz.cast(readSerializable());
+ }
+ }
+
+ public static class FakeBundle {
+ private Object parcel;
+
+ public static FakeBundle createWithSingleSerializable(Serializable thing) {
+ FakeBundle ret = new FakeBundle();
+ ret.parcel = FakeParcel.createWithSingleSerializable(thing);
+ return ret;
+ }
+
+ public <T extends Serializable> T getSerializable(String key, Class<T> clazz) {
+ return clazz.cast(getSerializable(key));
+ }
+
+ public Serializable getSerializable(String key) {
+ return ((FakeParcel) parcel).readSerializable();
+ }
+ }
+
+ public static class FakeIntent {
+ private Object parcel;
+
+ public static FakeIntent createWithSingleSerializable(Serializable thing) {
+ FakeIntent ret = new FakeIntent();
+ ret.parcel = FakeParcel.createWithSingleSerializable(thing);
+ return ret;
+ }
+
+ public <T extends Serializable> T getSerializableExtra(String key, Class<T> clazz) {
+ return clazz.cast(getSerializableExtra(key));
+ }
+
+ public Serializable getSerializableExtra(String key) {
+ return ((FakeParcel) parcel).readSerializable();
+ }
+ }
+
+ private static final List<String> EXPECTED_OUTPUT =
+ Arrays.asList(
+ "none: [A, B]",
+ "all: [B, C]",
+ "of: [C]",
+ "of: [D, E]",
+ "of: [E, F, G]",
+ "of: [F, G, H, I]",
+ "of: [G, H, I, J, K]",
+ "of: [H, I, J, K, L, M]",
+ "range: [I, J]",
+ "map: {J=1}",
+ "valueOf: K",
+ "bundle: L",
+ "bundle: M",
+ "parcel: N",
+ "parcel: O",
+ "intent: P",
+ "intent: Q",
+ "stream: R",
+ "array: [S]",
+ "array: [T]",
+ "phi: [B]");
+
+ public static class Helpers {
+
+ public enum EnumA {
+ A,
+ B
+ }
+
+ public enum EnumB {
+ B,
+ C
+ }
+
+ public enum EnumC {
+ C,
+ D
+ }
+
+ public enum EnumD {
+ D,
+ E
+ }
+
+ public enum EnumE {
+ E,
+ F,
+ G
+ }
+
+ public enum EnumF {
+ F,
+ G,
+ H,
+ I
+ }
+
+ public enum EnumG {
+ G,
+ H,
+ I,
+ J,
+ K
+ }
+
+ public enum EnumH {
+ H,
+ I,
+ J,
+ K,
+ L,
+ M
+ }
+
+ public enum EnumI {
+ I,
+ J
+ }
+
+ public enum EnumJ {
+ J,
+ K
+ }
+
+ public enum EnumK {
+ K,
+ L
+ }
+
+ public enum EnumL {
+ L,
+ M
+ }
+
+ public enum EnumM {
+ M,
+ N
+ }
+
+ public enum EnumN {
+ N,
+ O
+ }
+
+ public enum EnumO {
+ O,
+ P
+ }
+
+ public enum EnumP {
+ P,
+ Q
+ }
+
+ public enum EnumQ {
+ Q,
+ R
+ }
+
+ public enum EnumR {
+ R {}, // Test anonymous enum subtype.
+ S
+ }
+
+ public enum EnumS {
+ S,
+ T
+ }
+
+ public enum EnumT {
+ T,
+ U
+ }
+ }
+
+ public static class TestMain {
+ @NeverInline
+ private static void noneOf() {
+ System.out.println("none: " + EnumSet.complementOf(EnumSet.noneOf(EnumA.class)));
+ }
+
+ @NeverInline
+ private static void allOf() {
+ System.out.println("all: " + EnumSet.allOf(EnumB.class));
+ }
+
+ @NeverInline
+ private static void of1() {
+ System.out.println("of: " + EnumSet.of(EnumC.C));
+ }
+
+ @NeverInline
+ private static void of2() {
+ System.out.println("of: " + EnumSet.of(EnumD.D, EnumD.E));
+ }
+
+ @NeverInline
+ private static void of3() {
+ System.out.println("of: " + EnumSet.of(EnumE.E, EnumE.F, EnumE.G));
+ }
+
+ @NeverInline
+ private static void of4() {
+ System.out.println("of: " + EnumSet.of(EnumF.F, EnumF.G, EnumF.H, EnumF.I));
+ }
+
+ @NeverInline
+ private static void of5() {
+ System.out.println("of: " + EnumSet.of(EnumG.G, EnumG.H, EnumG.I, EnumG.J, EnumG.K));
+ }
+
+ @NeverInline
+ private static void ofVarArgs() {
+ System.out.println("of: " + EnumSet.of(EnumH.H, EnumH.I, EnumH.J, EnumH.K, EnumH.L, EnumH.M));
+ }
+
+ @NeverInline
+ private static void range() {
+ System.out.println("range: " + EnumSet.range(EnumI.I, EnumI.J));
+ }
+
+ @NeverInline
+ private static void map() {
+ EnumMap<EnumJ, Integer> map = new EnumMap<>(EnumJ.class);
+ map.put(EnumJ.J, 1);
+ System.out.println("map: " + map);
+ }
+
+ @NeverInline
+ private static void valueOf() {
+ System.out.println("valueOf: " + EnumK.valueOf("K"));
+ }
+
+ @NeverInline
+ private static void androidBundle1() {
+ FakeBundle b = FakeBundle.createWithSingleSerializable(EnumL.L);
+ EnumL result = (EnumL) b.getSerializable("");
+ System.out.println("bundle: " + result);
+ }
+
+ @NeverInline
+ private static void androidBundle2() {
+ FakeBundle b = FakeBundle.createWithSingleSerializable(EnumM.M);
+ EnumM result = b.getSerializable("", EnumM.class);
+ System.out.println("bundle: " + result);
+ }
+
+ @NeverInline
+ private static void androidParcel1() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(EnumN.N);
+ EnumN result = (EnumN) p.readSerializable();
+ System.out.println("parcel: " + result);
+ }
+
+ @NeverInline
+ private static void androidParcel2() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(EnumO.O);
+ System.out.println("parcel: " + p.readSerializable(null, EnumO.class));
+ }
+
+ @NeverInline
+ private static void androidIntent1() {
+ FakeIntent i = FakeIntent.createWithSingleSerializable(EnumP.P);
+ EnumP result = (EnumP) i.getSerializableExtra("");
+ System.out.println("intent: " + result);
+ }
+
+ @NeverInline
+ private static void androidIntent2() {
+ FakeIntent i = FakeIntent.createWithSingleSerializable(EnumQ.Q);
+ System.out.println("intent: " + i.getSerializableExtra("", EnumQ.class));
+ }
+
+ @NeverInline
+ private static void array1() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumS[] {EnumS.S});
+ EnumS[] result = (EnumS[]) p.readSerializable();
+ System.out.println("array: " + Arrays.toString(result));
+ }
+
+ @NeverInline
+ private static void array2() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumT[] {EnumT.T});
+ System.out.println("array: " + Arrays.toString(p.readSerializable(null, EnumT[].class)));
+ }
+
+ @NeverInline
+ private static void objectStream() {
+ try {
+ ObjectInputStream objectInputStream =
+ new ObjectInputStream(new ByteArrayInputStream(FakeParcel.toBytes(EnumR.R)));
+ EnumR result = (EnumR) objectInputStream.readObject();
+ System.out.println("stream: " + result);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void main(String[] args) {
+ // Use different methods to ensure Enqueuer.traceInvokeStatic() triggers for each one.
+ noneOf();
+ allOf();
+ of1();
+ of2();
+ of3();
+ of4();
+ of5();
+ ofVarArgs();
+ range();
+ map();
+ valueOf();
+ androidBundle1();
+ androidBundle2();
+ androidParcel1();
+ androidParcel2();
+ androidIntent1();
+ androidIntent2();
+ objectStream();
+ array1();
+ array2();
+ // Ensure phi as argument does not cause issues.
+ System.out.println(
+ "phi: " + EnumSet.of((Enum) (args.length > 10 ? (Object) EnumA.A : (Object) EnumB.B)));
+ }
+ }
+
+ // EnumR.R references this class in its constructor.
+ private static final String ENUM_SUBTYPE_BRIDGE_CLASS_NAME =
+ EnumReflectionTest.class.getName() + "$1";
+
+ private static final String PARCEL_DESCRIPTOR = "Landroid/os/Parcel;";
+ private static final String BUNDLE_DESCRIPTOR = "Landroid/os/Bundle;";
+ private static final String INTENT_DESCRIPTOR = "Landroid/content/Intent;";
+
+ private static byte[] rewriteTestMain() throws IOException {
+ return transformer(TestMain.class)
+ .replaceClassDescriptorInMethodInstructions(descriptor(FakeParcel.class), PARCEL_DESCRIPTOR)
+ .replaceClassDescriptorInMethodInstructions(descriptor(FakeBundle.class), BUNDLE_DESCRIPTOR)
+ .replaceClassDescriptorInMethodInstructions(descriptor(FakeIntent.class), INTENT_DESCRIPTOR)
+ .transform();
+ }
+
+ private static byte[] rewriteParcel() throws IOException {
+ return transformer(FakeParcel.class).setClassDescriptor(PARCEL_DESCRIPTOR).transform();
+ }
+
+ private static byte[] rewriteBundle() throws IOException {
+ return transformer(FakeBundle.class)
+ .setClassDescriptor(BUNDLE_DESCRIPTOR)
+ .replaceClassDescriptorInMethodInstructions(descriptor(FakeParcel.class), PARCEL_DESCRIPTOR)
+ .transform();
+ }
+
+ private static byte[] rewriteIntent() throws IOException {
+ return transformer(FakeIntent.class)
+ .setClassDescriptor(INTENT_DESCRIPTOR)
+ .replaceClassDescriptorInMethodInstructions(descriptor(FakeParcel.class), PARCEL_DESCRIPTOR)
+ .transform();
+ }
+
+ @Test
+ public void testRuntime() throws Exception {
+ byte[] parcelBytes = rewriteParcel();
+ byte[] bundleBytes = rewriteBundle();
+ byte[] intentBytes = rewriteIntent();
+ testForRuntime(parameters)
+ .addProgramClassesAndInnerClasses(Helpers.class)
+ .addProgramClassFileData(rewriteTestMain())
+ .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
+ .addClasspathClassFileData(parcelBytes, bundleBytes, intentBytes)
+ .addRunClasspathFiles(buildOnDexRuntime(parameters, parcelBytes, bundleBytes, intentBytes))
+ .run(parameters.getRuntime(), TestMain.class)
+ .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ byte[] parcelBytes = rewriteParcel();
+ byte[] bundleBytes = rewriteBundle();
+ byte[] intentBytes = rewriteIntent();
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addOptionsModification(options -> options.experimentalTraceEnumReflection = true)
+ .addProgramClassesAndInnerClasses(Helpers.class)
+ .addProgramClassFileData(rewriteTestMain())
+ .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
+ .addClasspathClassFileData(parcelBytes, bundleBytes, intentBytes)
+ .enableInliningAnnotations()
+ .addKeepMainRule(TestMain.class)
+ .compile()
+ .addRunClasspathClassFileData(parcelBytes, bundleBytes, intentBytes)
+ .run(parameters.getRuntime(), TestMain.class)
+ .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+ }
+}