| // Copyright (c) 2016, 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 static com.android.tools.r8.graph.FieldAccessInfoImpl.MISSING_FIELD_ACCESS_INFO; |
| import static com.android.tools.r8.naming.IdentifierNameStringUtils.identifyIdentifier; |
| import static com.android.tools.r8.naming.IdentifierNameStringUtils.isReflectionMethod; |
| import static com.android.tools.r8.shaking.AnnotationRemover.shouldKeepAnnotation; |
| import static com.android.tools.r8.shaking.EnqueuerUtils.toImmutableSortedMap; |
| import static com.google.common.base.Predicates.not; |
| |
| import com.android.tools.r8.Diagnostic; |
| import com.android.tools.r8.dex.IndexedItemCollection; |
| import com.android.tools.r8.errors.Unimplemented; |
| import com.android.tools.r8.errors.Unreachable; |
| import com.android.tools.r8.experimental.graphinfo.AnnotationGraphNode; |
| import com.android.tools.r8.experimental.graphinfo.ClassGraphNode; |
| import com.android.tools.r8.experimental.graphinfo.FieldGraphNode; |
| import com.android.tools.r8.experimental.graphinfo.GraphConsumer; |
| import com.android.tools.r8.experimental.graphinfo.GraphEdgeInfo; |
| import com.android.tools.r8.experimental.graphinfo.GraphEdgeInfo.EdgeKind; |
| import com.android.tools.r8.experimental.graphinfo.GraphNode; |
| import com.android.tools.r8.experimental.graphinfo.KeepRuleGraphNode; |
| import com.android.tools.r8.experimental.graphinfo.MethodGraphNode; |
| import com.android.tools.r8.graph.AppInfoWithSubtyping; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.Descriptor; |
| import com.android.tools.r8.graph.DexAnnotation; |
| import com.android.tools.r8.graph.DexCallSite; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexDefinition; |
| 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.DexItem; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexLibraryClass; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexMethodHandle; |
| 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.DexString; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl; |
| import com.android.tools.r8.graph.FieldAccessInfoImpl; |
| import com.android.tools.r8.graph.KeyedDexItem; |
| import com.android.tools.r8.graph.PresortedComparable; |
| import com.android.tools.r8.graph.ResolutionResult; |
| import com.android.tools.r8.graph.analysis.EnqueuerAnalysis; |
| import com.android.tools.r8.ir.analysis.proto.schema.ProtoEnqueuerExtension; |
| import com.android.tools.r8.ir.code.ArrayPut; |
| import com.android.tools.r8.ir.code.ConstantValueUtils; |
| 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.InvokeVirtual; |
| import com.android.tools.r8.ir.code.Value; |
| import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter; |
| import com.android.tools.r8.ir.desugar.LambdaDescriptor; |
| import com.android.tools.r8.logging.Log; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.references.Reference; |
| import com.android.tools.r8.references.TypeReference; |
| import com.android.tools.r8.shaking.EnqueuerWorklist.Action; |
| import com.android.tools.r8.shaking.RootSetBuilder.ConsequentRootSet; |
| import com.android.tools.r8.shaking.RootSetBuilder.RootSet; |
| import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult; |
| import com.android.tools.r8.utils.DequeUtils; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.SetUtils; |
| import com.android.tools.r8.utils.StringDiagnostic; |
| import com.android.tools.r8.utils.Timing; |
| import com.google.common.base.Equivalence.Wrapper; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableList.Builder; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.Sets.SetView; |
| import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap; |
| import it.unimi.dsi.fastutil.objects.Object2BooleanMap; |
| import java.lang.reflect.InvocationHandler; |
| import java.util.ArrayDeque; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Deque; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.function.BiConsumer; |
| import java.util.function.BiPredicate; |
| import java.util.function.Function; |
| import java.util.function.Predicate; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Approximates the runtime dependencies for the given set of roots. |
| * <p> |
| * <p>The implementation filters the static call-graph with liveness information on classes to |
| * remove virtual methods that are reachable by their static type but are unreachable at runtime as |
| * they are not visible from any instance. |
| * <p> |
| * <p>As result of the analysis, an instance of {@link AppInfoWithLiveness} is returned. See the |
| * field descriptions for details. |
| */ |
| public class Enqueuer { |
| |
| public enum Mode { |
| INITIAL_TREE_SHAKING, |
| FINAL_TREE_SHAKING, |
| MAIN_DEX_TRACING, |
| WHY_ARE_YOU_KEEPING; |
| |
| public boolean isInitialTreeShaking() { |
| return this == INITIAL_TREE_SHAKING; |
| } |
| |
| public boolean isFinalTreeShaking() { |
| return this == FINAL_TREE_SHAKING; |
| } |
| |
| public boolean isInitialOrFinalTreeShaking() { |
| return isInitialTreeShaking() || isFinalTreeShaking(); |
| } |
| |
| public boolean isTracingMainDex() { |
| return this == MAIN_DEX_TRACING; |
| } |
| } |
| |
| private final boolean forceProguardCompatibility; |
| private final Mode mode; |
| |
| private Set<EnqueuerAnalysis> analyses = Sets.newIdentityHashSet(); |
| private final AppInfoWithSubtyping appInfo; |
| private final AppView<? extends AppInfoWithSubtyping> appView; |
| private final InternalOptions options; |
| private RootSet rootSet; |
| private ProguardClassFilter dontWarnPatterns; |
| |
| private final Map<DexMethod, Set<DexEncodedMethod>> virtualInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> interfaceInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> superInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> directInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> staticInvokes = new IdentityHashMap<>(); |
| private final FieldAccessInfoCollectionImpl fieldAccessInfoCollection = |
| new FieldAccessInfoCollectionImpl(); |
| private final Set<DexField> instanceFieldsWrittenOutsideEnclosingInstanceInitializers = |
| Sets.newIdentityHashSet(); |
| private final Set<DexField> staticFieldsWrittenOutsideEnclosingStaticInitializer = |
| Sets.newIdentityHashSet(); |
| private final Set<DexCallSite> callSites = Sets.newIdentityHashSet(); |
| |
| private final Set<DexReference> identifierNameStrings = Sets.newIdentityHashSet(); |
| |
| // Canonicalization of external graph-nodes and edge info. |
| private final Map<DexItem, AnnotationGraphNode> annotationNodes = new IdentityHashMap<>(); |
| private final Map<DexType, ClassGraphNode> classNodes = new IdentityHashMap<>(); |
| private final Map<DexMethod, MethodGraphNode> methodNodes = new IdentityHashMap<>(); |
| private final Map<DexField, FieldGraphNode> fieldNodes = new IdentityHashMap<>(); |
| private final Map<ProguardKeepRuleBase, KeepRuleGraphNode> ruleNodes = new IdentityHashMap<>(); |
| private final Map<EdgeKind, GraphEdgeInfo> reasonInfo = new IdentityHashMap<>(); |
| |
| /** |
| * Set of method signatures used in invoke-super instructions that either cannot be resolved or |
| * resolve to a private method (leading to an IllegalAccessError). |
| */ |
| private final Set<DexMethod> brokenSuperInvokes = Sets.newIdentityHashSet(); |
| /** |
| * This map keeps a view of all virtual methods that are reachable from virtual invokes. A method |
| * is reachable even if no live subtypes exist, so this is not sufficient for inclusion in the |
| * live set. |
| */ |
| private final Map<DexProgramClass, SetWithStoredReason<DexEncodedMethod>> |
| reachableVirtualMethods = Maps.newIdentityHashMap(); |
| |
| /** |
| * Tracks the dependency between a method and the super-method it calls, if any. Used to make |
| * super methods become live when they become reachable from a live sub-method. |
| */ |
| private final Map<DexEncodedMethod, Set<DexEncodedMethod>> superInvokeDependencies = Maps |
| .newIdentityHashMap(); |
| /** Set of instance fields that can be reached by read/write operations. */ |
| private final Map<DexProgramClass, SetWithReason<DexEncodedField>> reachableInstanceFields = |
| Maps.newIdentityHashMap(); |
| |
| /** |
| * Set of types that are mentioned in the program. We at least need an empty abstract class item |
| * for these. |
| */ |
| private final SetWithReason<DexProgramClass> liveTypes = new SetWithReason<>(this::registerClass); |
| |
| /** Set of live types defined in the library and classpath. Used to avoid duplicate tracing. */ |
| private final Set<DexClass> liveNonProgramTypes = Sets.newIdentityHashSet(); |
| |
| /** |
| * Set of proto extension types that are technically live, but which we have not traced because |
| * they are dead according to the generated extension registry shrinker. |
| * |
| * <p>Only used if {@link InternalOptions#enableGeneratedExtensionRegistryShrinking} is set. |
| */ |
| private final Set<DexType> skippedProtoExtensionTypes = Sets.newIdentityHashSet(); |
| |
| /** Set of annotation types that are instantiated. */ |
| private final SetWithReason<DexAnnotation> liveAnnotations = |
| new SetWithReason<>(this::registerAnnotation); |
| |
| /** Set of types that are actually instantiated. These cannot be abstract. */ |
| private final SetWithReason<DexProgramClass> instantiatedTypes = |
| new SetWithReason<>(this::registerClass); |
| |
| /** Set of all types that are instantiated, directly or indirectly, thus may be abstract. */ |
| private final Set<DexProgramClass> directAndIndirectlyInstantiatedTypes = |
| Sets.newIdentityHashSet(); |
| |
| /** |
| * Set of methods that are the immediate target of an invoke. They might not actually be live but |
| * are required so that invokes can find the method. If a method is only a target but not live, |
| * its implementation may be removed and it may be marked abstract. |
| */ |
| private final SetWithReason<DexEncodedMethod> targetedMethods = |
| new SetWithReason<>(this::registerMethod); |
| /** |
| * Set of program methods that are used as the bootstrap method for an invoke-dynamic instruction. |
| */ |
| private final Set<DexMethod> bootstrapMethods = Sets.newIdentityHashSet(); |
| /** |
| * Set of direct methods that are the immediate target of an invoke-dynamic. |
| */ |
| private final Set<DexMethod> methodsTargetedByInvokeDynamic = Sets.newIdentityHashSet(); |
| /** |
| * Set of direct lambda methods that are the immediate target of an invoke-dynamic. |
| */ |
| private final Set<DexMethod> lambdaMethodsTargetedByInvokeDynamic = Sets.newIdentityHashSet(); |
| /** |
| * Set of virtual methods that are the immediate target of an invoke-direct. |
| * */ |
| private final Set<DexMethod> virtualMethodsTargetedByInvokeDirect = Sets.newIdentityHashSet(); |
| /** |
| * Set of methods that belong to live classes and can be reached by invokes. These need to be |
| * kept. |
| */ |
| private final SetWithReason<DexEncodedMethod> liveMethods = |
| new SetWithReason<>(this::registerMethod); |
| |
| /** |
| * Set of fields that belong to live classes and can be reached by invokes. These need to be kept. |
| */ |
| private final SetWithReason<DexEncodedField> liveFields = |
| new SetWithReason<>(this::registerField); |
| |
| /** |
| * Set of service types (from META-INF/services/) that may have been instantiated reflectively via |
| * ServiceLoader.load() or ServiceLoader.loadInstalled(). |
| */ |
| private final Set<DexType> instantiatedAppServices = Sets.newIdentityHashSet(); |
| |
| /** |
| * Set of interface types for which there may be instantiations, such as lambda expressions or |
| * explicit keep rules. |
| */ |
| private final SetWithReason<DexProgramClass> instantiatedInterfaceTypes = |
| new SetWithReason<>(this::registerInterface); |
| |
| /** A queue of items that need processing. Different items trigger different actions. */ |
| private final EnqueuerWorklist workList; |
| |
| /** A queue of items that have been added to try to keep Proguard compatibility. */ |
| private final EnqueuerWorklist proguardCompatibilityWorkList; |
| |
| /** |
| * A set of methods that need code inspection for Java reflection in use. |
| */ |
| private final Set<DexEncodedMethod> pendingReflectiveUses = Sets.newLinkedHashSet(); |
| |
| /** A cache for DexMethod that have been marked reachable. */ |
| private final Map<DexMethod, DexEncodedMethod> virtualTargetsMarkedAsReachable = |
| Maps.newIdentityHashMap(); |
| |
| /** |
| * A set of references we have reported missing to dedupe warnings. |
| */ |
| private final Set<DexReference> reportedMissing = Sets.newIdentityHashSet(); |
| |
| /** |
| * A set of references that we are keeping due to keep rules. This may differ from the root set |
| * due to dependent keep rules. |
| */ |
| private final Set<DexReference> pinnedItems = Sets.newIdentityHashSet(); |
| |
| /** |
| * A map from classes to annotations that need to be processed should the classes ever become |
| * live. |
| */ |
| private final Map<DexType, Set<DexAnnotation>> deferredAnnotations = new IdentityHashMap<>(); |
| |
| /** |
| * Set of keep rules generated for Proguard compatibility in Proguard compatibility mode. |
| */ |
| private final ProguardConfiguration.Builder compatibility; |
| |
| /** Map of active if rules to speed up aapt2 generated keep rules. */ |
| private Map<Wrapper<ProguardIfRule>, Set<ProguardIfRule>> activeIfRules; |
| |
| /** |
| * A cache of ScopedDexMethodSet for each live type used for determining that virtual methods that |
| * cannot be removed because they are widening access for another virtual method defined earlier |
| * in the type hierarchy. See b/136698023 for more information. |
| */ |
| private final Map<DexType, ScopedDexMethodSet> scopedMethodsForLiveTypes = |
| new IdentityHashMap<>(); |
| |
| private final GraphReporter graphReporter; |
| private final GraphConsumer keptGraphConsumer; |
| private CollectingGraphConsumer verificationGraphConsumer = null; |
| |
| Enqueuer( |
| AppView<? extends AppInfoWithSubtyping> appView, |
| GraphConsumer keptGraphConsumer, |
| ProguardConfiguration.Builder compatibility, |
| Mode mode) { |
| assert appView.appServices() != null; |
| InternalOptions options = appView.options(); |
| this.appInfo = appView.appInfo(); |
| this.appView = appView; |
| this.compatibility = compatibility; |
| this.forceProguardCompatibility = options.forceProguardCompatibility; |
| this.graphReporter = new GraphReporter(); |
| this.keptGraphConsumer = recordKeptGraph(options, keptGraphConsumer); |
| this.mode = mode; |
| this.options = options; |
| this.workList = EnqueuerWorklist.createWorklist(appView); |
| this.proguardCompatibilityWorkList = |
| EnqueuerWorklist.createProguardCompatibilityWorklist(appView); |
| |
| if (options.enableGeneratedMessageLiteShrinking && mode.isInitialOrFinalTreeShaking()) { |
| registerAnalysis(new ProtoEnqueuerExtension(appView)); |
| } |
| } |
| |
| public Mode getMode() { |
| return mode; |
| } |
| |
| public Enqueuer registerAnalysis(EnqueuerAnalysis analysis) { |
| this.analyses.add(analysis); |
| return this; |
| } |
| |
| private boolean isProgramClass(DexType type) { |
| return getProgramClassOrNull(type) != null; |
| } |
| |
| private DexProgramClass getProgramClassOrNull(DexType type) { |
| DexClass clazz = appView.definitionFor(type); |
| if (clazz != null) { |
| if (clazz.isProgramClass()) { |
| return clazz.asProgramClass(); |
| } |
| if (liveNonProgramTypes.add(clazz) && clazz.isLibraryClass()) { |
| ensureMethodsContinueToWidenAccess(clazz); |
| warnIfLibraryTypeInheritsFromProgramType(clazz.asLibraryClass()); |
| } |
| } |
| reportMissingClass(type); |
| return null; |
| } |
| |
| private void warnIfLibraryTypeInheritsFromProgramType(DexLibraryClass clazz) { |
| if (clazz.superType != null) { |
| ensureFromLibraryOrThrow(clazz.superType, clazz); |
| } |
| for (DexType iface : clazz.interfaces.values) { |
| ensureFromLibraryOrThrow(iface, clazz); |
| } |
| } |
| |
| private Set<DexField> instanceFieldsWrittenOnlyInEnclosingInstanceInitializers() { |
| Set<DexField> result = getNonPinnedWrittenFields(not(DexEncodedField::isStatic)); |
| result.removeAll(instanceFieldsWrittenOutsideEnclosingInstanceInitializers); |
| return result; |
| } |
| |
| private Set<DexField> staticFieldsWrittenOnlyInEnclosingStaticInitializer() { |
| Set<DexField> result = getNonPinnedWrittenFields(DexEncodedField::isStatic); |
| result.removeAll(staticFieldsWrittenOutsideEnclosingStaticInitializer); |
| return result; |
| } |
| |
| private Set<DexField> getNonPinnedWrittenFields(Predicate<DexEncodedField> predicate) { |
| Set<DexField> result = Sets.newIdentityHashSet(); |
| fieldAccessInfoCollection.forEach( |
| info -> { |
| if (info == MISSING_FIELD_ACCESS_INFO) { |
| return; |
| } |
| // Note that it is safe to use definitionFor() here, and not lookupField(), since the |
| // field held by `info` is a direct reference to the definition of the field. |
| DexEncodedField encodedField = appView.definitionFor(info.getField()); |
| if (encodedField == null) { |
| assert false; |
| return; |
| } |
| if (encodedField.isProgramField(appInfo) |
| && info.isWritten() |
| && predicate.test(encodedField)) { |
| result.add(encodedField.field); |
| } |
| }); |
| result.removeAll( |
| pinnedItems.stream() |
| .filter(DexReference::isDexField) |
| .map(DexReference::asDexField) |
| .collect(Collectors.toSet())); |
| return result; |
| } |
| |
| private static <T> SetWithReason<T> newSetWithoutReasonReporter() { |
| return new SetWithReason<>((f, r) -> {}); |
| } |
| |
| private void enqueueRootItems(Map<DexReference, Set<ProguardKeepRuleBase>> items) { |
| items.entrySet().forEach(this::enqueueRootItem); |
| } |
| |
| private void enqueueRootItem(Entry<DexReference, Set<ProguardKeepRuleBase>> root) { |
| DexDefinition item = appView.definitionFor(root.getKey()); |
| if (item != null) { |
| enqueueRootItem(item, root.getValue()); |
| } else { |
| // TODO(b/123923324): Verify that root items are present. |
| // assert false : "Expected root item `" + root.getKey().toSourceString() + "` to be present"; |
| } |
| } |
| |
| private void enqueueRootItem(DexDefinition item, Set<ProguardKeepRuleBase> rules) { |
| internalEnqueueRootItem(item, rules, null); |
| } |
| |
| private void internalEnqueueRootItem( |
| DexDefinition item, Set<ProguardKeepRuleBase> rules, DexDefinition precondition) { |
| if (item.isDexClass()) { |
| DexProgramClass clazz = item.asDexClass().asProgramClass(); |
| KeepReasonWitness witness = graphReporter.reportKeepClass(precondition, rules, clazz); |
| if (clazz.isInterface() && !clazz.accessFlags.isAnnotation()) { |
| markInterfaceAsInstantiated(clazz, witness); |
| } else { |
| workList.enqueueMarkInstantiatedAction(clazz, witness); |
| if (clazz.hasDefaultInitializer()) { |
| if (forceProguardCompatibility) { |
| ProguardKeepRule compatRule = |
| ProguardConfigurationUtils.buildDefaultInitializerKeepRule(clazz); |
| proguardCompatibilityWorkList.enqueueMarkMethodKeptAction( |
| clazz.getDefaultInitializer(), |
| KeepReason.dueToProguardCompatibilityKeepRule(compatRule)); |
| } |
| if (clazz.isExternalizable(appView)) { |
| enqueueMarkMethodLiveAction(clazz, clazz.getDefaultInitializer(), witness); |
| } |
| } |
| } |
| } else if (item.isDexEncodedField()) { |
| KeepReasonWitness witness = |
| graphReporter.reportKeepField(precondition, rules, item.asDexEncodedField()); |
| workList.enqueueMarkFieldKeptAction(item.asDexEncodedField(), witness); |
| } else if (item.isDexEncodedMethod()) { |
| KeepReasonWitness witness = |
| graphReporter.reportKeepMethod(precondition, rules, item.asDexEncodedMethod()); |
| workList.enqueueMarkMethodKeptAction(item.asDexEncodedMethod(), witness); |
| } else { |
| throw new IllegalArgumentException(item.toString()); |
| } |
| pinnedItems.add(item.toReference()); |
| } |
| |
| private void markInterfaceAsInstantiated(DexProgramClass clazz, KeepReason reason) { |
| assert clazz.isInterface() && !clazz.accessFlags.isAnnotation(); |
| |
| if (!instantiatedInterfaceTypes.add(clazz, reason)) { |
| return; |
| } |
| populateInstantiatedTypesCache(clazz); |
| markTypeAsLive(clazz, reason); |
| } |
| |
| private void enqueueFirstNonSerializableClassInitializer( |
| DexProgramClass clazz, KeepReason reason) { |
| assert clazz.isSerializable(appView); |
| // Climb up the class hierarchy. Break out if the definition is not found, or hit the library |
| // classes which are kept by definition, or encounter the first non-serializable class. |
| while (clazz.isSerializable(appView)) { |
| DexProgramClass superClass = getProgramClassOrNull(clazz.superType); |
| if (superClass == null) { |
| return; |
| } |
| clazz = superClass; |
| } |
| if (clazz.hasDefaultInitializer()) { |
| enqueueMarkMethodLiveAction(clazz, clazz.getDefaultInitializer(), reason); |
| } |
| } |
| |
| // Utility to avoid adding to the worklist if already live. |
| private boolean enqueueMarkMethodLiveAction( |
| DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { |
| assert method.method.holder == clazz.type; |
| if (liveMethods.add(method, reason)) { |
| workList.enqueueMarkMethodLiveAction(clazz, method, reason); |
| return true; |
| } |
| return false; |
| } |
| |
| private void compatEnqueueHolderIfDependentNonStaticMember( |
| DexClass holder, Set<ProguardKeepRuleBase> compatRules) { |
| if (!forceProguardCompatibility || compatRules == null) { |
| return; |
| } |
| enqueueRootItem(holder, compatRules); |
| } |
| |
| // |
| // Things to do with registering events. This is essentially the interface for byte-code |
| // traversals. |
| // |
| |
| private boolean registerMethodWithTargetAndContext( |
| Map<DexMethod, Set<DexEncodedMethod>> seen, DexMethod method, DexEncodedMethod context) { |
| DexType baseHolder = method.holder.toBaseType(appView.dexItemFactory()); |
| if (baseHolder.isClassType()) { |
| markTypeAsLive(baseHolder, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); |
| return seen.computeIfAbsent(method, ignore -> Sets.newIdentityHashSet()).add(context); |
| } |
| return false; |
| } |
| |
| public boolean registerFieldRead(DexField field, DexEncodedMethod context) { |
| return registerFieldAccess(field, context, true); |
| } |
| |
| public boolean registerFieldWrite(DexField field, DexEncodedMethod context) { |
| return registerFieldAccess(field, context, false); |
| } |
| |
| public boolean registerFieldAccess(DexField field, DexEncodedMethod context) { |
| boolean changed = registerFieldAccess(field, context, true); |
| changed |= registerFieldAccess(field, context, false); |
| return changed; |
| } |
| |
| private boolean registerFieldAccess(DexField field, DexEncodedMethod context, boolean isRead) { |
| FieldAccessInfoImpl info = fieldAccessInfoCollection.get(field); |
| if (info == null) { |
| DexEncodedField encodedField = appInfo.resolveField(field); |
| |
| // If the field does not exist, then record this in the mapping, such that we don't have to |
| // resolve the field the next time. |
| if (encodedField == null) { |
| fieldAccessInfoCollection.extend(field, MISSING_FIELD_ACCESS_INFO); |
| return true; |
| } |
| |
| // Check if we have previously created a FieldAccessInfo object for the field definition. |
| info = fieldAccessInfoCollection.get(encodedField.field); |
| |
| // If not, we must create one. |
| if (info == null) { |
| info = new FieldAccessInfoImpl(encodedField.field); |
| fieldAccessInfoCollection.extend(encodedField.field, info); |
| } |
| |
| // If `field` is an indirect reference, then create a mapping for it, such that we don't have |
| // to resolve the field the next time we see the reference. |
| if (field != encodedField.field) { |
| fieldAccessInfoCollection.extend(field, info); |
| } |
| } else if (info == MISSING_FIELD_ACCESS_INFO) { |
| return false; |
| } |
| return isRead ? info.recordRead(field, context) : info.recordWrite(field, context); |
| } |
| |
| private class UseRegistry extends com.android.tools.r8.graph.UseRegistry { |
| |
| private final DexEncodedMethod currentMethod; |
| |
| private UseRegistry(DexItemFactory factory, DexProgramClass holder, DexEncodedMethod method) { |
| super(factory); |
| assert holder.type == method.method.holder; |
| this.currentMethod = method; |
| } |
| |
| private KeepReasonWitness reportClassReferenced(DexProgramClass referencedClass) { |
| return graphReporter.reportClassReferencedFrom(referencedClass, currentMethod); |
| } |
| |
| @Override |
| public boolean registerInvokeVirtual(DexMethod method) { |
| return registerInvokeVirtual(method, KeepReason.invokedFrom(currentMethod)); |
| } |
| |
| boolean registerInvokeVirtual(DexMethod method, KeepReason keepReason) { |
| if (method == appView.dexItemFactory().classMethods.newInstance |
| || method == appView.dexItemFactory().constructorMethods.newInstance) { |
| pendingReflectiveUses.add(currentMethod); |
| } else if (appView.dexItemFactory().classMethods.isReflectiveMemberLookup(method)) { |
| // Implicitly add -identifiernamestring rule for the Java reflection in use. |
| identifierNameStrings.add(method); |
| // Revisit the current method to implicitly add -keep rule for items with reflective access. |
| pendingReflectiveUses.add(currentMethod); |
| } |
| if (!registerMethodWithTargetAndContext(virtualInvokes, method, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeVirtual `%s`.", method); |
| } |
| workList.enqueueMarkReachableVirtualAction(method, keepReason); |
| return true; |
| } |
| |
| @Override |
| public boolean registerInvokeDirect(DexMethod method) { |
| return registerInvokeDirect(method, KeepReason.invokedFrom(currentMethod)); |
| } |
| |
| boolean registerInvokeDirect(DexMethod method, KeepReason keepReason) { |
| if (!registerMethodWithTargetAndContext(directInvokes, method, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeDirect `%s`.", method); |
| } |
| handleInvokeOfDirectTarget(method, keepReason); |
| return true; |
| } |
| |
| @Override |
| public boolean registerInvokeStatic(DexMethod method) { |
| return registerInvokeStatic(method, KeepReason.invokedFrom(currentMethod)); |
| } |
| |
| boolean registerInvokeStatic(DexMethod method, KeepReason keepReason) { |
| DexItemFactory dexItemFactory = appView.dexItemFactory(); |
| if (method == dexItemFactory.classMethods.forName |
| || dexItemFactory.atomicFieldUpdaterMethods.isFieldUpdater(method)) { |
| // Implicitly add -identifiernamestring rule for the Java reflection in use. |
| identifierNameStrings.add(method); |
| // Revisit the current method to implicitly add -keep rule for items with reflective access. |
| pendingReflectiveUses.add(currentMethod); |
| } |
| // See comment in handleJavaLangEnumValueOf. |
| if (method == dexItemFactory.enumMethods.valueOf) { |
| pendingReflectiveUses.add(currentMethod); |
| } |
| // Handling of application services. |
| if (dexItemFactory.serviceLoaderMethods.isLoadMethod(method)) { |
| pendingReflectiveUses.add(currentMethod); |
| } |
| if (method == dexItemFactory.proxyMethods.newProxyInstance) { |
| pendingReflectiveUses.add(currentMethod); |
| } |
| if (!registerMethodWithTargetAndContext(staticInvokes, method, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeStatic `%s`.", method); |
| } |
| handleInvokeOfStaticTarget(method, keepReason); |
| return true; |
| } |
| |
| @Override |
| public boolean registerInvokeInterface(DexMethod method) { |
| return registerInvokeInterface(method, KeepReason.invokedFrom(currentMethod)); |
| } |
| |
| boolean registerInvokeInterface(DexMethod method, KeepReason keepReason) { |
| if (!registerMethodWithTargetAndContext(interfaceInvokes, method, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeInterface `%s`.", method); |
| } |
| workList.enqueueMarkReachableInterfaceAction(method, keepReason); |
| return true; |
| } |
| |
| @Override |
| public boolean registerInvokeSuper(DexMethod method) { |
| // We have to revisit super invokes based on the context they are found in. The same |
| // method descriptor will hit different targets, depending on the context it is used in. |
| DexMethod actualTarget = getInvokeSuperTarget(method, currentMethod); |
| if (!registerMethodWithTargetAndContext(superInvokes, method, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeSuper `%s`.", actualTarget); |
| } |
| workList.enqueueMarkReachableSuperAction(method, currentMethod); |
| return true; |
| } |
| |
| @Override |
| public boolean registerInstanceFieldWrite(DexField field) { |
| if (!registerFieldWrite(field, currentMethod)) { |
| return false; |
| } |
| |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| |
| DexEncodedField encodedField = appInfo.resolveField(field); |
| if (encodedField == null) { |
| reportMissingField(field); |
| return false; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(encodedField.field.holder); |
| if (clazz == null) { |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Iput `%s`.", field); |
| } |
| |
| // If it is written outside of the <init>s of its enclosing class, record it. |
| boolean isWrittenOutsideEnclosingInstanceInitializers = |
| currentMethod.method.holder != encodedField.field.holder |
| || !currentMethod.isInstanceInitializer(); |
| if (isWrittenOutsideEnclosingInstanceInitializers) { |
| instanceFieldsWrittenOutsideEnclosingInstanceInitializers.add(encodedField.field); |
| } |
| |
| // If unused interface removal is enabled, then we won't necessarily mark the actual holder of |
| // the field as live, if the holder is an interface. |
| if (appView.options().enableUnusedInterfaceRemoval) { |
| if (encodedField.field != field) { |
| markTypeAsLive(clazz, reportClassReferenced(clazz)); |
| markTypeAsLive(encodedField.field.type, this::reportClassReferenced); |
| } |
| } |
| |
| KeepReason reason = KeepReason.fieldReferencedIn(currentMethod); |
| workList.enqueueMarkReachableFieldAction(clazz, encodedField, reason); |
| return true; |
| } |
| |
| @Override |
| public boolean registerInstanceFieldRead(DexField field) { |
| if (!registerFieldRead(field, currentMethod)) { |
| return false; |
| } |
| |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| |
| DexEncodedField encodedField = appInfo.resolveField(field); |
| if (encodedField == null) { |
| reportMissingField(field); |
| return false; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(encodedField.field.holder); |
| if (clazz == null) { |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Iget `%s`.", field); |
| } |
| |
| // If unused interface removal is enabled, then we won't necessarily mark the actual holder of |
| // the field as live, if the holder is an interface. |
| if (appView.options().enableUnusedInterfaceRemoval) { |
| if (encodedField.field != field) { |
| markTypeAsLive(clazz, reportClassReferenced(clazz)); |
| markTypeAsLive(encodedField.field.type, this::reportClassReferenced); |
| } |
| } |
| |
| workList.enqueueMarkReachableFieldAction( |
| clazz, encodedField, KeepReason.fieldReferencedIn(currentMethod)); |
| return true; |
| } |
| |
| @Override |
| public boolean registerNewInstance(DexType type) { |
| return registerNewInstance(type, KeepReason.instantiatedIn(currentMethod)); |
| } |
| |
| public boolean registerNewInstance(DexType type, KeepReason keepReason) { |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null) { |
| if (clazz.isInterface()) { |
| markTypeAsLive(clazz, keepReason); |
| } else { |
| markInstantiated(clazz, keepReason); |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean registerStaticFieldRead(DexField field) { |
| if (!registerFieldRead(field, currentMethod)) { |
| return false; |
| } |
| |
| DexEncodedField encodedField = appInfo.resolveField(field); |
| if (encodedField == null) { |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| reportMissingField(field); |
| return false; |
| } |
| |
| if (!isProgramClass(encodedField.field.holder)) { |
| // No need to trace into the non-program code. |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Sget `%s`.", field); |
| } |
| |
| if (appView.options().enableGeneratedExtensionRegistryShrinking) { |
| // If it is a dead proto extension field, don't trace onwards. |
| boolean skipTracing = |
| appView.withGeneratedExtensionRegistryShrinker( |
| shrinker -> |
| shrinker.isDeadProtoExtensionField(encodedField, fieldAccessInfoCollection), |
| false); |
| if (skipTracing) { |
| return false; |
| } |
| } |
| |
| if (encodedField.field != field) { |
| // Mark the non-rebound field access as targeted. Note that this should only be done if the |
| // field is not a dead proto field (in which case we bail-out above). |
| markFieldAsTargeted(field, currentMethod); |
| } |
| |
| markStaticFieldAsLive(encodedField, KeepReason.fieldReferencedIn(currentMethod)); |
| return true; |
| } |
| |
| @Override |
| public boolean registerStaticFieldWrite(DexField field) { |
| if (!registerFieldWrite(field, currentMethod)) { |
| return false; |
| } |
| |
| DexEncodedField encodedField = appInfo.resolveField(field); |
| if (encodedField == null) { |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| reportMissingField(field); |
| return false; |
| } |
| |
| if (!isProgramClass(encodedField.field.holder)) { |
| // No need to trace into the non-program code. |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Sput `%s`.", field); |
| } |
| |
| if (appView.options().enableGeneratedExtensionRegistryShrinking) { |
| // If it is a dead proto extension field, don't trace onwards. |
| boolean skipTracing = |
| appView.withGeneratedExtensionRegistryShrinker( |
| shrinker -> |
| shrinker.isDeadProtoExtensionField(encodedField, fieldAccessInfoCollection), |
| false); |
| if (skipTracing) { |
| return false; |
| } |
| } |
| |
| // If it is written outside of the <clinit> of its enclosing class, record it. |
| boolean isWrittenOutsideEnclosingStaticInitializer = |
| currentMethod.method.holder != encodedField.field.holder |
| || !currentMethod.isClassInitializer(); |
| if (isWrittenOutsideEnclosingStaticInitializer) { |
| staticFieldsWrittenOutsideEnclosingStaticInitializer.add(encodedField.field); |
| } |
| |
| if (encodedField.field != field) { |
| // Mark the non-rebound field access as targeted. Note that this should only be done if the |
| // field is not a dead proto field (in which case we bail-out above). |
| markFieldAsTargeted(field, currentMethod); |
| } |
| |
| markStaticFieldAsLive(encodedField, KeepReason.fieldReferencedIn(currentMethod)); |
| return true; |
| } |
| |
| @Override |
| public boolean registerConstClass(DexType type) { |
| return registerConstClassOrCheckCast(type); |
| } |
| |
| @Override |
| public boolean registerCheckCast(DexType type) { |
| return registerConstClassOrCheckCast(type); |
| } |
| |
| @Override |
| public boolean registerTypeReference(DexType type) { |
| markTypeAsLive(type, this::reportClassReferenced); |
| return true; |
| } |
| |
| @Override |
| public void registerMethodHandle(DexMethodHandle methodHandle, MethodHandleUse use) { |
| super.registerMethodHandle(methodHandle, use); |
| // If a method handle is not an argument to a lambda metafactory it could flow to a |
| // MethodHandle.invokeExact invocation. For that to work, the receiver type cannot have |
| // changed and therefore we cannot perform member rebinding. For these handles, we maintain |
| // the receiver for the method handle. Therefore, we have to make sure that the receiver |
| // stays in the output (and is not class merged). To ensure that we treat the receiver |
| // as instantiated. |
| if (methodHandle.isMethodHandle() && use != MethodHandleUse.ARGUMENT_TO_LAMBDA_METAFACTORY) { |
| DexType type = methodHandle.asMethod().holder; |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null) { |
| KeepReason reason = KeepReason.methodHandleReferencedIn(currentMethod); |
| if (clazz.isInterface() && !clazz.accessFlags.isAnnotation()) { |
| markInterfaceAsInstantiated(clazz, reason); |
| } else { |
| markInstantiated(clazz, reason); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void registerCallSite(DexCallSite callSite) { |
| callSites.add(callSite); |
| super.registerCallSite(callSite); |
| |
| List<DexType> directInterfaces = LambdaDescriptor.getInterfaces(callSite, appInfo); |
| if (directInterfaces != null) { |
| for (DexType lambdaInstantiatedInterface : directInterfaces) { |
| markLambdaInstantiated(lambdaInstantiatedInterface, currentMethod); |
| } |
| } else { |
| if (!appInfo.isStringConcat(callSite.bootstrapMethod)) { |
| if (options.reporter != null) { |
| Diagnostic message = |
| new StringDiagnostic( |
| "Unknown bootstrap method " + callSite.bootstrapMethod, |
| appInfo.originFor(currentMethod.method.holder)); |
| options.reporter.warning(message); |
| } |
| } |
| } |
| |
| DexProgramClass bootstrapClass = |
| getProgramClassOrNull(callSite.bootstrapMethod.asMethod().holder); |
| if (bootstrapClass != null) { |
| bootstrapMethods.add(callSite.bootstrapMethod.asMethod()); |
| } |
| |
| LambdaDescriptor descriptor = LambdaDescriptor.tryInfer(callSite, appInfo); |
| if (descriptor == null) { |
| return; |
| } |
| |
| // For call sites representing a lambda, we link the targeted method |
| // or field as if it were referenced from the current method. |
| |
| DexMethodHandle implHandle = descriptor.implHandle; |
| assert implHandle != null; |
| |
| DexMethod method = implHandle.asMethod(); |
| if (descriptor.delegatesToLambdaImplMethod()) { |
| lambdaMethodsTargetedByInvokeDynamic.add(method); |
| } |
| |
| if (!methodsTargetedByInvokeDynamic.add(method)) { |
| return; |
| } |
| |
| switch (implHandle.type) { |
| case INVOKE_STATIC: |
| registerInvokeStatic(method, KeepReason.invokedFromLambdaCreatedIn(currentMethod)); |
| break; |
| case INVOKE_INTERFACE: |
| registerInvokeInterface(method, KeepReason.invokedFromLambdaCreatedIn(currentMethod)); |
| break; |
| case INVOKE_INSTANCE: |
| registerInvokeVirtual(method, KeepReason.invokedFromLambdaCreatedIn(currentMethod)); |
| break; |
| case INVOKE_DIRECT: |
| registerInvokeDirect(method, KeepReason.invokedFromLambdaCreatedIn(currentMethod)); |
| break; |
| case INVOKE_CONSTRUCTOR: |
| registerNewInstance(method.holder, KeepReason.invokedFromLambdaCreatedIn(currentMethod)); |
| break; |
| default: |
| throw new Unreachable(); |
| } |
| |
| // In similar way as what transitionMethodsForInstantiatedClass does for existing |
| // classes we need to process classes dynamically created by runtime for lambdas. |
| // We make an assumption that such classes are inherited directly from java.lang.Object |
| // and implement all lambda interfaces. |
| |
| if (directInterfaces == null) { |
| return; |
| } |
| |
| // The set now contains all virtual methods on the type and its supertype that are reachable. |
| // In a second step, we now look at interfaces. We have to do this in this order due to JVM |
| // semantics for default methods. A default method is only reachable if it is not overridden |
| // in any superclass. Also, it is not defined which default method is chosen if multiple |
| // interfaces define the same default method. Hence, for every interface (direct or indirect), |
| // we have to look at the interface chain and mark default methods as reachable, not taking |
| // the shadowing of other interface chains into account. |
| // See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4.3.3 |
| ScopedDexMethodSet seen = new ScopedDexMethodSet(); |
| for (DexType iface : directInterfaces) { |
| DexProgramClass ifaceClazz = getProgramClassOrNull(iface); |
| if (ifaceClazz != null) { |
| transitionDefaultMethodsForInstantiatedClass(iface, seen); |
| } |
| } |
| } |
| |
| private boolean registerConstClassOrCheckCast(DexType type) { |
| if (!forceProguardCompatibility) { |
| return registerTypeReference(type); |
| } |
| DexType baseType = type.toBaseType(appView.dexItemFactory()); |
| if (baseType.isClassType()) { |
| DexProgramClass baseClass = getProgramClassOrNull(baseType); |
| if (baseClass != null) { |
| // Don't require any constructor, see b/112386012. |
| markClassAsInstantiatedWithCompatRule(baseClass); |
| } else { |
| // This also handles reporting of missing classes. |
| markTypeAsLive(baseType, this::reportClassReferenced); |
| } |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| private void transitionReachableVirtualMethods(DexProgramClass clazz, ScopedDexMethodSet seen) { |
| SetWithStoredReason<DexEncodedMethod> reachableMethods = reachableVirtualMethods.get(clazz); |
| if (reachableMethods != null) { |
| transitionNonAbstractMethodsToLiveAndShadow(clazz, reachableMethods, seen); |
| } |
| } |
| |
| private DexMethod getInvokeSuperTarget(DexMethod method, DexEncodedMethod currentMethod) { |
| DexClass methodHolderClass = appView.definitionFor(method.holder); |
| if (methodHolderClass != null && methodHolderClass.isInterface()) { |
| return method; |
| } |
| DexClass holderClass = appView.definitionFor(currentMethod.method.holder); |
| if (holderClass == null || holderClass.superType == null || holderClass.isInterface()) { |
| // We do not know better or this call is made from an interface. |
| return method; |
| } |
| // Return the invoked method on the supertype. |
| return appView.dexItemFactory().createMethod(holderClass.superType, method.proto, method.name); |
| } |
| |
| // |
| // Actual actions performed. |
| // |
| |
| private boolean verifyMethodIsTargeted(DexEncodedMethod method) { |
| assert !method.isClassInitializer() : "Class initializers are never targeted"; |
| assert targetedMethods.contains(method); |
| return true; |
| } |
| |
| private boolean verifyTypeIsLive(DexProgramClass clazz) { |
| assert liveTypes.contains(clazz); |
| return true; |
| } |
| |
| private void markTypeAsLive(DexType type, KeepReason reason) { |
| if (type.isArrayType()) { |
| markTypeAsLive(type.toBaseType(appView.dexItemFactory()), reason); |
| return; |
| } |
| if (!type.isClassType()) { |
| // Ignore primitive types. |
| return; |
| } |
| DexProgramClass holder = getProgramClassOrNull(type); |
| if (holder == null) { |
| return; |
| } |
| markTypeAsLive( |
| holder, |
| scopedMethodsForLiveTypes.computeIfAbsent(type, ignore -> new ScopedDexMethodSet()), |
| reason); |
| } |
| |
| private void markTypeAsLive(DexType type, Function<DexProgramClass, KeepReason> reason) { |
| if (type.isArrayType()) { |
| markTypeAsLive(type.toBaseType(appView.dexItemFactory()), reason); |
| return; |
| } |
| if (!type.isClassType()) { |
| // Ignore primitive types. |
| return; |
| } |
| DexProgramClass holder = getProgramClassOrNull(type); |
| if (holder == null) { |
| return; |
| } |
| markTypeAsLive( |
| holder, |
| scopedMethodsForLiveTypes.computeIfAbsent(type, ignore -> new ScopedDexMethodSet()), |
| reason.apply(holder)); |
| } |
| |
| private void markTypeAsLive(DexProgramClass clazz, KeepReason reason) { |
| markTypeAsLive( |
| clazz, |
| scopedMethodsForLiveTypes.computeIfAbsent(clazz.type, ignore -> new ScopedDexMethodSet()), |
| reason); |
| } |
| |
| private void markTypeAsLive( |
| DexProgramClass holder, ScopedDexMethodSet seen, KeepReason reasonForType) { |
| if (!liveTypes.add(holder, reasonForType)) { |
| return; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Type `%s` has become live.", holder.type); |
| } |
| |
| KeepReason reason = KeepReason.reachableFromLiveType(holder.type); |
| |
| for (DexType iface : holder.interfaces.values) { |
| markInterfaceTypeAsLiveViaInheritanceClause(iface, reason); |
| } |
| |
| if (holder.superType != null) { |
| ScopedDexMethodSet seenForSuper = |
| scopedMethodsForLiveTypes.computeIfAbsent( |
| holder.superType, ignore -> new ScopedDexMethodSet()); |
| seen.setParent(seenForSuper); |
| markTypeAsLive(holder.superType, reason); |
| } |
| |
| |
| // We cannot remove virtual methods defined earlier in the type hierarchy if it is widening |
| // access and is defined in an interface: |
| // |
| // public interface I { |
| // void clone(); |
| // } |
| // |
| // class Model implements I { |
| // public void clone() { ... } <-- this cannot be removed |
| // } |
| // |
| // Any class loading of Model with Model.clone() removed will result in an illegal access |
| // error because their exists an existing implementation (here it is Object.clone()). This is |
| // only a problem in the DEX VM. We have to make this check no matter the output because |
| // CF libraries can be used by Android apps. See b/136698023 for more information. |
| ensureMethodsContinueToWidenAccess(holder, seen, reason); |
| |
| // We also need to add the corresponding <clinit> to the set of live methods, as otherwise |
| // static field initialization (and other class-load-time sideeffects) will not happen. |
| if (holder.hasClassInitializer()) { |
| DexEncodedMethod clinit = holder.getClassInitializer(); |
| if (clinit != null && clinit.getOptimizationInfo().mayHaveSideEffects()) { |
| assert clinit.method.holder == holder.type; |
| markDirectStaticOrConstructorMethodAsLive(holder, clinit, reason); |
| } |
| } |
| |
| if (holder.isSerializable(appView)) { |
| enqueueFirstNonSerializableClassInitializer(holder, reason); |
| } |
| |
| if (!holder.annotations.isEmpty()) { |
| processAnnotations(holder, holder.annotations.annotations); |
| } |
| // If this type has deferred annotations, we have to process those now, too. |
| Set<DexAnnotation> annotations = deferredAnnotations.remove(holder.type); |
| if (annotations != null && !annotations.isEmpty()) { |
| assert holder.accessFlags.isAnnotation(); |
| assert annotations.stream().allMatch(a -> a.annotation.type == holder.type); |
| annotations.forEach(annotation -> handleAnnotation(holder, annotation)); |
| } |
| |
| rootSet.forEachDependentStaticMember(holder, appView, this::enqueueDependentItem); |
| compatEnqueueHolderIfDependentNonStaticMember( |
| holder, rootSet.getDependentKeepClassCompatRule(holder.getType())); |
| } |
| |
| private void ensureMethodsContinueToWidenAccess(DexClass clazz) { |
| assert !clazz.isProgramClass(); |
| ScopedDexMethodSet seen = |
| scopedMethodsForLiveTypes.computeIfAbsent(clazz.type, ignore -> new ScopedDexMethodSet()); |
| clazz.virtualMethods().forEach(seen::addMethodIfMoreVisible); |
| } |
| |
| private void ensureMethodsContinueToWidenAccess( |
| DexProgramClass clazz, ScopedDexMethodSet seen, KeepReason reason) { |
| for (DexEncodedMethod method : clazz.virtualMethods()) { |
| if (seen.addMethodIfMoreVisible(method) == AddMethodIfMoreVisibleResult.ADDED_MORE_VISIBLE |
| && clazz.isProgramClass() |
| && appView.appInfo().methodDefinedInInterfaces(method, clazz.type)) { |
| markMethodAsTargeted(clazz, method, reason); |
| } |
| } |
| } |
| |
| private void markInterfaceTypeAsLiveViaInheritanceClause(DexType type, KeepReason reason) { |
| if (appView.options().enableUnusedInterfaceRemoval && !mode.isTracingMainDex()) { |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz == null) { |
| return; |
| } |
| |
| assert clazz.isInterface(); |
| |
| if (!clazz.interfaces.isEmpty()) { |
| markTypeAsLive(type, reason); |
| return; |
| } |
| |
| for (DexEncodedMethod method : clazz.virtualMethods()) { |
| if (!method.accessFlags.isAbstract()) { |
| markTypeAsLive(type, reason); |
| return; |
| } |
| } |
| |
| // No need to mark the type as live. If an interface type is only reachable via the |
| // inheritance clause of another type, and the interface only has abstract methods, it can |
| // simply be removed from the inheritance clause. |
| } else { |
| markTypeAsLive(type, reason); |
| } |
| } |
| |
| private void enqueueDependentItem( |
| DexDefinition precondition, DexDefinition consequent, Set<ProguardKeepRuleBase> reasons) { |
| internalEnqueueRootItem(consequent, reasons, precondition); |
| } |
| |
| private void processAnnotations(DexDefinition holder, DexAnnotation[] annotations) { |
| for (DexAnnotation annotation : annotations) { |
| processAnnotation(holder, annotation); |
| } |
| } |
| |
| private void processAnnotation(DexDefinition holder, DexAnnotation annotation) { |
| handleAnnotation(holder, annotation); |
| } |
| |
| private void handleAnnotation(DexDefinition holder, DexAnnotation annotation) { |
| assert !holder.isDexClass() || holder.asDexClass().isProgramClass(); |
| DexType type = annotation.annotation.type; |
| DexClass clazz = appView.definitionFor(type); |
| boolean annotationTypeIsLibraryClass = clazz == null || clazz.isNotProgramClass(); |
| boolean isLive = annotationTypeIsLibraryClass || liveTypes.contains(clazz.asProgramClass()); |
| if (!shouldKeepAnnotation(annotation, isLive, appView.dexItemFactory(), options)) { |
| // Remember this annotation for later. |
| if (!annotationTypeIsLibraryClass) { |
| deferredAnnotations.computeIfAbsent(type, ignore -> new HashSet<>()).add(annotation); |
| } |
| return; |
| } |
| KeepReason reason = KeepReason.annotatedOn(holder); |
| liveAnnotations.add(annotation, reason); |
| AnnotationReferenceMarker referenceMarker = |
| new AnnotationReferenceMarker(annotation.annotation.type, appView.dexItemFactory(), reason); |
| annotation.annotation.collectIndexedItems(referenceMarker); |
| } |
| |
| private void handleInvokeOfStaticTarget(DexMethod method, KeepReason reason) { |
| ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method); |
| if (resolutionResult == null) { |
| reportMissingMethod(method); |
| return; |
| } |
| DexEncodedMethod encodedMethod = resolutionResult.asSingleTarget(); |
| if (encodedMethod == null) { |
| // Note: should this be reported too? Or is this unreachable? |
| return; |
| } |
| DexProgramClass clazz = getProgramClassOrNull(encodedMethod.method.holder); |
| if (clazz == null) { |
| return; |
| } |
| |
| // We have to mark the resolved method as targeted even if it cannot actually be invoked |
| // to make sure the invocation will keep failing in the appropriate way. |
| markMethodAsTargeted(clazz, encodedMethod, reason); |
| |
| // Only mark methods for which invocation will succeed at runtime live. |
| if (encodedMethod.isStatic()) { |
| registerClassInitializer(clazz, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, encodedMethod, reason); |
| } |
| } |
| |
| private void registerClassInitializer(DexProgramClass definition, KeepReason reason) { |
| if (definition.hasClassInitializer()) { |
| registerMethod(definition.getClassInitializer(), reason); |
| } |
| } |
| |
| private void markNonStaticDirectMethodAsReachable(DexMethod method, KeepReason reason) { |
| handleInvokeOfDirectTarget(method, reason); |
| } |
| |
| private void handleInvokeOfDirectTarget(DexMethod method, KeepReason reason) { |
| DexType holder = method.holder; |
| DexProgramClass clazz = getProgramClassOrNull(holder); |
| if (clazz == null) { |
| return; |
| } |
| // TODO(zerny): Is it ok that we lookup in both the direct and virtual pool here? |
| DexEncodedMethod encodedMethod = clazz.lookupMethod(method); |
| if (encodedMethod == null) { |
| reportMissingMethod(method); |
| return; |
| } |
| |
| // We have to mark the resolved method as targeted even if it cannot actually be invoked |
| // to make sure the invocation will keep failing in the appropriate way. |
| markMethodAsTargeted(clazz, encodedMethod, reason); |
| |
| // Only mark methods for which invocation will succeed at runtime live. |
| if (encodedMethod.isStatic()) { |
| return; |
| } |
| |
| markDirectStaticOrConstructorMethodAsLive(clazz, encodedMethod, reason); |
| |
| // It is valid to have an invoke-direct instruction in a default interface method that |
| // targets another default method in the same interface (see testInvokeSpecialToDefault- |
| // Method). In a class, that would lead to a verification error. |
| if (encodedMethod.isVirtualMethod() |
| && virtualMethodsTargetedByInvokeDirect.add(encodedMethod.method)) { |
| enqueueMarkMethodLiveAction(clazz, encodedMethod, reason); |
| } |
| } |
| |
| private void ensureFromLibraryOrThrow(DexType type, DexClass context) { |
| if (!mode.isInitialTreeShaking()) { |
| // b/72312389: android.jar contains parts of JUnit and most developers include JUnit in |
| // their programs. This leads to library classes extending program classes. When tracing |
| // main dex lists we allow this. |
| return; |
| } |
| |
| if (dontWarnPatterns.matches(context.type)) { |
| // Ignore. |
| return; |
| } |
| |
| DexClass holder = appView.definitionFor(type); |
| if (holder != null && !holder.isLibraryClass()) { |
| Diagnostic message = |
| new StringDiagnostic( |
| "Library class " |
| + context.type.toSourceString() |
| + (holder.isInterface() ? " implements " : " extends ") |
| + "program class " |
| + type.toSourceString()); |
| if (forceProguardCompatibility) { |
| options.reporter.warning(message); |
| } else { |
| options.reporter.error(message); |
| } |
| } |
| } |
| |
| private void reportMissingClass(DexType clazz) { |
| if (Log.ENABLED && reportedMissing.add(clazz)) { |
| Log.verbose(Enqueuer.class, "Class `%s` is missing.", clazz); |
| } |
| } |
| |
| private void reportMissingMethod(DexMethod method) { |
| if (Log.ENABLED && reportedMissing.add(method)) { |
| Log.verbose(Enqueuer.class, "Method `%s` is missing.", method); |
| } |
| } |
| |
| private void reportMissingField(DexField field) { |
| if (Log.ENABLED && reportedMissing.add(field)) { |
| Log.verbose(Enqueuer.class, "Field `%s` is missing.", field); |
| } |
| } |
| |
| private void markMethodAsTargeted( |
| DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { |
| assert method.method.holder == clazz.type; |
| if (!targetedMethods.add(method, reason)) { |
| // Already targeted. |
| return; |
| } |
| markTypeAsLive(method.method.holder, |
| holder -> graphReporter.reportClassReferencedFrom(holder, method)); |
| markParameterAndReturnTypesAsLive(method); |
| processAnnotations(method, method.annotations.annotations); |
| method.parameterAnnotationsList.forEachAnnotation( |
| annotation -> processAnnotation(method, annotation)); |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Method `%s` is targeted.", method.method); |
| } |
| if (forceProguardCompatibility) { |
| // Keep targeted default methods in compatibility mode. The tree pruner will otherwise make |
| // these methods abstract, whereas Proguard does not (seem to) touch their code. |
| if (!method.accessFlags.isAbstract() && clazz.isInterface()) { |
| markMethodAsLiveWithCompatRule(clazz, method); |
| } |
| } |
| } |
| |
| /** |
| * Adds the class to the set of instantiated classes and marks its fields and methods live |
| * depending on the currently seen invokes and field reads. |
| */ |
| private void processNewlyInstantiatedClass(DexProgramClass clazz, KeepReason reason) { |
| assert !clazz.isInterface() || clazz.accessFlags.isAnnotation(); |
| // Notify analyses. This is done even if `clazz` has already been marked as instantiated, |
| // because each analysis may depend on seeing all the (clazz, reason) pairs. Thus, not doing so |
| // could lead to nondeterminism. |
| analyses.forEach( |
| analysis -> analysis.processNewlyInstantiatedClass(clazz.asProgramClass(), reason)); |
| |
| if (!instantiatedTypes.add(clazz, reason)) { |
| return; |
| } |
| |
| populateInstantiatedTypesCache(clazz); |
| |
| collectProguardCompatibilityRule(reason); |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Class `%s` is instantiated, processing...", clazz); |
| } |
| // This class becomes live, so it and all its supertypes become live types. |
| markTypeAsLive(clazz, reason); |
| // For all methods of the class, if we have seen a call, mark the method live. |
| // We only do this for virtual calls, as the other ones will be done directly. |
| transitionMethodsForInstantiatedClass(clazz); |
| // For all instance fields visible from the class, mark them live if we have seen a read. |
| transitionFieldsForInstantiatedClass(clazz); |
| // Add all dependent instance members to the workqueue. |
| transitionDependentItemsForInstantiatedClass(clazz); |
| } |
| |
| private void populateInstantiatedTypesCache(DexProgramClass clazz) { |
| if (!directAndIndirectlyInstantiatedTypes.add(clazz)) { |
| return; |
| } |
| if (clazz.superType != null) { |
| DexProgramClass superClass = getProgramClassOrNull(clazz.superType); |
| if (superClass != null) { |
| populateInstantiatedTypesCache(superClass); |
| } |
| } |
| for (DexType iface : clazz.interfaces.values) { |
| DexProgramClass ifaceClass = getProgramClassOrNull(iface); |
| if (ifaceClass != null) { |
| populateInstantiatedTypesCache(ifaceClass); |
| } |
| } |
| } |
| |
| /** |
| * Marks all methods live that can be reached by calls previously seen. |
| * |
| * <p>This should only be invoked if the given type newly becomes instantiated. In essence, this |
| * method replays all the invokes we have seen so far that could apply to this type and marks the |
| * corresponding methods live. |
| * |
| * <p>Only methods that are visible in this type are considered. That is, only those methods that |
| * are either defined directly on this type or that are defined on a supertype but are not |
| * shadowed by another inherited method. Furthermore, default methods from implemented interfaces |
| * that are not otherwise shadowed are considered, too. |
| * |
| * <p>Finally all methods on library types that resolve starting at the instantiated type are |
| * marked live. |
| */ |
| private void transitionMethodsForInstantiatedClass(DexProgramClass instantiatedClass) { |
| ScopedDexMethodSet seen = new ScopedDexMethodSet(); |
| Set<DexType> interfaces = Sets.newIdentityHashSet(); |
| DexProgramClass current = instantiatedClass; |
| do { |
| // We only have to look at virtual methods here, as only those can actually be executed at |
| // runtime. Illegal dispatch situations and the corresponding exceptions are already handled |
| // by the reachability logic. |
| transitionReachableVirtualMethods(current, seen); |
| Collections.addAll(interfaces, current.interfaces.values); |
| current = getProgramClassOrNull(current.superType); |
| } while (current != null && !instantiatedTypes.contains(current)); |
| |
| // The set now contains all virtual methods on the type and its supertype that are reachable. |
| // In a second step, we now look at interfaces. We have to do this in this order due to JVM |
| // semantics for default methods. A default method is only reachable if it is not overridden in |
| // any superclass. Also, it is not defined which default method is chosen if multiple |
| // interfaces define the same default method. Hence, for every interface (direct or indirect), |
| // we have to look at the interface chain and mark default methods as reachable, not taking |
| // the shadowing of other interface chains into account. |
| // See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4.3.3 |
| for (DexType iface : interfaces) { |
| DexClass clazz = appView.definitionFor(iface); |
| if (clazz == null) { |
| reportMissingClass(iface); |
| // TODO(herhut): In essence, our subtyping chain is broken here. Handle that case better. |
| break; |
| } |
| transitionDefaultMethodsForInstantiatedClass(iface, seen); |
| } |
| |
| // When tracing the main-dex content, library roots must be specified, thus there are no |
| // implicit edges from library methods. |
| if (getMode().isTracingMainDex()) { |
| return; |
| } |
| |
| // When a type becomes live, all library methods on that type become live too. |
| // This is done by searching the library supertypes and then resolving each method defined by |
| // such a library type from the point of the instantiated type. If the resolved targets are in |
| // the program, i.e., the instantiated type has a method overidding a library method, then the |
| // program method is live. |
| Deque<DexClass> librarySearchItems = new ArrayDeque<>(); |
| librarySearchItems.add(instantiatedClass); |
| while (!librarySearchItems.isEmpty()) { |
| DexClass clazz = librarySearchItems.pop(); |
| if (clazz.isNotProgramClass()) { |
| markLibraryAndClasspathMethodOverriddesAsLive(clazz, instantiatedClass); |
| } |
| if (clazz.superType != null) { |
| DexClass superClass = appView.definitionFor(clazz.superType); |
| if (superClass != null) { |
| librarySearchItems.add(superClass); |
| } |
| } |
| for (DexType iface : clazz.interfaces.values) { |
| DexClass ifaceClass = appView.definitionFor(iface); |
| if (ifaceClass != null) { |
| librarySearchItems.add(ifaceClass); |
| } |
| } |
| } |
| } |
| |
| private void markLibraryAndClasspathMethodOverriddesAsLive( |
| DexClass libraryClass, DexProgramClass instantiatedClass) { |
| assert libraryClass.isNotProgramClass(); |
| assert !instantiatedClass.isInterface() || instantiatedClass.accessFlags.isAnnotation(); |
| for (DexEncodedMethod method : libraryClass.virtualMethods()) { |
| // Note: it may be worthwhile to add a resolution cache here. If so, it must till ensure |
| // that all library override edges are reported to the kept-graph consumer. |
| ResolutionResult resolution = |
| appView.appInfo().resolveMethod(instantiatedClass, method.method); |
| if (resolution.isValidVirtualTarget(options)) { |
| resolution.forEachTarget( |
| target -> { |
| if (!target.isAbstract()) { |
| DexClass targetHolder = appView.definitionFor(target.method.holder); |
| if (targetHolder != null && targetHolder.isProgramClass()) { |
| DexProgramClass programClass = targetHolder.asProgramClass(); |
| if (shouldMarkLibraryMethodOverrideAsReachable(programClass, target)) { |
| target.setLibraryMethodOverride(); |
| markVirtualMethodAsLive( |
| programClass, |
| target, |
| KeepReason.isLibraryMethod(programClass, libraryClass.type)); |
| } |
| } |
| } |
| }); |
| } |
| } |
| } |
| |
| private void transitionDefaultMethodsForInstantiatedClass( |
| DexType iface, ScopedDexMethodSet seen) { |
| DexProgramClass clazz = getProgramClassOrNull(iface); |
| if (clazz == null) { |
| return; |
| } |
| assert clazz.accessFlags.isInterface(); |
| transitionReachableVirtualMethods(clazz, seen.newNestedScope()); |
| for (DexType subInterface : clazz.interfaces.values) { |
| transitionDefaultMethodsForInstantiatedClass(subInterface, seen); |
| } |
| } |
| |
| private void transitionNonAbstractMethodsToLiveAndShadow( |
| DexProgramClass clazz, |
| SetWithStoredReason<DexEncodedMethod> reachable, |
| ScopedDexMethodSet seen) { |
| for (DexEncodedMethod encodedMethod : reachable.getItems()) { |
| if (seen.addMethod(encodedMethod)) { |
| // Abstract methods do shadow implementations but they cannot be live, as they have no code. |
| if (!encodedMethod.accessFlags.isAbstract()) { |
| markVirtualMethodAsLive( |
| clazz, |
| encodedMethod, |
| registerMethod(encodedMethod, reachable.getReasons(encodedMethod))); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Marks all fields live that can be reached by a read assuming that the given type or one of its |
| * subtypes is instantiated. |
| */ |
| private void transitionFieldsForInstantiatedClass(DexProgramClass clazz) { |
| do { |
| SetWithReason<DexEncodedField> reachableFields = reachableInstanceFields.get(clazz); |
| if (reachableFields != null) { |
| for (DexEncodedField field : reachableFields.getItems()) { |
| // TODO(b/120959039): Should the reason this field is reachable come from the set? |
| markInstanceFieldAsLive(field, KeepReason.reachableFromLiveType(clazz.type)); |
| } |
| } |
| clazz = getProgramClassOrNull(clazz.superType); |
| } while (clazz != null && !instantiatedTypes.contains(clazz)); |
| } |
| |
| private void transitionDependentItemsForInstantiatedClass(DexClass clazz) { |
| DexClass current = clazz; |
| do { |
| // Handle keep rules that are dependent on the class being instantiated. |
| rootSet.forEachDependentNonStaticMember(current, appView, this::enqueueDependentItem); |
| |
| // Visit the super type. |
| current = current.superType != null ? appView.definitionFor(current.superType) : null; |
| } while (current != null |
| && current.isProgramClass() |
| && !instantiatedTypes.contains(current.asProgramClass())); |
| } |
| |
| private void markFieldAsTargeted(DexField field, DexEncodedMethod context) { |
| markTypeAsLive(field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); |
| markTypeAsLive(field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); |
| } |
| |
| private void markStaticFieldAsLive(DexEncodedField encodedField, KeepReason reason) { |
| // Mark the type live here, so that the class exists at runtime. |
| DexField field = encodedField.field; |
| markTypeAsLive( |
| field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| markTypeAsLive( |
| field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| |
| DexProgramClass clazz = getProgramClassOrNull(field.holder); |
| if (clazz == null) { |
| return; |
| } |
| |
| registerClassInitializer(clazz, reason); |
| |
| // This field might be an instance field reachable from a static context, e.g. a getStatic that |
| // resolves to an instance field. We have to keep the instance field nonetheless, as otherwise |
| // we might unmask a shadowed static field and hence change semantics. |
| if (encodedField.accessFlags.isStatic()) { |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding static field `%s` to live set.", encodedField.field); |
| } |
| } else { |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding instance field `%s` to live set (static context).", |
| encodedField.field); |
| } |
| } |
| processAnnotations(encodedField, encodedField.annotations.annotations); |
| liveFields.add(encodedField, reason); |
| collectProguardCompatibilityRule(reason); |
| |
| // Add all dependent members to the workqueue. |
| enqueueRootItems(rootSet.getDependentItems(encodedField)); |
| |
| // Notify analyses. |
| analyses.forEach(analysis -> analysis.processNewlyLiveField(encodedField)); |
| } |
| |
| private void markInstanceFieldAsLive(DexEncodedField field, KeepReason reason) { |
| assert field != null; |
| assert field.isProgramField(appView); |
| markTypeAsLive(field.field.holder, reason); |
| markTypeAsLive(field.field.type, reason); |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding instance field `%s` to live set.", field.field); |
| } |
| processAnnotations(field, field.annotations.annotations); |
| liveFields.add(field, reason); |
| collectProguardCompatibilityRule(reason); |
| |
| // Add all dependent members to the workqueue. |
| enqueueRootItems(rootSet.getDependentItems(field)); |
| |
| // Notify analyses. |
| analyses.forEach(analysis -> analysis.processNewlyLiveField(field)); |
| } |
| |
| private void markInstantiated(DexProgramClass clazz, KeepReason reason) { |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register new instantiation of `%s`.", clazz); |
| } |
| workList.enqueueMarkInstantiatedAction(clazz, reason); |
| } |
| |
| private void markLambdaInstantiated(DexType itf, DexEncodedMethod method) { |
| DexClass clazz = appView.definitionFor(itf); |
| if (clazz == null) { |
| StringDiagnostic message = |
| new StringDiagnostic( |
| "Lambda expression implements missing interface `" + itf.toSourceString() + "`", |
| appInfo.originFor(method.method.holder)); |
| options.reporter.warning(message); |
| return; |
| } |
| if (!clazz.isInterface()) { |
| StringDiagnostic message = |
| new StringDiagnostic( |
| "Lambda expression expected to implement an interface, but found " |
| + "`" |
| + itf.toSourceString() |
| + "`", |
| appInfo.originFor(method.method.holder)); |
| options.reporter.warning(message); |
| return; |
| } |
| if (clazz.isProgramClass()) { |
| KeepReason reason = KeepReason.instantiatedIn(method); |
| if (instantiatedInterfaceTypes.add(clazz.asProgramClass(), reason)) { |
| populateInstantiatedTypesCache(clazz.asProgramClass()); |
| } |
| } |
| } |
| |
| private void markDirectStaticOrConstructorMethodAsLive( |
| DexProgramClass clazz, DexEncodedMethod encodedMethod, KeepReason reason) { |
| assert encodedMethod.method.holder == clazz.type; |
| |
| if (!enqueueMarkMethodLiveAction(clazz, encodedMethod, reason)) { |
| // Already marked live. |
| return; |
| } |
| // Should already have marked the type live previously. |
| DexMethod method = encodedMethod.method; |
| assert encodedMethod.isClassInitializer() || verifyMethodIsTargeted(encodedMethod); |
| assert verifyTypeIsLive(clazz); |
| if (Log.ENABLED) { |
| Log.verbose( |
| getClass(), "Method `%s` has become live due to direct invoke", encodedMethod.method); |
| } |
| } |
| |
| private void markVirtualMethodAsLive( |
| DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { |
| assert method != null; |
| // Only explicit keep rules or reflective use should make abstract methods live. |
| assert !method.accessFlags.isAbstract() |
| || reason.isDueToKeepRule() |
| || reason.isDueToReflectiveUse(); |
| if (enqueueMarkMethodLiveAction(clazz, method, reason)) { |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding virtual method `%s` to live set.", method.method); |
| } |
| } |
| } |
| |
| public boolean isFieldLive(DexEncodedField field) { |
| return liveFields.contains(field); |
| } |
| |
| private boolean isInstantiatedOrHasInstantiatedSubtype(DexProgramClass clazz) { |
| return directAndIndirectlyInstantiatedTypes.contains(clazz); |
| } |
| |
| private void markInstanceFieldAsReachable(DexEncodedField encodedField, KeepReason reason) { |
| DexField field = encodedField.field; |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Marking instance field `%s` as reachable.", field); |
| } |
| |
| markTypeAsLive( |
| field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| markTypeAsLive( |
| field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| |
| DexProgramClass clazz = getProgramClassOrNull(field.holder); |
| if (clazz == null) { |
| return; |
| } |
| |
| // We might have a instance field access that is dispatched to a static field. In such case, |
| // we have to keep the static field, so that the dispatch fails at runtime in the same way that |
| // it did before. We have to keep the field even if the receiver has no live inhabitants, as |
| // field resolution happens before the receiver is inspected. |
| if (encodedField.accessFlags.isStatic()) { |
| markStaticFieldAsLive(encodedField, reason); |
| } else { |
| if (isInstantiatedOrHasInstantiatedSubtype(clazz)) { |
| markInstanceFieldAsLive(encodedField, reason); |
| } else { |
| // Add the field to the reachable set if the type later becomes instantiated. |
| reachableInstanceFields |
| .computeIfAbsent(clazz, ignore -> newSetWithoutReasonReporter()) |
| .add(encodedField, reason); |
| } |
| } |
| } |
| |
| private void markVirtualMethodAsReachable( |
| DexMethod method, boolean interfaceInvoke, KeepReason reason) { |
| markVirtualMethodAsReachable(method, interfaceInvoke, reason, (x, y) -> true); |
| } |
| |
| private void markVirtualMethodAsReachable( |
| DexMethod method, |
| boolean interfaceInvoke, |
| KeepReason reason, |
| BiPredicate<DexProgramClass, DexEncodedMethod> possibleTargetsFilter) { |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Marking virtual method `%s` as reachable.", method); |
| } |
| if (method.holder.isArrayType()) { |
| // This is an array type, so the actual class will be generated at runtime. We treat this |
| // like an invoke on a direct subtype of java.lang.Object that has no further subtypes. |
| // As it has no subtypes, it cannot affect liveness of the program we are processing. |
| // Ergo, we can ignore it. We need to make sure that the element type is available, though. |
| markTypeAsLive(method.holder, reason); |
| return; |
| } |
| DexClass holder = appView.definitionFor(method.holder); |
| if (holder == null) { |
| reportMissingClass(method.holder); |
| return; |
| } |
| |
| DexEncodedMethod resolutionTarget = virtualTargetsMarkedAsReachable.get(method); |
| if (resolutionTarget != null) { |
| registerMethod(resolutionTarget, reason); |
| return; |
| } |
| resolutionTarget = findAndMarkResolutionTarget(method, interfaceInvoke, reason); |
| virtualTargetsMarkedAsReachable.put(method, resolutionTarget); |
| if (resolutionTarget == null || !resolutionTarget.isValidVirtualTarget(options)) { |
| // There is no valid resolution, so any call will lead to a runtime exception. |
| return; |
| } |
| |
| assert interfaceInvoke == holder.isInterface(); |
| Set<DexEncodedMethod> possibleTargets = |
| resolutionTarget.lookupVirtualDispatchTargets(interfaceInvoke, appInfo); |
| if (possibleTargets == null || possibleTargets.isEmpty()) { |
| return; |
| } |
| KeepReason overridesReason = KeepReason.overridesMethod(resolutionTarget); |
| for (DexEncodedMethod encodedPossibleTarget : possibleTargets) { |
| if (encodedPossibleTarget.isAbstract()) { |
| continue; |
| } |
| markPossibleTargetsAsReachable( |
| resolutionTarget == encodedPossibleTarget ? reason : overridesReason, |
| possibleTargetsFilter, |
| encodedPossibleTarget); |
| } |
| } |
| |
| private void markPossibleTargetsAsReachable( |
| KeepReason reason, |
| BiPredicate<DexProgramClass, DexEncodedMethod> possibleTargetsFilter, |
| DexEncodedMethod encodedPossibleTarget) { |
| assert encodedPossibleTarget.isVirtualMethod() || options.canUseNestBasedAccess(); |
| assert !encodedPossibleTarget.isAbstract(); |
| DexMethod possibleTarget = encodedPossibleTarget.method; |
| DexProgramClass clazz = getProgramClassOrNull(possibleTarget.holder); |
| if (clazz == null) { |
| return; |
| } |
| if (!possibleTargetsFilter.test(clazz, encodedPossibleTarget)) { |
| return; |
| } |
| SetWithStoredReason<DexEncodedMethod> reachable = |
| reachableVirtualMethods.computeIfAbsent(clazz, ignore -> SetWithStoredReason.create()); |
| if (!reachable.add(encodedPossibleTarget, reason)) { |
| return; |
| } |
| |
| // If the holder type is instantiated, the method is live. Otherwise check whether we find |
| // a subtype that does not shadow this methods but is instantiated. |
| // Note that library classes are always considered instantiated, as we do not know where |
| // they are instantiated. |
| if (!isInstantiatedOrHasInstantiatedSubtype(clazz)) { |
| return; |
| } |
| |
| if (instantiatedTypes.contains(clazz) |
| || instantiatedInterfaceTypes.contains(clazz) |
| || pinnedItems.contains(clazz.type)) { |
| markVirtualMethodAsLive(clazz, encodedPossibleTarget, reason); |
| } else { |
| Deque<DexType> worklist = |
| new ArrayDeque<>(appInfo.allImmediateSubtypes(possibleTarget.holder)); |
| while (!worklist.isEmpty()) { |
| DexType current = worklist.pollFirst(); |
| DexProgramClass currentClass = getProgramClassOrNull(current); |
| // If this class overrides the virtual, abort the search. Note that, according to |
| // the JVM spec, private methods cannot override a virtual method. |
| if (currentClass == null || currentClass.lookupVirtualMethod(possibleTarget) != null) { |
| continue; |
| } |
| // TODO(zerny): Why does not not confer with lambdas and pinned too? |
| if (instantiatedTypes.contains(currentClass)) { |
| markVirtualMethodAsLive(clazz, encodedPossibleTarget, reason); |
| break; |
| } |
| appInfo.allImmediateSubtypes(current).forEach(worklist::addLast); |
| } |
| } |
| } |
| |
| private DexEncodedMethod findAndMarkResolutionTarget( |
| DexMethod method, boolean interfaceInvoke, KeepReason reason) { |
| DexEncodedMethod resolutionTarget = |
| appInfo.resolveMethod(method.holder, method, interfaceInvoke).asResultOfResolve(); |
| if (resolutionTarget == null) { |
| reportMissingMethod(method); |
| return null; |
| } |
| |
| DexClass resolutionTargetClass = appInfo.definitionFor(resolutionTarget.method.holder); |
| if (resolutionTargetClass == null) { |
| reportMissingClass(resolutionTarget.method.holder); |
| return null; |
| } |
| |
| // We have to mark this as targeted, as even if this specific instance never becomes live, we |
| // need at least an abstract version of it so that we have a target for the corresponding |
| // invoke. This also ensures preserving the errors detailed below. |
| if (resolutionTargetClass.isProgramClass()) { |
| markMethodAsTargeted(resolutionTargetClass.asProgramClass(), resolutionTarget, reason); |
| } |
| |
| // If the method of an invoke-virtual instruction resolves to a private or static method, then |
| // the invoke fails with an IllegalAccessError or IncompatibleClassChangeError, respectively. |
| // |
| // Unfortunately the above is not always the case: |
| // Some Art VMs do not fail with an IllegalAccessError or IncompatibleClassChangeError if the |
| // method of an invoke-virtual instruction resolves to a private or static method, but instead |
| // ignores private and static methods during resolution (see also NonVirtualOverrideTest). |
| // Therefore, we need to continue resolution from the super type until we find a virtual method. |
| if (resolutionTarget.isPrivateMethod() || resolutionTarget.isStatic()) { |
| assert !interfaceInvoke || resolutionTargetClass.isInterface(); |
| DexEncodedMethod possiblyValidTarget = |
| markPossiblyValidTarget(method, reason, resolutionTarget, resolutionTargetClass); |
| if (possiblyValidTarget != null) { |
| // Since some Art runtimes may actually end up targeting this method, it is returned as |
| // the basis of lookup for the enqueuing of virtual dispatches. Not doing so may cause it |
| // to be marked abstract, thus leading to an AbstractMethodError on said Art runtimes. |
| return possiblyValidTarget; |
| } |
| } |
| |
| return resolutionTarget; |
| } |
| |
| private DexEncodedMethod markPossiblyValidTarget( |
| DexMethod method, |
| KeepReason reason, |
| DexEncodedMethod resolutionTarget, |
| DexClass resolutionTargetClass) { |
| while (resolutionTarget.isPrivateMethod() || resolutionTarget.isStatic()) { |
| resolutionTarget = |
| appInfo |
| .resolveMethod( |
| resolutionTargetClass.superType, method, resolutionTargetClass.isInterface()) |
| .asResultOfResolve(); |
| if (resolutionTarget == null) { |
| return null; |
| } |
| resolutionTargetClass = appInfo.definitionFor(resolutionTarget.method.holder); |
| if (resolutionTargetClass == null) { |
| return null; |
| } |
| } |
| if (resolutionTargetClass.isProgramClass()) { |
| markMethodAsTargeted(resolutionTargetClass.asProgramClass(), resolutionTarget, reason); |
| } |
| return resolutionTarget; |
| } |
| |
| private DexMethod generatedEnumValuesMethod(DexClass enumClass) { |
| DexType arrayOfEnumClass = |
| appView |
| .dexItemFactory() |
| .createType( |
| appView.dexItemFactory().createString("[" + enumClass.type.toDescriptorString())); |
| DexProto proto = appView.dexItemFactory().createProto(arrayOfEnumClass); |
| return appView |
| .dexItemFactory() |
| .createMethod(enumClass.type, proto, appView.dexItemFactory().createString("values")); |
| } |
| |
| private void markEnumValuesAsReachable(DexClass clazz, KeepReason reason) { |
| DexEncodedMethod valuesMethod = clazz.lookupMethod(generatedEnumValuesMethod(clazz)); |
| if (valuesMethod != null) { |
| // TODO(sgjesse): Does this have to be enqueued as a root item? Right now it is done as the |
| // marking for not renaming it is in the root set. |
| workList.enqueueMarkMethodKeptAction(valuesMethod, reason); |
| pinnedItems.add(valuesMethod.toReference()); |
| rootSet.shouldNotBeMinified(valuesMethod.toReference()); |
| } |
| } |
| |
| private void markSuperMethodAsReachable(DexMethod method, DexEncodedMethod from) { |
| // We have to mark the immediate target of the descriptor as targeted, as otherwise |
| // the invoke super will fail in the resolution step with a NSM error. |
| // See <a |
| // href="https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokespecial"> |
| // the JVM spec for invoke-special. |
| DexEncodedMethod resolutionTarget = appInfo.resolveMethod(method.holder, method) |
| .asResultOfResolve(); |
| if (resolutionTarget == null) { |
| brokenSuperInvokes.add(method); |
| reportMissingMethod(method); |
| return; |
| } |
| |
| if (resolutionTarget.accessFlags.isPrivate() || resolutionTarget.accessFlags.isStatic()) { |
| brokenSuperInvokes.add(method); |
| } |
| DexProgramClass resolutionTargetClass = getProgramClassOrNull(resolutionTarget.method.holder); |
| if (resolutionTargetClass != null) { |
| markMethodAsTargeted( |
| resolutionTargetClass, resolutionTarget, KeepReason.targetedBySuperFrom(from)); |
| } |
| |
| // Now we need to compute the actual target in the context. |
| DexEncodedMethod target = appInfo.lookupSuperTarget(method, from.method.holder); |
| if (target == null) { |
| // The actual implementation in the super class is missing. |
| reportMissingMethod(method); |
| return; |
| } |
| DexProgramClass clazz = getProgramClassOrNull(target.method.holder); |
| if (clazz == null) { |
| return; |
| } |
| if (target.accessFlags.isPrivate()) { |
| brokenSuperInvokes.add(method); |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding super constraint from `%s` to `%s`", from.method, |
| target.method); |
| } |
| if (superInvokeDependencies.computeIfAbsent( |
| from, ignore -> Sets.newIdentityHashSet()).add(target)) { |
| if (liveMethods.contains(from)) { |
| markMethodAsTargeted(clazz, target, KeepReason.invokedViaSuperFrom(from)); |
| if (!target.accessFlags.isAbstract()) { |
| markVirtualMethodAsLive(clazz, target, KeepReason.invokedViaSuperFrom(from)); |
| } |
| } |
| } |
| } |
| |
| // Returns the set of live types. |
| public Set<DexProgramClass> traceMainDex( |
| RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException { |
| assert analyses.isEmpty(); |
| assert mode.isTracingMainDex(); |
| this.rootSet = rootSet; |
| // Translate the result of root-set computation into enqueuer actions. |
| enqueueRootItems(rootSet.noShrinking); |
| trace(executorService, timing); |
| options.reporter.failIfPendingErrors(); |
| return liveTypes.getItems(); |
| } |
| |
| public AppInfoWithLiveness traceApplication( |
| RootSet rootSet, |
| ProguardClassFilter dontWarnPatterns, |
| ExecutorService executorService, |
| Timing timing) |
| throws ExecutionException { |
| this.rootSet = rootSet; |
| this.dontWarnPatterns = dontWarnPatterns; |
| // Translate the result of root-set computation into enqueuer actions. |
| enqueueRootItems(rootSet.noShrinking); |
| trace(executorService, timing); |
| options.reporter.failIfPendingErrors(); |
| analyses.forEach(EnqueuerAnalysis::done); |
| assert verifyKeptGraph(); |
| return createAppInfo(appInfo); |
| } |
| |
| private GraphConsumer recordKeptGraph(InternalOptions options, GraphConsumer consumer) { |
| if (options.testing.verifyKeptGraphInfo) { |
| verificationGraphConsumer = new CollectingGraphConsumer(consumer); |
| return verificationGraphConsumer; |
| } |
| return consumer; |
| } |
| |
| private boolean verifyKeptGraph() { |
| if (options.testing.verifyKeptGraphInfo) { |
| assert verificationGraphConsumer != null; |
| for (DexProgramClass liveType : liveTypes.items) { |
| assert verifyRootedPath(liveType, verificationGraphConsumer); |
| } |
| } |
| return true; |
| } |
| |
| private boolean verifyRootedPath(DexProgramClass liveType, CollectingGraphConsumer graph) { |
| ClassGraphNode node = getClassGraphNode(liveType.type); |
| Set<GraphNode> seen = Sets.newIdentityHashSet(); |
| Deque<GraphNode> targets = DequeUtils.newArrayDeque(node); |
| while (!targets.isEmpty()) { |
| GraphNode item = targets.pop(); |
| if (item instanceof KeepRuleGraphNode) { |
| KeepRuleGraphNode rule = (KeepRuleGraphNode) item; |
| if (rule.getPreconditions().isEmpty()) { |
| return true; |
| } |
| } |
| if (seen.add(item)) { |
| Map<GraphNode, Set<GraphEdgeInfo>> sources = graph.getSourcesTargeting(item); |
| assert sources != null : "No sources set for " + item; |
| assert !sources.isEmpty() : "Empty sources set for " + item; |
| targets.addAll(sources.keySet()); |
| } |
| } |
| assert false : "No rooted path to " + liveType.type; |
| return true; |
| } |
| |
| private AppInfoWithLiveness createAppInfo(AppInfoWithSubtyping appInfo) { |
| ImmutableSortedSet.Builder<DexType> builder = |
| ImmutableSortedSet.orderedBy(PresortedComparable::slowCompareTo); |
| liveAnnotations.items.forEach(annotation -> builder.add(annotation.annotation.type)); |
| |
| // Remove the temporary mappings that have been inserted into the field access info collection |
| // and verify that the mapping is then one-to-one. |
| fieldAccessInfoCollection.removeIf( |
| (field, info) -> field != info.getField() || info == MISSING_FIELD_ACCESS_INFO); |
| assert fieldAccessInfoCollection.verifyMappingIsOneToOne(); |
| |
| AppInfoWithLiveness appInfoWithLiveness = |
| new AppInfoWithLiveness( |
| appInfo, |
| SetUtils.mapIdentityHashSet(liveTypes.getItems(), DexProgramClass::getType), |
| SetUtils.mapIdentityHashSet( |
| liveAnnotations.getItems(), DexAnnotation::getAnnotationType), |
| Collections.unmodifiableSet(instantiatedAppServices), |
| SetUtils.mapIdentityHashSet(instantiatedTypes.getItems(), DexProgramClass::getType), |
| Enqueuer.toSortedDescriptorSet(targetedMethods.getItems()), |
| ImmutableSortedSet.copyOf(DexMethod::slowCompareTo, bootstrapMethods), |
| ImmutableSortedSet.copyOf(DexMethod::slowCompareTo, methodsTargetedByInvokeDynamic), |
| ImmutableSortedSet.copyOf( |
| DexMethod::slowCompareTo, virtualMethodsTargetedByInvokeDirect), |
| toSortedDescriptorSet(liveMethods.getItems()), |
| // Filter out library fields and pinned fields, because these are read by default. |
| fieldAccessInfoCollection, |
| instanceFieldsWrittenOnlyInEnclosingInstanceInitializers(), |
| staticFieldsWrittenOnlyInEnclosingStaticInitializer(), |
| // TODO(b/132593519): Do we require these sets to be sorted for determinism? |
| toImmutableSortedMap(virtualInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(interfaceInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(superInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(directInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(staticInvokes, PresortedComparable::slowCompare), |
| callSites, |
| ImmutableSortedSet.copyOf(DexMethod::slowCompareTo, brokenSuperInvokes), |
| pinnedItems, |
| rootSet.mayHaveSideEffects, |
| rootSet.noSideEffects, |
| rootSet.assumedValues, |
| rootSet.alwaysInline, |
| rootSet.forceInline, |
| rootSet.neverInline, |
| rootSet.keepConstantArguments, |
| rootSet.keepUnusedArguments, |
| rootSet.neverClassInline, |
| rootSet.neverMerge, |
| rootSet.neverPropagateValue, |
| joinIdentifierNameStrings(rootSet.identifierNameStrings, identifierNameStrings), |
| Collections.emptySet(), |
| Collections.emptyMap(), |
| Collections.emptyMap(), |
| SetUtils.mapIdentityHashSet( |
| instantiatedInterfaceTypes.getItems(), DexProgramClass::getType)); |
| appInfo.markObsolete(); |
| return appInfoWithLiveness; |
| } |
| |
| private static <T extends PresortedComparable<T>> SortedSet<T> toSortedDescriptorSet( |
| Set<? extends KeyedDexItem<T>> set) { |
| ImmutableSortedSet.Builder<T> builder = |
| new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompareTo); |
| for (KeyedDexItem<T> item : set) { |
| builder.add(item.getKey()); |
| } |
| return builder.build(); |
| } |
| |
| private static Object2BooleanMap<DexReference> joinIdentifierNameStrings( |
| Set<DexReference> explicit, Set<DexReference> implicit) { |
| Object2BooleanMap<DexReference> result = new Object2BooleanArrayMap<>(); |
| for (DexReference e : explicit) { |
| result.putIfAbsent(e, true); |
| } |
| for (DexReference i : implicit) { |
| result.putIfAbsent(i, false); |
| } |
| return result; |
| } |
| |
| private void trace(ExecutorService executorService, Timing timing) throws ExecutionException { |
| timing.begin("Grow the tree."); |
| try { |
| while (true) { |
| long numOfLiveItems = (long) liveTypes.items.size(); |
| numOfLiveItems += (long) liveMethods.items.size(); |
| numOfLiveItems += (long) liveFields.items.size(); |
| while (!workList.isEmpty()) { |
| Action action = workList.poll(); |
| switch (action.kind) { |
| case MARK_INSTANTIATED: |
| processNewlyInstantiatedClass((DexProgramClass) action.target, action.reason); |
| break; |
| case MARK_REACHABLE_FIELD: |
| markInstanceFieldAsReachable((DexEncodedField) action.target, action.reason); |
| break; |
| case MARK_REACHABLE_DIRECT: |
| markNonStaticDirectMethodAsReachable((DexMethod) action.target, action.reason); |
| break; |
| case MARK_REACHABLE_VIRTUAL: |
| markVirtualMethodAsReachable((DexMethod) action.target, false, action.reason); |
| break; |
| case MARK_REACHABLE_INTERFACE: |
| markVirtualMethodAsReachable((DexMethod) action.target, true, action.reason); |
| break; |
| case MARK_REACHABLE_SUPER: |
| markSuperMethodAsReachable((DexMethod) action.target, |
| (DexEncodedMethod) action.context); |
| break; |
| case MARK_METHOD_KEPT: |
| markMethodAsKept((DexEncodedMethod) action.target, action.reason); |
| break; |
| case MARK_FIELD_KEPT: |
| markFieldAsKept((DexEncodedField) action.target, action.reason); |
| break; |
| case MARK_METHOD_LIVE: |
| markMethodAsLive(((DexEncodedMethod) action.target), action.reason); |
| break; |
| default: |
| throw new IllegalArgumentException("" + action.kind); |
| } |
| } |
| |
| // Continue fix-point processing if -if rules are enabled by items that newly became live. |
| long numOfLiveItemsAfterProcessing = (long) liveTypes.items.size(); |
| numOfLiveItemsAfterProcessing += (long) liveMethods.items.size(); |
| numOfLiveItemsAfterProcessing += (long) liveFields.items.size(); |
| if (numOfLiveItemsAfterProcessing > numOfLiveItems) { |
| // Build the mapping of active if rules. We use a single collection of if-rules to allow |
| // removing if rules that have a constant sequent keep rule when they materialize. |
| if (activeIfRules == null) { |
| activeIfRules = new HashMap<>(); |
| IfRuleClassPartEquivalence equivalence = new IfRuleClassPartEquivalence(); |
| for (ProguardIfRule ifRule : rootSet.ifRules) { |
| Wrapper<ProguardIfRule> wrap = equivalence.wrap(ifRule); |
| activeIfRules.computeIfAbsent(wrap, ignore -> new LinkedHashSet<>()).add(ifRule); |
| } |
| } |
| RootSetBuilder consequentSetBuilder = new RootSetBuilder(appView, null); |
| IfRuleEvaluator ifRuleEvaluator = |
| new IfRuleEvaluator( |
| appView, |
| executorService, |
| activeIfRules, |
| liveFields.getItems(), |
| liveMethods.getItems(), |
| liveTypes.getItems(), |
| mode, |
| consequentSetBuilder, |
| targetedMethods.getItems()); |
| ConsequentRootSet consequentRootSet = ifRuleEvaluator.run(); |
| // TODO(b/132600955): This modifies the root set. Should the consequent be persistent? |
| rootSet.addConsequentRootSet(consequentRootSet); |
| enqueueRootItems(consequentRootSet.noShrinking); |
| // TODO(b/132828740): Seems incorrect that the precondition is not always met here. |
| consequentRootSet.dependentNoShrinking.forEach( |
| (precondition, dependentItems) -> enqueueRootItems(dependentItems)); |
| // Check for compatibility rules indicating that the holder must be implicitly kept. |
| if (forceProguardCompatibility) { |
| consequentRootSet.dependentKeepClassCompatRule.forEach( |
| (precondition, compatRules) -> { |
| assert precondition.isDexType(); |
| DexClass preconditionHolder = appView.definitionFor(precondition.asDexType()); |
| compatEnqueueHolderIfDependentNonStaticMember(preconditionHolder, compatRules); |
| }); |
| } |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| } |
| |
| // Continue fix-point processing while there are additional work items to ensure items that |
| // are passed to Java reflections are traced. |
| if (!pendingReflectiveUses.isEmpty()) { |
| pendingReflectiveUses.forEach(this::handleReflectiveBehavior); |
| pendingReflectiveUses.clear(); |
| } |
| if (!proguardCompatibilityWorkList.isEmpty()) { |
| proguardCompatibilityWorkList.transferTo(workList, liveMethods::add); |
| } |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| |
| // Notify each analysis that a fixpoint has been reached, and give each analysis an |
| // opportunity to add items to the worklist. |
| analyses.forEach(analysis -> analysis.notifyFixpoint(this, workList)); |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| |
| // Reached the fixpoint. |
| break; |
| } |
| |
| if (Log.ENABLED) { |
| Set<DexEncodedMethod> allLive = Sets.newIdentityHashSet(); |
| for (Entry<DexProgramClass, SetWithStoredReason<DexEncodedMethod>> entry : |
| reachableVirtualMethods.entrySet()) { |
| allLive.addAll(entry.getValue().getItems()); |
| } |
| Set<DexEncodedMethod> reachableNotLive = Sets.difference(allLive, liveMethods.getItems()); |
| Log.debug(getClass(), "%s methods are reachable but not live", reachableNotLive.size()); |
| Log.info(getClass(), "Only reachable: %s", reachableNotLive); |
| Set<DexProgramClass> liveButNotInstantiated = |
| Sets.difference(liveTypes.getItems(), instantiatedTypes.getItems()); |
| Log.debug(getClass(), "%s classes are live but not instantiated", |
| liveButNotInstantiated.size()); |
| Log.info(getClass(), "Live but not instantiated: %s", liveButNotInstantiated); |
| SetView<DexEncodedMethod> targetedButNotLive = Sets |
| .difference(targetedMethods.getItems(), liveMethods.getItems()); |
| Log.debug(getClass(), "%s methods are targeted but not live", targetedButNotLive.size()); |
| Log.info(getClass(), "Targeted but not live: %s", targetedButNotLive); |
| } |
| } finally { |
| timing.end(); |
| } |
| unpinLambdaMethods(); |
| } |
| |
| private void unpinLambdaMethods() { |
| for (DexMethod method : lambdaMethodsTargetedByInvokeDynamic) { |
| pinnedItems.remove(method); |
| rootSet.prune(method); |
| } |
| lambdaMethodsTargetedByInvokeDynamic.clear(); |
| } |
| |
| private void markMethodAsKept(DexEncodedMethod target, KeepReason reason) { |
| DexMethod method = target.method; |
| DexProgramClass holder = getProgramClassOrNull(method.holder); |
| if (holder == null) { |
| return; |
| } |
| if (target.isVirtualMethod()) { |
| // A virtual method. Mark it as reachable so that subclasses, if instantiated, keep |
| // their overrides. However, we don't mark it live, as a keep rule might not imply that |
| // the corresponding class is live. |
| if (!holder.isInterface()) { |
| workList.enqueueMarkReachableVirtualAction(method, reason); |
| } else { |
| workList.enqueueMarkReachableInterfaceAction(method, reason); |
| // Reachability for default methods is based on live subtypes in general. For keep rules, |
| // we need special handling as we essentially might have live subtypes that are outside of |
| // the current compilation unit. Keep either the default-method or its implementation |
| // method. |
| // TODO(b/120959039): Codify the kept-graph expectations for these cases in tests. |
| if (target.isNonAbstractVirtualMethod()) { |
| markVirtualMethodAsLive(holder, target, reason); |
| } else { |
| DexEncodedMethod implementation = target.getDefaultInterfaceMethodImplementation(); |
| if (implementation != null) { |
| DexProgramClass companion = getProgramClassOrNull(implementation.method.holder); |
| markTypeAsLive(companion, graphReporter.reportCompanionClass(holder, companion)); |
| markVirtualMethodAsLive( |
| companion, |
| implementation, |
| graphReporter.reportCompanionMethod(target, implementation)); |
| } |
| } |
| } |
| } else { |
| markMethodAsTargeted(holder, target, reason); |
| markDirectStaticOrConstructorMethodAsLive(holder, target, reason); |
| } |
| } |
| |
| private void markFieldAsKept(DexEncodedField target, KeepReason reason) { |
| DexProgramClass clazz = getProgramClassOrNull(target.field.holder); |
| if (clazz == null) { |
| return; |
| } |
| if (target.accessFlags.isStatic()) { |
| markStaticFieldAsLive(target, reason); |
| } else { |
| markInstanceFieldAsReachable(target, reason); |
| } |
| } |
| |
| private boolean shouldMarkLibraryMethodOverrideAsReachable( |
| DexProgramClass clazz, DexEncodedMethod method) { |
| assert method.isVirtualMethod(); |
| |
| if (appView.isClassEscapingIntoLibrary(clazz.type)) { |
| return true; |
| } |
| |
| // If there is a subtype of `clazz` that escapes into the library and does not override `method` |
| // then we need to mark the method as being reachable. |
| Deque<DexType> worklist = new ArrayDeque<>(appView.appInfo().allImmediateSubtypes(clazz.type)); |
| |
| Set<DexType> visited = Sets.newIdentityHashSet(); |
| visited.addAll(worklist); |
| |
| while (!worklist.isEmpty()) { |
| DexClass current = appView.definitionFor(worklist.removeFirst()); |
| if (current == null) { |
| continue; |
| } |
| |
| assert visited.contains(current.type); |
| |
| if (current.lookupVirtualMethod(method.method) != null) { |
| continue; |
| } |
| |
| if (appView.isClassEscapingIntoLibrary(current.type)) { |
| return true; |
| } |
| |
| for (DexType subtype : appView.appInfo().allImmediateSubtypes(current.type)) { |
| if (visited.add(subtype)) { |
| worklist.add(subtype); |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| private void markMethodAsLive(DexEncodedMethod method, KeepReason reason) { |
| assert liveMethods.contains(method); |
| |
| DexProgramClass clazz = getProgramClassOrNull(method.method.holder); |
| if (clazz == null) { |
| return; |
| } |
| |
| collectProguardCompatibilityRule(reason); |
| |
| Set<DexEncodedMethod> superCallTargets = superInvokeDependencies.get(method); |
| if (superCallTargets != null) { |
| for (DexEncodedMethod superCallTarget : superCallTargets) { |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Found super invoke constraint on `%s`.", superCallTarget.method); |
| } |
| DexProgramClass targetClass = getProgramClassOrNull(superCallTarget.method.holder); |
| assert targetClass != null; |
| if (targetClass != null) { |
| markMethodAsTargeted( |
| targetClass, superCallTarget, KeepReason.invokedViaSuperFrom(method)); |
| markVirtualMethodAsLive( |
| targetClass, superCallTarget, KeepReason.invokedViaSuperFrom(method)); |
| } |
| } |
| } |
| markParameterAndReturnTypesAsLive(method); |
| if (appView.definitionFor(method.method.holder).isProgramClass()) { |
| processAnnotations(method, method.annotations.annotations); |
| method.parameterAnnotationsList.forEachAnnotation( |
| annotation -> processAnnotation(method, annotation)); |
| } |
| method.registerCodeReferences(new UseRegistry(options.itemFactory, clazz, method)); |
| |
| // Add all dependent members to the workqueue. |
| enqueueRootItems(rootSet.getDependentItems(method)); |
| |
| // Notify analyses. |
| analyses.forEach(analysis -> analysis.processNewlyLiveMethod(method)); |
| } |
| |
| private void markParameterAndReturnTypesAsLive(DexEncodedMethod method) { |
| for (DexType parameterType : method.method.proto.parameters.values) { |
| markTypeAsLive( |
| parameterType, clazz -> graphReporter.reportClassReferencedFrom(clazz, method)); |
| } |
| markTypeAsLive( |
| method.method.proto.returnType, |
| clazz -> graphReporter.reportClassReferencedFrom(clazz, method)); |
| } |
| |
| private void collectProguardCompatibilityRule(KeepReason reason) { |
| if (reason.isDueToProguardCompatibility() && compatibility != null) { |
| compatibility.addRule(reason.getProguardKeepRule()); |
| } |
| } |
| |
| private void markClassAsInstantiatedWithReason(DexProgramClass clazz, KeepReason reason) { |
| workList.enqueueMarkInstantiatedAction(clazz, reason); |
| if (clazz.hasDefaultInitializer()) { |
| workList.enqueueMarkReachableDirectAction(clazz.getDefaultInitializer().method, reason); |
| } |
| } |
| |
| private void markClassAsInstantiatedWithCompatRule(DexProgramClass clazz) { |
| ProguardKeepRule rule = ProguardConfigurationUtils.buildDefaultInitializerKeepRule(clazz); |
| KeepReason reason = KeepReason.dueToProguardCompatibilityKeepRule(rule); |
| if (clazz.isInterface() && !clazz.accessFlags.isAnnotation()) { |
| markInterfaceAsInstantiated(clazz, reason); |
| return; |
| } |
| proguardCompatibilityWorkList.enqueueMarkInstantiatedAction(clazz, reason); |
| if (clazz.hasDefaultInitializer()) { |
| proguardCompatibilityWorkList.enqueueMarkReachableDirectAction( |
| clazz.getDefaultInitializer().method, reason); |
| } |
| } |
| |
| private void markMethodAsLiveWithCompatRule(DexProgramClass clazz, DexEncodedMethod method) { |
| proguardCompatibilityWorkList.enqueueMarkMethodLiveAction( |
| clazz, |
| method, |
| KeepReason.dueToProguardCompatibilityKeepRule( |
| ProguardConfigurationUtils.buildMethodKeepRule(clazz, method))); |
| } |
| |
| private void handleReflectiveBehavior(DexEncodedMethod method) { |
| DexType originHolder = method.method.holder; |
| Origin origin = appInfo.originFor(originHolder); |
| IRCode code = method.buildIR(appView, origin); |
| InstructionIterator iterator = code.instructionIterator(); |
| while (iterator.hasNext()) { |
| Instruction instruction = iterator.next(); |
| handleReflectiveBehavior(method, instruction); |
| } |
| } |
| |
| private void handleReflectiveBehavior(DexEncodedMethod method, Instruction instruction) { |
| if (!instruction.isInvokeMethod()) { |
| return; |
| } |
| InvokeMethod invoke = instruction.asInvokeMethod(); |
| DexMethod invokedMethod = invoke.getInvokedMethod(); |
| DexItemFactory dexItemFactory = appView.dexItemFactory(); |
| if (invokedMethod == dexItemFactory.classMethods.newInstance) { |
| handleJavaLangClassNewInstance(method, invoke); |
| return; |
| } |
| if (invokedMethod == dexItemFactory.constructorMethods.newInstance) { |
| handleJavaLangReflectConstructorNewInstance(method, invoke); |
| return; |
| } |
| if (invokedMethod == dexItemFactory.enumMethods.valueOf) { |
| handleJavaLangEnumValueOf(method, invoke); |
| return; |
| } |
| if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) { |
| handleJavaLangReflectProxyNewProxyInstance(method, invoke); |
| return; |
| } |
| if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) { |
| handleServiceLoaderInvocation(method, invoke); |
| return; |
| } |
| if (!isReflectionMethod(dexItemFactory, invokedMethod)) { |
| return; |
| } |
| DexReference identifierItem = identifyIdentifier(invoke, appView); |
| if (identifierItem == null) { |
| return; |
| } |
| if (identifierItem.isDexType()) { |
| DexProgramClass clazz = getProgramClassOrNull(identifierItem.asDexType()); |
| if (clazz == null) { |
| return; |
| } |
| if (!clazz.isInterface()) { |
| markInstantiated(clazz, KeepReason.reflectiveUseIn(method)); |
| if (clazz.hasDefaultInitializer()) { |
| DexEncodedMethod initializer = clazz.getDefaultInitializer(); |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| markMethodAsTargeted(clazz, initializer, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, initializer, reason); |
| } |
| } |
| } else if (identifierItem.isDexField()) { |
| DexField field = identifierItem.asDexField(); |
| DexProgramClass clazz = getProgramClassOrNull(field.holder); |
| if (clazz == null) { |
| return; |
| } |
| DexEncodedField encodedField = appView.definitionFor(field); |
| if (encodedField == null) { |
| return; |
| } |
| // Normally, we generate a -keepclassmembers rule for the field, such that the field is only |
| // kept if it is a static field, or if the holder or one of its subtypes are instantiated. |
| // However, if the invoked method is a field updater, then we always need to keep instance |
| // fields since the creation of a field updater throws a NoSuchFieldException if the field |
| // is not present. |
| boolean keepClass = |
| !encodedField.accessFlags.isStatic() |
| && dexItemFactory.atomicFieldUpdaterMethods.isFieldUpdater(invokedMethod); |
| if (keepClass) { |
| markInstantiated(clazz, KeepReason.reflectiveUseIn(method)); |
| } |
| markFieldAsKept(encodedField, KeepReason.reflectiveUseIn(method)); |
| // Fields accessed by reflection is marked as both read and written. |
| registerFieldRead(encodedField.field, method); |
| registerFieldWrite(encodedField.field, method); |
| |
| } else { |
| assert identifierItem.isDexMethod(); |
| DexMethod targetedMethod = identifierItem.asDexMethod(); |
| DexProgramClass clazz = getProgramClassOrNull(targetedMethod.holder); |
| if (clazz == null) { |
| return; |
| } |
| DexEncodedMethod encodedMethod = appView.definitionFor(targetedMethod); |
| if (encodedMethod == null) { |
| return; |
| } |
| if (encodedMethod.accessFlags.isStatic() || encodedMethod.accessFlags.isConstructor()) { |
| markDirectStaticOrConstructorMethodAsLive( |
| clazz, encodedMethod, KeepReason.reflectiveUseIn(method)); |
| } else { |
| markVirtualMethodAsLive(clazz, encodedMethod, KeepReason.reflectiveUseIn(method)); |
| } |
| } |
| } |
| |
| /** Handles reflective uses of {@link Class#newInstance()}. */ |
| private void handleJavaLangClassNewInstance(DexEncodedMethod method, InvokeMethod invoke) { |
| if (!invoke.isInvokeVirtual()) { |
| assert false; |
| return; |
| } |
| |
| DexType instantiatedType = |
| ConstantValueUtils.getDexTypeRepresentedByValue( |
| invoke.asInvokeVirtual().getReceiver(), appView); |
| if (instantiatedType == null || !instantiatedType.isClassType()) { |
| // Give up, we can't tell which class is being instantiated, or the type is not a class type. |
| // The latter should not happen in practice. |
| return; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(instantiatedType); |
| if (clazz == null) { |
| return; |
| } |
| DexEncodedMethod defaultInitializer = clazz.getDefaultInitializer(); |
| if (defaultInitializer != null) { |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| markClassAsInstantiatedWithReason(clazz, reason); |
| markMethodAsTargeted(clazz, defaultInitializer, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, defaultInitializer, reason); |
| } |
| } |
| |
| /** Handles reflective uses of {@link java.lang.reflect.Constructor#newInstance(Object...)}. */ |
| private void handleJavaLangReflectConstructorNewInstance( |
| DexEncodedMethod method, InvokeMethod invoke) { |
| if (!invoke.isInvokeVirtual()) { |
| assert false; |
| return; |
| } |
| |
| Value constructorValue = invoke.asInvokeVirtual().getReceiver().getAliasedValue(); |
| if (constructorValue.isPhi() || !constructorValue.definition.isInvokeVirtual()) { |
| // Give up, we can't tell which class is being instantiated. |
| return; |
| } |
| |
| InvokeVirtual constructorDefinition = constructorValue.definition.asInvokeVirtual(); |
| if (constructorDefinition.getInvokedMethod() |
| != appView.dexItemFactory().classMethods.getDeclaredConstructor) { |
| // Give up, we can't tell which constructor is being invoked. |
| return; |
| } |
| |
| DexType instantiatedType = |
| ConstantValueUtils.getDexTypeRepresentedByValue( |
| constructorDefinition.getReceiver(), appView); |
| if (instantiatedType == null || !instantiatedType.isClassType()) { |
| // Give up, we can't tell which constructor is being invoked, or the type is not a class type. |
| // The latter should not happen in practice. |
| return; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(instantiatedType); |
| if (clazz == null) { |
| return; |
| } |
| Value parametersValue = constructorDefinition.inValues().get(1); |
| if (parametersValue.isPhi() || !parametersValue.definition.isNewArrayEmpty()) { |
| // Give up, we can't tell which constructor is being invoked. |
| return; |
| } |
| |
| Value parametersSizeValue = parametersValue.definition.asNewArrayEmpty().size(); |
| if (parametersSizeValue.isPhi() || !parametersSizeValue.definition.isConstNumber()) { |
| // Give up, we can't tell which constructor is being invoked. |
| return; |
| } |
| |
| DexEncodedMethod initializer = null; |
| |
| int parametersSize = parametersSizeValue.definition.asConstNumber().getIntValue(); |
| if (parametersSize == 0) { |
| initializer = clazz.getDefaultInitializer(); |
| } else { |
| DexType[] parameterTypes = new DexType[parametersSize]; |
| int missingIndices = parametersSize; |
| for (Instruction user : parametersValue.uniqueUsers()) { |
| if (user.isArrayPut()) { |
| ArrayPut arrayPutInstruction = user.asArrayPut(); |
| if (arrayPutInstruction.array() != parametersValue) { |
| return; |
| } |
| |
| Value indexValue = arrayPutInstruction.index(); |
| if (indexValue.isPhi() || !indexValue.definition.isConstNumber()) { |
| return; |
| } |
| int index = indexValue.definition.asConstNumber().getIntValue(); |
| if (index >= parametersSize) { |
| return; |
| } |
| |
| DexType type = |
| ConstantValueUtils.getDexTypeRepresentedByValue(arrayPutInstruction.value(), appView); |
| if (type == null) { |
| return; |
| } |
| |
| if (parameterTypes[index] == type) { |
| continue; |
| } |
| if (parameterTypes[index] != null) { |
| return; |
| } |
| parameterTypes[index] = type; |
| missingIndices--; |
| } |
| } |
| |
| if (missingIndices == 0) { |
| initializer = clazz.getInitializer(parameterTypes); |
| } |
| } |
| |
| if (initializer != null) { |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| markClassAsInstantiatedWithReason(clazz, reason); |
| markMethodAsTargeted(clazz, initializer, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, initializer, reason); |
| } |
| } |
| |
| /** |
| * Handles reflective uses of {@link java.lang.reflect.Proxy#newProxyInstance(ClassLoader, |
| * Class[], InvocationHandler)}. |
| */ |
| private void handleJavaLangReflectProxyNewProxyInstance( |
| DexEncodedMethod method, InvokeMethod invoke) { |
| if (!invoke.isInvokeStatic()) { |
| assert false; |
| return; |
| } |
| |
| Value interfacesValue = invoke.arguments().get(1); |
| if (interfacesValue.isPhi() || !interfacesValue.definition.isNewArrayEmpty()) { |
| // Give up, we can't tell which interfaces the proxy implements. |
| return; |
| } |
| |
| for (Instruction user : interfacesValue.uniqueUsers()) { |
| if (!user.isArrayPut()) { |
| continue; |
| } |
| |
| ArrayPut arrayPut = user.asArrayPut(); |
| DexType type = ConstantValueUtils.getDexTypeRepresentedByValue(arrayPut.value(), appView); |
| if (type == null || !type.isClassType()) { |
| continue; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null && clazz.isInterface()) { |
| // Add this interface to the set of pinned items to ensure that we do not merge the |
| // interface into its subtype and to ensure that the devirtualizer does not perform illegal |
| // rewritings of invoke-interface instructions into invoke-virtual instructions. |
| pinnedItems.add(clazz.type); |
| } |
| } |
| } |
| |
| private void handleJavaLangEnumValueOf(DexEncodedMethod method, InvokeMethod invoke) { |
| // 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. |
| if (invoke.inValues().get(0).isConstClass()) { |
| DexClass clazz = |
| appView.definitionFor(invoke.inValues().get(0).definition.asConstClass().getValue()); |
| if (clazz.accessFlags.isEnum() && clazz.superType == appView.dexItemFactory().enumType) { |
| markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(method)); |
| } |
| } |
| } |
| |
| private void handleServiceLoaderInvocation(DexEncodedMethod method, InvokeMethod invoke) { |
| if (invoke.inValues().size() == 0) { |
| // Should never happen. |
| return; |
| } |
| |
| Value argument = invoke.inValues().get(0).getAliasedValue(); |
| if (!argument.isPhi() && argument.definition.isConstClass()) { |
| DexType serviceType = argument.definition.asConstClass().getValue(); |
| if (!appView.appServices().allServiceTypes().contains(serviceType)) { |
| // Should never happen. |
| if (Log.ENABLED) { |
| options.reporter.warning( |
| new StringDiagnostic( |
| "The type `" |
| + serviceType.toSourceString() |
| + "` is being passed to the method `" |
| + invoke.getInvokedMethod().toSourceString() |
| + "`, but was not found in `META-INF/services/`.", |
| appInfo.originFor(method.method.holder))); |
| } |
| return; |
| } |
| |
| handleServiceInstantiation(serviceType, KeepReason.reflectiveUseIn(method)); |
| } else { |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| for (DexType serviceType : appView.appServices().allServiceTypes()) { |
| handleServiceInstantiation(serviceType, reason); |
| } |
| } |
| } |
| |
| private void handleServiceInstantiation(DexType serviceType, KeepReason reason) { |
| instantiatedAppServices.add(serviceType); |
| |
| List<DexType> serviceImplementationTypes = |
| appView.appServices().serviceImplementationsFor(serviceType); |
| for (DexType serviceImplementationType : serviceImplementationTypes) { |
| if (!serviceImplementationType.isClassType()) { |
| // Should never happen. |
| continue; |
| } |
| |
| DexProgramClass serviceImplementationClass = getProgramClassOrNull(serviceImplementationType); |
| if (serviceImplementationClass != null && serviceImplementationClass.isProgramClass()) { |
| markClassAsInstantiatedWithReason(serviceImplementationClass, reason); |
| } |
| } |
| } |
| |
| private static class SetWithReason<T> { |
| |
| private final Set<T> items = Sets.newIdentityHashSet(); |
| |
| private final BiConsumer<T, KeepReason> register; |
| |
| public SetWithReason(BiConsumer<T, KeepReason> register) { |
| this.register = register; |
| } |
| |
| boolean add(T item, KeepReason reason) { |
| register.accept(item, reason); |
| return items.add(item); |
| } |
| |
| boolean contains(T item) { |
| return items.contains(item); |
| } |
| |
| Set<T> getItems() { |
| return Collections.unmodifiableSet(items); |
| } |
| } |
| |
| private static class SetWithStoredReason<T> extends SetWithReason<T> { |
| private final Map<T, Set<KeepReason>> reasons; |
| |
| static <T> SetWithStoredReason<T> create() { |
| final Map<T, Set<KeepReason>> reasons = new IdentityHashMap<>(); |
| return new SetWithStoredReason<>(register(reasons), reasons); |
| } |
| |
| private SetWithStoredReason( |
| BiConsumer<T, KeepReason> register, Map<T, Set<KeepReason>> reasons) { |
| super(register); |
| this.reasons = reasons; |
| } |
| |
| private static <T> BiConsumer<T, KeepReason> register(Map<T, Set<KeepReason>> reasons) { |
| return (T item, KeepReason reason) -> |
| reasons.computeIfAbsent(item, k -> new HashSet<>()).add(reason); |
| } |
| |
| public Set<KeepReason> getReasons(T item) { |
| return reasons.get(item); |
| } |
| } |
| |
| private static final class TargetWithContext<T extends Descriptor<?, T>> { |
| |
| private final T target; |
| private final DexEncodedMethod context; |
| |
| private TargetWithContext(T target, DexEncodedMethod context) { |
| this.target = target; |
| this.context = context; |
| } |
| |
| public T getTarget() { |
| return target; |
| } |
| |
| public DexEncodedMethod getContext() { |
| return context; |
| } |
| |
| @Override |
| public int hashCode() { |
| return target.hashCode() * 31 + context.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof TargetWithContext)) { |
| return false; |
| } |
| TargetWithContext other = (TargetWithContext) obj; |
| return (this.target == other.target) && (this.context == other.context); |
| } |
| } |
| |
| private class AnnotationReferenceMarker implements IndexedItemCollection { |
| |
| private final DexItem annotationHolder; |
| private final DexItemFactory dexItemFactory; |
| private final KeepReason reason; |
| |
| private AnnotationReferenceMarker( |
| DexItem annotationHolder, DexItemFactory dexItemFactory, KeepReason reason) { |
| this.annotationHolder = annotationHolder; |
| this.dexItemFactory = dexItemFactory; |
| this.reason = reason; |
| } |
| |
| @Override |
| public boolean addClass(DexProgramClass dexProgramClass) { |
| return false; |
| } |
| |
| @Override |
| public boolean addField(DexField field) { |
| DexClass holder = appView.definitionFor(field.holder); |
| if (holder == null) { |
| return false; |
| } |
| DexEncodedField target = holder.lookupStaticField(field); |
| if (target != null) { |
| // There is no dispatch on annotations, so only keep what is directly referenced. |
| if (target.field == field) { |
| if (!registerFieldRead(field, DexEncodedMethod.ANNOTATION_REFERENCE)) { |
| return false; |
| } |
| markStaticFieldAsLive(target, KeepReason.referencedInAnnotation(annotationHolder)); |
| // When an annotation has a field of an enum type with a default value then Java VM |
| // will use the values() method on that enum class. |
| if (options.isGeneratingClassFiles() |
| && annotationHolder == dexItemFactory.annotationDefault) { |
| DexClass clazz = appView.definitionFor(field.type); |
| if (clazz != null && clazz.isProgramClass() && clazz.accessFlags.isEnum()) { |
| markEnumValuesAsReachable(clazz, KeepReason.referencedInAnnotation(annotationHolder)); |
| } |
| } |
| } |
| } else { |
| target = holder.lookupInstanceField(field); |
| // There is no dispatch on annotations, so only keep what is directly referenced. |
| if (target != null && target.field != field) { |
| markInstanceFieldAsReachable(target, KeepReason.referencedInAnnotation(annotationHolder)); |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean addMethod(DexMethod method) { |
| DexProgramClass holder = getProgramClassOrNull(method.holder); |
| if (holder == null) { |
| return false; |
| } |
| DexEncodedMethod target = holder.lookupDirectMethod(method); |
| if (target != null) { |
| // There is no dispatch on annotations, so only keep what is directly referenced. |
| if (target.method == method) { |
| markDirectStaticOrConstructorMethodAsLive( |
| holder, target, KeepReason.referencedInAnnotation(annotationHolder)); |
| } |
| } else { |
| target = holder.lookupVirtualMethod(method); |
| // There is no dispatch on annotations, so only keep what is directly referenced. |
| if (target != null && target.method == method) { |
| markMethodAsTargeted(holder, target, KeepReason.referencedInAnnotation(annotationHolder)); |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean addString(DexString string) { |
| return false; |
| } |
| |
| @Override |
| public boolean addProto(DexProto proto) { |
| return false; |
| } |
| |
| @Override |
| public boolean addCallSite(DexCallSite callSite) { |
| return false; |
| } |
| |
| @Override |
| public boolean addMethodHandle(DexMethodHandle methodHandle) { |
| return false; |
| } |
| |
| @Override |
| public boolean addType(DexType type) { |
| // Annotations can also contain the void type, which is not a class type, so filter it out |
| // here. |
| if (type != dexItemFactory.voidType) { |
| markTypeAsLive(type, reason); |
| } |
| return false; |
| } |
| } |
| |
| class GraphReporter { |
| |
| private EdgeKind reportPrecondition(KeepRuleGraphNode keepRuleGraphNode) { |
| if (keepRuleGraphNode.getPreconditions().isEmpty()) { |
| return EdgeKind.KeepRule; |
| } |
| for (GraphNode precondition : keepRuleGraphNode.getPreconditions()) { |
| reportEdge(precondition, keepRuleGraphNode, EdgeKind.KeepRulePrecondition); |
| } |
| return EdgeKind.ConditionalKeepRule; |
| } |
| |
| KeepReasonWitness reportKeepClass( |
| DexDefinition precondition, ProguardKeepRuleBase rule, DexProgramClass clazz) { |
| if (keptGraphConsumer == null) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| KeepRuleGraphNode ruleNode = getKeepRuleGraphNode(precondition, rule); |
| EdgeKind edgeKind = reportPrecondition(ruleNode); |
| return reportEdge(ruleNode, getClassGraphNode(clazz.type), edgeKind); |
| } |
| |
| KeepReasonWitness reportKeepClass( |
| DexDefinition precondition, Collection<ProguardKeepRuleBase> rules, DexProgramClass clazz) { |
| assert !rules.isEmpty(); |
| if (keptGraphConsumer != null) { |
| for (ProguardKeepRuleBase rule : rules) { |
| reportKeepClass(precondition, rule, clazz); |
| } |
| } |
| return KeepReasonWitness.INSTANCE; |
| } |
| |
| KeepReasonWitness reportKeepMethod( |
| DexDefinition precondition, ProguardKeepRuleBase rule, DexEncodedMethod method) { |
| if (keptGraphConsumer == null) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| KeepRuleGraphNode ruleNode = getKeepRuleGraphNode(precondition, rule); |
| EdgeKind edgeKind = reportPrecondition(ruleNode); |
| return reportEdge(ruleNode, getMethodGraphNode(method.method), edgeKind); |
| } |
| |
| KeepReasonWitness reportKeepMethod( |
| DexDefinition precondition, |
| Collection<ProguardKeepRuleBase> rules, |
| DexEncodedMethod method) { |
| assert !rules.isEmpty(); |
| if (keptGraphConsumer != null) { |
| for (ProguardKeepRuleBase rule : rules) { |
| reportKeepMethod(precondition, rule, method); |
| } |
| } |
| return KeepReasonWitness.INSTANCE; |
| } |
| |
| KeepReasonWitness reportKeepField( |
| DexDefinition precondition, ProguardKeepRuleBase rule, DexEncodedField field) { |
| if (keptGraphConsumer == null) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| KeepRuleGraphNode ruleNode = getKeepRuleGraphNode(precondition, rule); |
| EdgeKind edgeKind = reportPrecondition(ruleNode); |
| return reportEdge(ruleNode, getFieldGraphNode(field.field), edgeKind); |
| } |
| |
| KeepReasonWitness reportKeepField( |
| DexDefinition precondition, Collection<ProguardKeepRuleBase> rules, DexEncodedField field) { |
| assert !rules.isEmpty(); |
| if (keptGraphConsumer != null) { |
| for (ProguardKeepRuleBase rule : rules) { |
| reportKeepField(precondition, rule, field); |
| } |
| } |
| return KeepReasonWitness.INSTANCE; |
| } |
| |
| public KeepReasonWitness reportClassReferencedFrom( |
| DexProgramClass clazz, DexEncodedMethod method) { |
| if (keptGraphConsumer != null) { |
| MethodGraphNode source = getMethodGraphNode(method.method); |
| ClassGraphNode target = getClassGraphNode(clazz.type); |
| return reportEdge(source, target, EdgeKind.ReferencedFrom); |
| } |
| return KeepReasonWitness.INSTANCE; |
| } |
| |
| public KeepReasonWitness reportClassReferencedFrom( |
| DexProgramClass clazz, DexEncodedField field) { |
| if (keptGraphConsumer != null) { |
| FieldGraphNode source = getFieldGraphNode(field.field); |
| ClassGraphNode target = getClassGraphNode(clazz.type); |
| return reportEdge(source, target, EdgeKind.ReferencedFrom); |
| } |
| return KeepReasonWitness.INSTANCE; |
| } |
| |
| private KeepReason reportCompanionClass(DexProgramClass iface, DexProgramClass companion) { |
| assert iface.isInterface(); |
| assert InterfaceMethodRewriter.isCompanionClassType(companion.type); |
| if (keptGraphConsumer == null) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| return reportEdge( |
| getClassGraphNode(iface.type), |
| getClassGraphNode(companion.type), |
| EdgeKind.CompanionClass); |
| } |
| |
| private KeepReason reportCompanionMethod( |
| DexEncodedMethod definition, DexEncodedMethod implementation) { |
| assert InterfaceMethodRewriter.isCompanionClassType(implementation.method.holder); |
| if (keptGraphConsumer == null) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| return reportEdge( |
| getMethodGraphNode(definition.method), |
| getMethodGraphNode(implementation.method), |
| EdgeKind.CompanionMethod); |
| } |
| |
| private KeepReasonWitness reportEdge( |
| GraphNode source, GraphNode target, GraphEdgeInfo.EdgeKind kind) { |
| assert keptGraphConsumer != null; |
| keptGraphConsumer.acceptEdge(source, target, getEdgeInfo(kind)); |
| return KeepReasonWitness.INSTANCE; |
| } |
| } |
| |
| /** |
| * Sentinel value indicating that a keep reason has been reported. |
| * |
| * <p>Should only ever be returned by the register* function below. |
| */ |
| private static class KeepReasonWitness extends KeepReason { |
| |
| private static KeepReasonWitness INSTANCE = new KeepReasonWitness(); |
| |
| @Override |
| public EdgeKind edgeKind() { |
| throw new Unreachable(); |
| } |
| |
| @Override |
| public GraphNode getSourceNode(Enqueuer enqueuer) { |
| throw new Unreachable(); |
| } |
| } |
| |
| private boolean skipReporting(KeepReason reason) { |
| assert reason != null; |
| if (reason == KeepReasonWitness.INSTANCE) { |
| return true; |
| } |
| assert getSourceNode(reason) != null; |
| return keptGraphConsumer == null; |
| } |
| |
| private KeepReasonWitness registerType(DexType type, KeepReason reason) { |
| if (skipReporting(reason)) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| return registerEdge(getClassGraphNode(type), reason); |
| } |
| |
| private KeepReasonWitness registerInterface(DexProgramClass iface, KeepReason reason) { |
| assert iface.isInterface(); |
| if (skipReporting(reason)) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| return registerEdge(getClassGraphNode(iface.type), reason); |
| } |
| |
| private KeepReasonWitness registerClass(DexProgramClass clazz, KeepReason reason) { |
| if (skipReporting(reason)) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| return registerEdge(getClassGraphNode(clazz.type), reason); |
| } |
| |
| private KeepReasonWitness registerAnnotation(DexAnnotation annotation, KeepReason reason) { |
| if (skipReporting(reason)) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| return registerEdge(getAnnotationGraphNode(annotation.annotation.type), reason); |
| } |
| |
| private KeepReasonWitness registerMethod( |
| DexEncodedMethod method, Collection<KeepReason> reasons) { |
| assert !reasons.isEmpty(); |
| if (keptGraphConsumer != null) { |
| for (KeepReason reason : reasons) { |
| registerMethod(method, reason); |
| } |
| } |
| return KeepReasonWitness.INSTANCE; |
| } |
| |
| private KeepReasonWitness registerMethod(DexEncodedMethod method, KeepReason reason) { |
| if (skipReporting(reason)) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| if (reason.edgeKind() == EdgeKind.IsLibraryMethod && isNonProgramClass(method.method.holder)) { |
| // Don't report edges to actual library methods. |
| // TODO(b/120959039): This should be dead code once no library classes are ever enqueued. |
| return KeepReasonWitness.INSTANCE; |
| } |
| return registerEdge(getMethodGraphNode(method.method), reason); |
| } |
| |
| private KeepReasonWitness registerField(DexEncodedField field, KeepReason reason) { |
| if (skipReporting(reason)) { |
| return KeepReasonWitness.INSTANCE; |
| } |
| return registerEdge(getFieldGraphNode(field.field), reason); |
| } |
| |
| private KeepReasonWitness registerEdge(GraphNode target, KeepReason reason) { |
| assert !skipReporting(reason); |
| GraphNode sourceNode = getSourceNode(reason); |
| // TODO(b/120959039): Make sure we do have edges to nodes deriving library nodes! |
| if (!sourceNode.isLibraryNode()) { |
| GraphEdgeInfo edgeInfo = getEdgeInfo(reason); |
| keptGraphConsumer.acceptEdge(sourceNode, target, edgeInfo); |
| } |
| return KeepReasonWitness.INSTANCE; |
| } |
| |
| private boolean isNonProgramClass(DexType type) { |
| DexClass clazz = appView.definitionFor(type); |
| return clazz == null || clazz.isNotProgramClass(); |
| } |
| |
| private GraphNode getSourceNode(KeepReason reason) { |
| return reason.getSourceNode(this); |
| } |
| |
| public GraphNode getGraphNode(DexReference reference) { |
| if (reference.isDexType()) { |
| return getClassGraphNode(reference.asDexType()); |
| } |
| if (reference.isDexMethod()) { |
| return getMethodGraphNode(reference.asDexMethod()); |
| } |
| if (reference.isDexField()) { |
| return getFieldGraphNode(reference.asDexField()); |
| } |
| throw new Unreachable(); |
| } |
| |
| GraphEdgeInfo getEdgeInfo(KeepReason reason) { |
| return getEdgeInfo(reason.edgeKind()); |
| } |
| |
| GraphEdgeInfo getEdgeInfo(GraphEdgeInfo.EdgeKind kind) { |
| return reasonInfo.computeIfAbsent(kind, k -> new GraphEdgeInfo(k)); |
| } |
| |
| AnnotationGraphNode getAnnotationGraphNode(DexItem type) { |
| return annotationNodes.computeIfAbsent(type, t -> { |
| if (t instanceof DexType) { |
| return new AnnotationGraphNode(getClassGraphNode(((DexType) t))); |
| } |
| throw new Unimplemented("Incomplete support for annotation node on item: " + type.getClass()); |
| }); |
| } |
| |
| ClassGraphNode getClassGraphNode(DexType type) { |
| return classNodes.computeIfAbsent( |
| type, |
| t -> { |
| DexClass definition = appView.definitionFor(t); |
| return new ClassGraphNode( |
| definition != null && definition.isNotProgramClass(), |
| Reference.classFromDescriptor(t.toDescriptorString())); |
| }); |
| } |
| |
| MethodGraphNode getMethodGraphNode(DexMethod context) { |
| return methodNodes.computeIfAbsent( |
| context, |
| m -> { |
| DexClass holderDefinition = appView.definitionFor(context.holder); |
| Builder<TypeReference> builder = ImmutableList.builder(); |
| for (DexType param : m.proto.parameters.values) { |
| builder.add(Reference.typeFromDescriptor(param.toDescriptorString())); |
| } |
| return new MethodGraphNode( |
| holderDefinition != null && holderDefinition.isNotProgramClass(), |
| Reference.method( |
| Reference.classFromDescriptor(m.holder.toDescriptorString()), |
| m.name.toString(), |
| builder.build(), |
| m.proto.returnType.isVoidType() |
| ? null |
| : Reference.typeFromDescriptor(m.proto.returnType.toDescriptorString()))); |
| }); |
| } |
| |
| FieldGraphNode getFieldGraphNode(DexField context) { |
| return fieldNodes.computeIfAbsent( |
| context, |
| f -> { |
| DexClass holderDefinition = appView.definitionFor(context.holder); |
| return new FieldGraphNode( |
| holderDefinition != null && holderDefinition.isNotProgramClass(), |
| Reference.field( |
| Reference.classFromDescriptor(f.holder.toDescriptorString()), |
| f.name.toString(), |
| Reference.typeFromDescriptor(f.type.toDescriptorString()))); |
| }); |
| } |
| |
| // Due to the combined encoding of dependent rules, ala keepclassmembers and conditional keep |
| // rules the conversion to a keep-rule graph node can be one of three forms: |
| // 1. A non-dependent keep rule. In this case precondtion == null and the rule is not an if-rule. |
| // 2. A dependent keep rule. In this case precondtion != null and rule is not an if-rule. |
| // 3. A conditional keep rule. In this case rule is an if-rule, but precondition may or may not be |
| // null. In the non-null case, the precondition is the type the consequent may depend on, |
| // say T for the consequent "-keep T { f; }". It is *not* the precondition of the conditional |
| // rule. |
| KeepRuleGraphNode getKeepRuleGraphNode(DexDefinition precondition, ProguardKeepRuleBase rule) { |
| if (rule instanceof ProguardKeepRule) { |
| Set<GraphNode> preconditions = |
| precondition != null |
| ? Collections.singleton(getGraphNode(precondition.toReference())) |
| : Collections.emptySet(); |
| return ruleNodes.computeIfAbsent(rule, key -> new KeepRuleGraphNode(rule, preconditions)); |
| } |
| if (rule instanceof ProguardIfRule) { |
| ProguardIfRule ifRule = (ProguardIfRule) rule; |
| assert !ifRule.getPreconditions().isEmpty(); |
| return ruleNodes.computeIfAbsent( |
| ifRule, |
| key -> { |
| Set<GraphNode> preconditions = new HashSet<>(ifRule.getPreconditions().size()); |
| for (DexReference condition : ifRule.getPreconditions()) { |
| preconditions.add(getGraphNode(condition)); |
| } |
| return new KeepRuleGraphNode(ifRule, preconditions); |
| }); |
| } |
| throw new Unreachable("Unexpected type of keep rule: " + rule); |
| } |
| } |