Add experimental option to trace enum usage in Android serialization APIs
Enabled by system property:
com.android.tools.r8.experimentalTraceAndroidEnumSerialization
The APIs that are looked for:
* Bundle.getSerializable()
* Parcel.readSerializable()
* Intent.getSerializableExtra()
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.
Reorganizes enum tracing into two helper classes:
* EnqueuerEnumReflectionSupportAndroid
* EnqueuerEnumReflectionSupportJava
Adds support for tracing to recognize enum subclasses.
Bug: b/204939965
Change-Id: I7db7a73a63e611fd0465a0c6888f6416b4c1ef05
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..1dda2a0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -402,9 +402,6 @@
// Method names used on MethodHandles.
public final DexString lookupString = createString("lookup");
public final DexString privateLookupInString = createString("privateLookupIn");
- public final DexString mockString = createString("mock");
- public final DexString spyString = createString("spy");
-
public final DexType booleanType = createStaticallyKnownType(booleanDescriptor);
public final DexType byteType = createStaticallyKnownType(byteDescriptor);
public final DexType charType = createStaticallyKnownType(charDescriptor);
@@ -640,9 +637,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 +758,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(
@@ -896,8 +888,6 @@
createStaticallyKnownType(desugarVarHandleDescriptorString);
public final DexType desugarMethodHandlesLookupType =
createStaticallyKnownType(desugarMethodHandlesLookupDescriptorString);
- public final DexType mockitoType = createStaticallyKnownType("Lorg/mockito/Mockito;");
-
public final DexType javaUtilConcurrentExecutorServiceType =
createStaticallyKnownType("Ljava/util/concurrent/ExecutorService;");
public final DexType javaUtilConcurrentForkJoinPoolType =
@@ -1614,28 +1604,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/graph/analysis/EnqueuerAnalysisCollection.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
index babfa95..f1320a4 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
@@ -17,6 +17,7 @@
import com.android.tools.r8.graph.ProgramDefinition;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.shaking.DefaultEnqueuerUseRegistry;
import com.android.tools.r8.shaking.Enqueuer;
import com.android.tools.r8.shaking.Enqueuer.FieldAccessKind;
@@ -39,6 +40,9 @@
private final TraceInvokeEnqueuerAnalysis[] invokeAnalyses;
private final TraceNewInstanceEnqueuerAnalysis[] newInstanceAnalyses;
+ // IR analysis.
+ private final IrBasedEnqueuerAnalysis[] irBasedEnqueuerAnalyses;
+
// Reachability events.
private final NewlyFailedMethodResolutionEnqueuerAnalysis[] newlyFailedMethodResolutionAnalyses;
private final NewlyLiveClassEnqueuerAnalysis[] newlyLiveClassAnalyses;
@@ -64,6 +68,8 @@
TraceInstanceOfEnqueuerAnalysis[] instanceOfAnalyses,
TraceInvokeEnqueuerAnalysis[] invokeAnalyses,
TraceNewInstanceEnqueuerAnalysis[] newInstanceAnalyses,
+ // IR analysis.
+ IrBasedEnqueuerAnalysis[] irBasedEnqueuerAnalyses,
// Reachability events.
NewlyFailedMethodResolutionEnqueuerAnalysis[] newlyFailedMethodResolutionAnalyses,
NewlyLiveClassEnqueuerAnalysis[] newlyLiveClassAnalyses,
@@ -86,6 +92,8 @@
this.instanceOfAnalyses = instanceOfAnalyses;
this.invokeAnalyses = invokeAnalyses;
this.newInstanceAnalyses = newInstanceAnalyses;
+ // IR Analysis.
+ this.irBasedEnqueuerAnalyses = irBasedEnqueuerAnalyses;
// Reachability events.
this.newlyFailedMethodResolutionAnalyses = newlyFailedMethodResolutionAnalyses;
this.newlyLiveClassAnalyses = newlyLiveClassAnalyses;
@@ -357,6 +365,15 @@
}
}
+ public boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke) {
+ for (IrBasedEnqueuerAnalysis analysis : irBasedEnqueuerAnalyses) {
+ if (analysis.handleReflectiveInvoke(method, invoke)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public static class Builder {
// Trace events.
@@ -369,6 +386,9 @@
private final List<TraceInvokeEnqueuerAnalysis> invokeAnalyses = new ArrayList<>();
private final List<TraceNewInstanceEnqueuerAnalysis> newInstanceAnalyses = new ArrayList<>();
+ // IR analysis.
+ private final List<IrBasedEnqueuerAnalysis> irBasedEnqueuerAnalyses = new ArrayList<>();
+
// Reachability events.
private final List<NewlyFailedMethodResolutionEnqueuerAnalysis>
newlyFailedMethodResolutionAnalyses = new ArrayList<>();
@@ -430,6 +450,12 @@
return this;
}
+ // IR analysis.
+ public Builder addIrBasedEnqueuerAnalysis(IrBasedEnqueuerAnalysis analysis) {
+ irBasedEnqueuerAnalyses.add(analysis);
+ return this;
+ }
+
// Reachability events.
public Builder addNewlyFailedMethodResolutionAnalysis(
@@ -507,6 +533,8 @@
instanceOfAnalyses.toArray(TraceInstanceOfEnqueuerAnalysis[]::new),
invokeAnalyses.toArray(TraceInvokeEnqueuerAnalysis[]::new),
newInstanceAnalyses.toArray(TraceNewInstanceEnqueuerAnalysis[]::new),
+ // IR analysis.
+ irBasedEnqueuerAnalyses.toArray(IrBasedEnqueuerAnalysis[]::new),
// Reachability events.
newlyFailedMethodResolutionAnalyses.toArray(
NewlyFailedMethodResolutionEnqueuerAnalysis[]::new),
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/IrBasedEnqueuerAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/IrBasedEnqueuerAnalysis.java
new file mode 100644
index 0000000..4940118
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/IrBasedEnqueuerAnalysis.java
@@ -0,0 +1,13 @@
+// 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.graph.analysis;
+
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.InvokeMethod;
+
+public interface IrBasedEnqueuerAnalysis {
+
+ /** Called for methods added by Enqueuer.addMethodThatRequireIrAnalysis(). */
+ boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke);
+}
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..626bc31 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;
@@ -398,7 +396,7 @@
private final ProguardCompatibilityActions.Builder proguardCompatibilityActionsBuilder;
/** A set of methods that need code inspection for Java reflection in use. */
- private final ProgramMethodSet pendingReflectiveUses = ProgramMethodSet.createLinked();
+ private final ProgramMethodSet methodsThatRequireIrAnalysis = ProgramMethodSet.create();
/** Mapping of types to the resolved methods for that type along with the context. */
private final Map<DexProgramClass, Map<ResolutionSearchKey, ProgramMethodSet>>
@@ -553,6 +551,14 @@
ProtoEnqueuerExtension.register(appView, analysesBuilder);
ResourceAccessAnalysis.register(appView, this, analysesBuilder);
RuntimeTypeCheckInfo.register(runtimeTypeCheckInfoBuilder, analysesBuilder);
+ EnqueuerMockitoAnalysis.register(appView, this, analysesBuilder);
+ // Enum reflection tracing is best-effort, but since it is more common for non-Android uses to
+ // omit the Enum.values() -keep that Android Studio uses, we enable Java enum reflection
+ // tracing by default, but leave Android's behind a flag.
+ new EnqueuerEnumReflectionAnalysisJava(appView, this).register(analysesBuilder);
+ if (options.experimentalTraceAndroidEnumSerialization) {
+ new EnqueuerEnumReflectionAnalysisAndroid(appView, this).register(analysesBuilder);
+ }
}
analyses = analysesBuilder.build();
@@ -1548,11 +1554,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(
@@ -1606,18 +1607,12 @@
// 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.
- pendingReflectiveUses.add(context);
- } else if (invokedMethod == dexItemFactory.enumMembers.valueOf
- || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
- // See comment in handleEnumValueOfOrCollectionInstantiation.
- pendingReflectiveUses.add(context);
+ addMethodThatRequireIrAnalysis(context);
} else if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
- pendingReflectiveUses.add(context);
+ addMethodThatRequireIrAnalysis(context);
} else if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) {
// Handling of application services.
- pendingReflectiveUses.add(context);
- } else if (EnqueuerMockitoSupport.isReflectiveMockInvoke(dexItemFactory, invokedMethod)) {
- pendingReflectiveUses.add(context);
+ addMethodThatRequireIrAnalysis(context);
}
markTypeAsLive(invokedMethod.getHolderType(), context);
MethodResolutionResult resolutionResult =
@@ -1647,7 +1642,6 @@
invokedMethod, context, registry, KeepReason.invokedFromLambdaCreatedIn(context));
}
- @SuppressWarnings("ReferenceEquality")
private void traceInvokeVirtual(
DexMethod invokedMethod,
ProgramMethod context,
@@ -1656,14 +1650,15 @@
if (registry != null && !registry.markInvokeVirtualAsSeen(invokedMethod)) {
return;
}
- if (invokedMethod == appView.dexItemFactory().classMethods.newInstance
- || invokedMethod == appView.dexItemFactory().constructorMethods.newInstance) {
- pendingReflectiveUses.add(context);
- } else if (appView.dexItemFactory().classMethods.isReflectiveMemberLookup(invokedMethod)) {
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.newInstance)
+ || invokedMethod.isIdenticalTo(dexItemFactory.constructorMethods.newInstance)) {
+ addMethodThatRequireIrAnalysis(context);
+ } 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.
- pendingReflectiveUses.add(context);
+ addMethodThatRequireIrAnalysis(context);
}
markTypeAsLive(invokedMethod.getHolderType(), context);
MethodResolutionResult resolutionResult =
@@ -3731,7 +3726,7 @@
.createMethod(enumClass.type, proto, appView.dexItemFactory().createString("values"));
}
- private void markEnumValuesAsReachable(DexProgramClass clazz, KeepReason reason) {
+ void markEnumValuesAsReachable(DexProgramClass clazz, KeepReason reason) {
ProgramMethod valuesMethod = clazz.lookupProgramMethod(generatedEnumValuesMethod(clazz));
if (valuesMethod != null) {
// TODO(sgjesse): Does this have to be enqueued as a root item? Right now it is done as the
@@ -4750,10 +4745,10 @@
// Continue fix-point processing while there are additional work items to ensure items that
// are passed to Java reflections are traced.
- if (!pendingReflectiveUses.isEmpty()) {
+ if (!methodsThatRequireIrAnalysis.isEmpty()) {
timing.begin("Handle reflective behavior");
- pendingReflectiveUses.forEach(this::handleReflectiveBehavior);
- pendingReflectiveUses.clear();
+ methodsThatRequireIrAnalysis.forEach(this::handleReflectiveBehavior);
+ methodsThatRequireIrAnalysis.clear();
timing.end();
}
if (worklist.hasNext()) {
@@ -5219,38 +5214,33 @@
method, method, graphReporter.reportCompatKeepMethod(method));
}
+ public void addMethodThatRequireIrAnalysis(ProgramMethod programMethod) {
+ methodsThatRequireIrAnalysis.add(programMethod);
+ }
+
private void handleReflectiveBehavior(ProgramMethod method) {
IRCode code = method.buildIR(appView, MethodConversionOptions.nonConverting());
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;
}
@@ -5258,10 +5248,10 @@
handleServiceLoaderInvocation(method, invoke);
return;
}
- if (EnqueuerMockitoSupport.isReflectiveMockInvoke(appView.dexItemFactory(), invokedMethod)) {
- EnqueuerMockitoSupport.handleReflectiveMockInvoke(appView, keepInfo, method, invoke);
+ if (analyses.handleReflectiveInvoke(method, invoke)) {
return;
}
+
if (!isReflectionMethod(dexItemFactory, invokedMethod)) {
return;
}
@@ -5573,47 +5563,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/EnqueuerEnumReflectionAnalysisAndroid.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisAndroid.java
new file mode 100644
index 0000000..7e7735a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisAndroid.java
@@ -0,0 +1,149 @@
+// 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.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+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.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+
+/**
+ * Finds Enum.values() methods that must be kept due to uses of common Android serialization APIs.
+ *
+ * <p>Looks for:
+ *
+ * <ul>
+ * <li>Parcel.readSerializable()
+ * <li>Bundle.getSerializable()
+ * <li>Intent.getSerializableExtra()
+ * </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 EnqueuerEnumReflectionAnalysisAndroid extends EnqueuerEnumReflectionAnalysisBase {
+ private final DexType androidContentIntentType;
+ private final DexType androidOsParcelType;
+ private final DexMethod intentGetSerializableExtra1;
+ private final DexMethod intentGetSerializableExtra2;
+ private final DexMethod bundleGetSerializable1;
+ private final DexMethod bundleGetSerializable2;
+ private final DexMethod parcelReadSerializable1;
+ private final DexMethod parcelReadSerializable2;
+
+ public EnqueuerEnumReflectionAnalysisAndroid(
+ AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+ super(appView, enqueuer);
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+
+ androidContentIntentType = dexItemFactory.createType("Landroid/content/Intent;");
+ androidOsParcelType = dexItemFactory.createType("Landroid/os/Parcel;");
+ 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");
+ }
+
+ @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)) {
+ enqueuer.addMethodThatRequireIrAnalysis(context);
+ }
+ }
+
+ @Override
+ public boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke) {
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ if (invokedMethod.isIdenticalTo(bundleGetSerializable2)
+ || invokedMethod.isIdenticalTo(parcelReadSerializable2)
+ || invokedMethod.isIdenticalTo(intentGetSerializableExtra2)) {
+ handleReflectiveEnumInvoke(method, invoke, 1);
+ return true;
+ }
+ if (invokedMethod.isIdenticalTo(bundleGetSerializable1)
+ || invokedMethod.isIdenticalTo(parcelReadSerializable1)
+ || invokedMethod.isIdenticalTo(intentGetSerializableExtra1)) {
+ handleEnumCastFromSerializable(method, invoke.outValue());
+ return true;
+ }
+ return false;
+ }
+
+ private void handleEnumCastFromSerializable(ProgramMethod context, Value value) {
+ // These forms do not take a Class parameter, but are often followed by a checked-cast to their
+ // concrete type.
+ // There is no checked-cast when the return value is used as an Object (e.g. toString(), or
+ // inserted into a List<Object>).
+ // Does not identify when the above methods are wrapped in a helper method
+ // (e.g. a "IntentUtils.safeGetSerializableExtra()").
+ // Does not find enums nested in Serializable types. E.g., and ArrayList of enums.
+ 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) {
+ enqueuer.markEnumValuesAsReachable(enumClass, KeepReason.invokedFrom(context));
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisBase.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisBase.java
new file mode 100644
index 0000000..237959c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisBase.java
@@ -0,0 +1,93 @@
+// 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.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.analysis.EnqueuerAnalysisCollection;
+import com.android.tools.r8.graph.analysis.IrBasedEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.TraceInvokeEnqueuerAnalysis;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+
+abstract class EnqueuerEnumReflectionAnalysisBase
+ implements TraceInvokeEnqueuerAnalysis, IrBasedEnqueuerAnalysis {
+ private final AppView<? extends AppInfoWithClassHierarchy> appView;
+ protected final Enqueuer enqueuer;
+
+ public EnqueuerEnumReflectionAnalysisBase(
+ AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+ this.appView = appView;
+ this.enqueuer = enqueuer;
+ }
+
+ public void register(EnqueuerAnalysisCollection.Builder builder) {
+ builder.addTraceInvokeAnalysis(this);
+ builder.addIrBasedEnqueuerAnalysis(this);
+ }
+
+ protected 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;
+ }
+
+ protected void handleReflectiveEnumInvoke(
+ ProgramMethod context, InvokeMethod invoke, int classParamIndex) {
+ // 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();
+ }
+ DexProgramClass clazz = maybeGetProgramEnumType(type, true);
+ if (clazz != null) {
+ enqueuer.markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(context));
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisJava.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisJava.java
new file mode 100644
index 0000000..1d528fb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisJava.java
@@ -0,0 +1,102 @@
+// 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.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+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.ir.code.InvokeMethod;
+
+/**
+ * Finds Enum.values() 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()
+ * </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, an instance of the Enum subclass class must be used.
+ * </ul>
+ */
+public class EnqueuerEnumReflectionAnalysisJava extends EnqueuerEnumReflectionAnalysisBase {
+ private final DexString enumSetAllOfString;
+ private final DexString enumSetNoneOfString;
+ private final DexString enumSetRangeString;
+ private final DexString enumSetOfString;
+ private final DexType javaUtilEnumMapType;
+ private final DexType javaUtilEnumSetType;
+ private final DexMethod enumValueOfMethod;
+ private final DexMethod enumMapConstructor;
+
+ public EnqueuerEnumReflectionAnalysisJava(
+ AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+ super(appView, enqueuer);
+
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ enumSetAllOfString = dexItemFactory.createString("allOf");
+ enumSetNoneOfString = dexItemFactory.createString("noneOf");
+ enumSetRangeString = dexItemFactory.createString("range");
+ enumSetOfString = dexItemFactory.ofMethodName;
+ javaUtilEnumMapType = dexItemFactory.createType("Ljava/util/EnumMap;");
+ javaUtilEnumSetType = dexItemFactory.createType("Ljava/util/EnumSet;");
+ enumValueOfMethod = dexItemFactory.enumMembers.valueOf;
+ enumMapConstructor =
+ dexItemFactory.createInstanceInitializer(javaUtilEnumMapType, dexItemFactory.classType);
+ }
+
+ private boolean isEnumSetFactoryMethod(DexMethod invokedMethod) {
+ if (!invokedMethod.getHolderType().isIdenticalTo(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)) {
+ enqueuer.addMethodThatRequireIrAnalysis(context);
+ }
+ }
+
+ @Override
+ public void traceInvokeDirect(
+ DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+ // EnumMap uses reflection.
+ if (invokedMethod.isIdenticalTo(enumMapConstructor)) {
+ enqueuer.addMethodThatRequireIrAnalysis(context);
+ }
+ }
+
+ @Override
+ public boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke) {
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ if (invokedMethod.isIdenticalTo(enumValueOfMethod)
+ || invokedMethod.isIdenticalTo(enumMapConstructor)
+ || isEnumSetFactoryMethod(invokedMethod)) {
+ handleReflectiveEnumInvoke(method, invoke, 0);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
new file mode 100644
index 0000000..6b1ae52
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
@@ -0,0 +1,126 @@
+// 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.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.IrBasedEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.TraceInvokeEnqueuerAnalysis;
+import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+
+/** Ensure classes passed to Mockito.mock() and Mockito.spy() are not marked as "final". */
+class EnqueuerMockitoAnalysis implements TraceInvokeEnqueuerAnalysis, IrBasedEnqueuerAnalysis {
+
+ private final AppView<? extends AppInfoWithClassHierarchy> appView;
+ private final Enqueuer enqueuer;
+
+ public final DexType mockitoType;
+ public final DexString mockString;
+ public final DexString spyString;
+
+ public EnqueuerMockitoAnalysis(
+ AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+ this.appView = appView;
+ this.enqueuer = enqueuer;
+
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ mockitoType = dexItemFactory.createType("Lorg/mockito/Mockito;");
+ mockString = dexItemFactory.createString("mock");
+ spyString = dexItemFactory.createString("spy");
+ }
+
+ public static void register(
+ AppView<? extends AppInfoWithClassHierarchy> appView,
+ Enqueuer enqueuer,
+ EnqueuerAnalysisCollection.Builder builder) {
+ EnqueuerMockitoAnalysis instance = new EnqueuerMockitoAnalysis(appView, enqueuer);
+ builder.addTraceInvokeAnalysis(instance);
+ builder.addIrBasedEnqueuerAnalysis(instance);
+ }
+
+ private boolean isReflectiveMockInvoke(DexMethod invokedMethod) {
+ return invokedMethod.holder.isIdenticalTo(mockitoType)
+ && (invokedMethod.getName().isIdenticalTo(mockString)
+ || invokedMethod.getName().isIdenticalTo(spyString));
+ }
+
+ @Override
+ public void traceInvokeStatic(
+ DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+ if (isReflectiveMockInvoke(invokedMethod)) {
+ enqueuer.addMethodThatRequireIrAnalysis(context);
+ }
+ }
+
+ @Override
+ public boolean handleReflectiveInvoke(ProgramMethod context, InvokeMethod invoke) {
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+
+ if (!isReflectiveMockInvoke(invokedMethod)) {
+ return false;
+ }
+
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ DexType mockedType;
+ if (invokedMethod.getParameter(0).isIdenticalTo(dexItemFactory.classType)) {
+ // Given an explicit const-cast
+ Value classValue = invoke.getFirstArgument();
+ if (!classValue.isConstClass()) {
+ return true;
+ }
+ mockedType = classValue.getDefinition().asConstClass().getType();
+ } else if (invokedMethod.getParameter(invokedMethod.getArity() - 1).isArrayType()) {
+ // This should always be an empty array of the mocked type.
+ Value arrayValue = invoke.getLastArgument();
+ ArrayTypeElement arrayType = arrayValue.getType().asArrayType();
+ if (arrayType == null) {
+ // Should never happen.
+ return true;
+ }
+ ClassTypeElement memberType = arrayType.getMemberType().asClassType();
+ if (memberType == null) {
+ return true;
+ }
+ mockedType = memberType.getClassType();
+ } else {
+ // Should be Mockito.spy(Object).
+ if (invokedMethod.getArity() != 1
+ || !invokedMethod.getParameter(0).isIdenticalTo(dexItemFactory.objectType)) {
+ return true;
+ }
+ Value objectValue = invoke.getFirstArgument();
+ if (objectValue == null || objectValue.isPhi()) {
+ return true;
+ }
+ ClassTypeElement classType = objectValue.getType().asClassType();
+ if (classType == null) {
+ return true;
+ }
+ mockedType = classType.toDexType(dexItemFactory);
+ }
+
+ DexClass dexClass = appView.definitionFor(mockedType, context);
+ if (dexClass == null || !dexClass.isProgramClass()) {
+ return true;
+ }
+
+ // Make sure the type is not made final so that it can still be subclassed by Mockito.
+ enqueuer
+ .getKeepInfo()
+ .joinClass(dexClass.asProgramClass(), joiner -> joiner.disallowOptimization());
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoSupport.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoSupport.java
deleted file mode 100644
index 19be910..0000000
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoSupport.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// 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.DexClass;
-import com.android.tools.r8.graph.DexDefinitionSupplier;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.shaking.KeepInfoCollection.MutableKeepInfoCollection;
-
-class EnqueuerMockitoSupport {
-
- static boolean isReflectiveMockInvoke(DexItemFactory dexItemFactory, DexMethod invokedMethod) {
- return invokedMethod.holder.isIdenticalTo(dexItemFactory.mockitoType)
- && (invokedMethod.getName().isIdenticalTo(dexItemFactory.mockString)
- || invokedMethod.getName().isIdenticalTo(dexItemFactory.spyString));
- }
-
- /** Ensure classes passed to Mockito.mock() and Mockito.spy() are not marked as "final". */
- static void handleReflectiveMockInvoke(
- DexDefinitionSupplier appView,
- MutableKeepInfoCollection keepInfo,
- ProgramMethod context,
- InvokeMethod invoke) {
- DexMethod method = invoke.getInvokedMethod();
- DexItemFactory dexItemFactory = appView.dexItemFactory();
-
- DexType mockedType;
- if (method.getParameter(0).isIdenticalTo(dexItemFactory.classType)) {
- // Given an explicit const-cast
- Value classValue = invoke.getFirstArgument();
- if (!classValue.isConstClass()) {
- return;
- }
- mockedType = classValue.getDefinition().asConstClass().getType();
- } else if (method.getParameter(method.getArity() - 1).isArrayType()) {
- // This should always be an empty array of the mocked type.
- Value arrayValue = invoke.getLastArgument();
- ArrayTypeElement arrayType = arrayValue.getType().asArrayType();
- if (arrayType == null) {
- // Should never happen.
- return;
- }
- ClassTypeElement memberType = arrayType.getMemberType().asClassType();
- if (memberType == null) {
- return;
- }
- mockedType = memberType.getClassType();
- } else {
- // Should be Mockito.spy(Object).
- if (method.getArity() != 1
- || !method.getParameter(0).isIdenticalTo(dexItemFactory.objectType)) {
- return;
- }
- Value objectValue = invoke.getFirstArgument();
- if (objectValue == null || objectValue.isPhi()) {
- return;
- }
- ClassTypeElement classType = objectValue.getType().asClassType();
- if (classType == null) {
- return;
- }
- mockedType = classType.toDexType(dexItemFactory);
- }
-
- DexClass dexClass = appView.definitionFor(mockedType, context);
- if (dexClass == null || !dexClass.isProgramClass()) {
- return;
- }
-
- // Make sure the type is not made final so that it can still be subclassed by Mockito.
- keepInfo.joinClass(dexClass.asProgramClass(), joiner -> joiner.disallowOptimization());
- }
-}
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 64de864..b408a09 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 experimentalTraceAndroidEnumSerialization =
+ System.getProperty("com.android.tools.r8.experimentalTraceAndroidEnumSerialization") != 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/shaking/enums/EnumReflectionAndroidTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionAndroidTest.java
new file mode 100644
index 0000000..83921be
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionAndroidTest.java
@@ -0,0 +1,323 @@
+// 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.EnumReflectionAndroidTest.Helpers.EnumA;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumB;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumC;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumD;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumE;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumF;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumG;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumH;
+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.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 Android-specific APIs that cause an Enum type's values() method to be kept. */
+@RunWith(Parameterized.class)
+public class EnumReflectionAndroidTest 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(
+ "bundle: A",
+ "bundle: B",
+ "parcel: C",
+ "parcel: D",
+ "intent: E",
+ "intent: F",
+ "array: [G]",
+ "array: [H]");
+
+ 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,
+ }
+
+ public enum EnumF {
+ F,
+ G,
+ }
+
+ public enum EnumG {
+ G {},
+ H,
+ }
+
+ public enum EnumH {
+ H,
+ I,
+ }
+ }
+
+ public static class TestMain {
+ @NeverInline
+ private static void androidBundle1() {
+ FakeBundle b = FakeBundle.createWithSingleSerializable(EnumA.A);
+ EnumA result = (EnumA) b.getSerializable("");
+ System.out.println("bundle: " + result);
+ }
+
+ @NeverInline
+ private static void androidBundle2() {
+ FakeBundle b = FakeBundle.createWithSingleSerializable(EnumB.B);
+ EnumB result = b.getSerializable("", EnumB.class);
+ System.out.println("bundle: " + result);
+ }
+
+ @NeverInline
+ private static void androidParcel1() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(EnumC.C);
+ EnumC result = (EnumC) p.readSerializable();
+ System.out.println("parcel: " + result);
+ }
+
+ @NeverInline
+ private static void androidParcel2() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(EnumD.D);
+ System.out.println("parcel: " + p.readSerializable(null, EnumD.class));
+ }
+
+ @NeverInline
+ private static void androidIntent1() {
+ FakeIntent i = FakeIntent.createWithSingleSerializable(EnumE.E);
+ EnumE result = (EnumE) i.getSerializableExtra("");
+ System.out.println("intent: " + result);
+ }
+
+ @NeverInline
+ private static void androidIntent2() {
+ FakeIntent i = FakeIntent.createWithSingleSerializable(EnumF.F);
+ System.out.println("intent: " + i.getSerializableExtra("", EnumF.class));
+ }
+
+ @NeverInline
+ private static void array1() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumG[] {EnumG.G});
+ EnumG[] result = (EnumG[]) p.readSerializable();
+ System.out.println("array: " + Arrays.toString(result));
+ }
+
+ @NeverInline
+ private static void array2() {
+ FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumH[] {EnumH.H});
+ System.out.println("array: " + Arrays.toString(p.readSerializable(null, EnumH[].class)));
+ }
+
+ public static void main(String[] args) {
+ // Use different methods to ensure Enqueuer.traceInvokeStatic() triggers for each one.
+ androidBundle1();
+ androidBundle2();
+ androidParcel1();
+ androidParcel2();
+ androidIntent1();
+ androidIntent2();
+ array1();
+ array2();
+ }
+ }
+
+ 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;";
+ // EnumE.E and EnumG.G references this class in its constructor.
+ private static final String ENUM_SUBTYPE_BRIDGE_CLASS_NAME =
+ EnumReflectionAndroidTest.class.getName() + "$1";
+
+ 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.experimentalTraceAndroidEnumSerialization = 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);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionJavaTest.java
similarity index 72%
rename from src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
rename to src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionJavaTest.java
index 2a9872c..9c8f0cd 100644
--- a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionJavaTest.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// 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.
@@ -8,6 +8,17 @@
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.EnumReflectionJavaTest.Helpers.EnumA;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumB;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumC;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumD;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumE;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumF;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumG;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumH;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumI;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumJ;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumK;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
@@ -18,8 +29,9 @@
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
+/** Tests for the non-Android-specific APIs that cause an Enum type's values() method to be kept. */
@RunWith(Parameterized.class)
-public class EnumCollectionsTest extends TestBase {
+public class EnumReflectionJavaTest extends TestBase {
@Parameter(0)
public TestParameters parameters;
@@ -44,7 +56,8 @@
"valueOf: K",
"phi: [B]");
- public static class TestMain {
+ public static class Helpers {
+
public enum EnumA {
A,
B
@@ -56,7 +69,7 @@
}
public enum EnumC {
- C,
+ C {}, // Test anonymous subtype.
D
}
@@ -109,7 +122,9 @@
K,
L
}
+ }
+ public static class TestMain {
@NeverInline
private static void noneOf() {
System.out.println("none: " + EnumSet.complementOf(EnumSet.noneOf(EnumA.class)));
@@ -186,10 +201,16 @@
}
}
+ // EnumC.C references this class in its constructor.
+ private static final String ENUM_SUBTYPE_BRIDGE_CLASS_NAME =
+ EnumReflectionJavaTest.class.getName() + "$1";
+
@Test
public void testRuntime() throws Exception {
testForRuntime(parameters)
- .addProgramClassesAndInnerClasses(TestMain.class)
+ .addProgramClassesAndInnerClasses(Helpers.class)
+ .addProgramClasses(TestMain.class)
+ .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
.run(parameters.getRuntime(), TestMain.class)
.assertSuccessWithOutputLines(EXPECTED_OUTPUT);
}
@@ -198,7 +219,9 @@
public void testR8() throws Exception {
testForR8(parameters.getBackend())
.setMinApi(parameters)
- .addProgramClassesAndInnerClasses(TestMain.class)
+ .addProgramClassesAndInnerClasses(Helpers.class)
+ .addProgramClasses(TestMain.class)
+ .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
.enableInliningAnnotations()
.addKeepMainRule(TestMain.class)
.compile()