blob: 0b39506a9ef0925cb8134b92a26678d02124fc62 [file] [log] [blame]
// Copyright (c) 2020, 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.ir.optimize.enums;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMember;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
import com.android.tools.r8.ir.optimize.enums.EnumUnboxer.Reason;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.google.common.collect.Sets;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
class EnumUnboxingCandidateAnalysis {
private final AppView<AppInfoWithLiveness> appView;
private final EnumUnboxer enumUnboxer;
private final DexItemFactory factory;
private Map<DexType, Set<DexEncodedMethod>> enumToUnboxCandidates = new ConcurrentHashMap<>();
EnumUnboxingCandidateAnalysis(AppView<AppInfoWithLiveness> appView, EnumUnboxer enumUnboxer) {
this.appView = appView;
this.enumUnboxer = enumUnboxer;
factory = appView.dexItemFactory();
}
Map<DexType, Set<DexEncodedMethod>> findCandidates() {
for (DexProgramClass clazz : appView.appInfo().classes()) {
if (isEnumUnboxingCandidate(clazz)) {
enumToUnboxCandidates.put(clazz.type, Sets.newConcurrentHashSet());
}
}
removePinnedCandidates();
return enumToUnboxCandidates;
}
private boolean isEnumUnboxingCandidate(DexProgramClass clazz) {
if (!clazz.isEnum()) {
return false;
}
if (!clazz.isEffectivelyFinal(appView)) {
enumUnboxer.reportFailure(clazz.type, Reason.SUBTYPES);
return false;
}
// TODO(b/147860220): interfaces without default methods should be acceptable if the build setup
// is correct (all abstract methods are implemented).
if (!clazz.interfaces.isEmpty()) {
enumUnboxer.reportFailure(clazz.type, Reason.INTERFACE);
return false;
}
if (!clazz.instanceFields().isEmpty()) {
enumUnboxer.reportFailure(clazz.type, Reason.INSTANCE_FIELD);
return false;
}
if (!enumHasBasicStaticFields(clazz)) {
enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_STATIC_FIELD);
return false;
}
if (!clazz.virtualMethods().isEmpty()) {
enumUnboxer.reportFailure(clazz.type, Reason.VIRTUAL_METHOD);
return false;
}
EnumValueInfoMap enumValueInfoMap =
appView.appInfo().withLiveness().getEnumValueInfoMap(clazz.type);
if (enumValueInfoMap == null) {
enumUnboxer.reportFailure(clazz.type, Reason.MISSING_INFO_MAP);
return false;
}
// Methods values, valueOf, init, clinit are present on each enum.
// Methods init and clinit are required if the enum is used.
// Methods valueOf and values are normally kept by the commonly used/recommended enum keep rule
// -keepclassmembers,allowoptimization enum * {
// public static **[] values();
// public static ** valueOf(java.lang.String);
// }
// In general there will be 4 methods, unless the enum keep rule is not present.
if (clazz.directMethods().size() > 4) {
enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_DIRECT_METHOD);
return false;
}
for (DexEncodedMethod directMethod : clazz.directMethods()) {
if (!(factory.enumMethods.isValuesMethod(directMethod.method, clazz)
|| factory.enumMethods.isValueOfMethod(directMethod.method, clazz)
|| isStandardEnumInitializer(directMethod)
|| directMethod.isClassInitializer())) {
enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_DIRECT_METHOD);
return false;
}
}
return true;
}
private boolean isStandardEnumInitializer(DexEncodedMethod method) {
return method.isInstanceInitializer()
&& method.method.proto == factory.enumMethods.constructor.proto;
}
// The enum should have the $VALUES static field and only fields directly referencing the enum
// instances.
private boolean enumHasBasicStaticFields(DexProgramClass clazz) {
for (DexEncodedField staticField : clazz.staticFields()) {
if (staticField.field.type == clazz.type
&& staticField.accessFlags.isEnum()
&& staticField.accessFlags.isFinal()) {
// Enum field, valid, do nothing.
} else if (staticField.field.type.isArrayType()
&& staticField.field.type.toArrayElementType(factory) == clazz.type
&& staticField.accessFlags.isSynthetic()
&& staticField.accessFlags.isFinal()
&& staticField.field.name == factory.enumValuesFieldName) {
// Field $VALUES, valid, do nothing.
} else {
return false;
}
}
return true;
}
private void removePinnedCandidates() {
// A holder type, for field or method, should block enum unboxing only if the enum type is
// also kept. This is to allow the keep rule -keepclassmembers to be used on enums while
// enum unboxing can still be performed.
for (DexReference item : appView.appInfo().getPinnedItems()) {
if (item.isDexType()) {
removePinnedCandidate(item.asDexType());
} else if (item.isDexField()) {
DexField field = item.asDexField();
removePinnedIfNotHolder(field, field.type);
} else {
assert item.isDexMethod();
DexMethod method = item.asDexMethod();
DexProto proto = method.proto;
removePinnedIfNotHolder(method, proto.returnType);
for (DexType parameterType : proto.parameters.values) {
removePinnedIfNotHolder(method, parameterType);
}
}
}
}
private void removePinnedIfNotHolder(DexMember<?, ?> member, DexType type) {
if (type != member.holder) {
removePinnedCandidate(type);
}
}
private void removePinnedCandidate(DexType type) {
if (enumToUnboxCandidates.containsKey(type)) {
enumUnboxer.reportFailure(type, Reason.PINNED);
enumToUnboxCandidates.remove(type);
}
}
}