| // Copyright (c) 2023, 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.optimize.accessmodification; |
| |
| import static com.android.tools.r8.dex.Constants.ACC_PRIVATE; |
| import static com.android.tools.r8.dex.Constants.ACC_PROTECTED; |
| import static com.android.tools.r8.dex.Constants.ACC_PUBLIC; |
| |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexMethodSignature; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo; |
| import com.android.tools.r8.graph.InnerClassAttribute; |
| import com.android.tools.r8.graph.MethodAccessFlags; |
| import com.android.tools.r8.graph.ProgramDefinition; |
| import com.android.tools.r8.graph.ProgramField; |
| import com.android.tools.r8.graph.ProgramMethod; |
| import com.android.tools.r8.optimize.accessmodification.AccessModifierTraversal.BottomUpTraversalState; |
| import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph; |
| import com.android.tools.r8.optimize.utils.ConcurrentNonProgramMethodsCollection; |
| import com.android.tools.r8.optimize.utils.NonProgramMethodsCollection; |
| import com.android.tools.r8.shaking.AppInfoWithLiveness; |
| import com.android.tools.r8.shaking.KeepMethodInfo; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.ListUtils; |
| import com.android.tools.r8.utils.OptionalBool; |
| import com.android.tools.r8.utils.ThreadUtils; |
| import com.android.tools.r8.utils.Timing; |
| import com.google.common.collect.BiMap; |
| import com.google.common.collect.HashBiMap; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| |
| public class AccessModifier { |
| |
| private final AppView<AppInfoWithLiveness> appView; |
| private final ImmediateProgramSubtypingInfo immediateSubtypingInfo; |
| private final AccessModifierLens.Builder lensBuilder = AccessModifierLens.builder(); |
| private final NonProgramMethodsCollection nonProgramMethodsCollection; |
| private final InternalOptions options; |
| |
| private AccessModifier(AppView<AppInfoWithLiveness> appView) { |
| this.appView = appView; |
| this.immediateSubtypingInfo = |
| ImmediateProgramSubtypingInfo.createWithDeterministicOrder(appView); |
| this.nonProgramMethodsCollection = |
| ConcurrentNonProgramMethodsCollection.createVirtualMethodsCollection(appView); |
| this.options = appView.options(); |
| } |
| |
| public static void run( |
| AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing) |
| throws ExecutionException { |
| timing.begin("Access modification"); |
| AccessModifierOptions accessModifierOptions = appView.options().getAccessModifierOptions(); |
| if (accessModifierOptions.isAccessModificationEnabled()) { |
| new AccessModifier(appView) |
| .processStronglyConnectedComponents(executorService) |
| .installLens(executorService, timing); |
| } |
| timing.end(); |
| } |
| |
| private AccessModifier processStronglyConnectedComponents(ExecutorService executorService) |
| throws ExecutionException { |
| // Compute the connected program classes and process the components in parallel. |
| List<Set<DexProgramClass>> stronglyConnectedComponents = |
| new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo) |
| .computeStronglyConnectedComponents(); |
| ThreadUtils.processItems( |
| stronglyConnectedComponents, |
| this::processStronglyConnectedComponent, |
| appView.options().getThreadingModule(), |
| executorService); |
| return this; |
| } |
| |
| private void processStronglyConnectedComponent(Set<DexProgramClass> stronglyConnectedComponent) { |
| // Perform a top-down traversal over the class hierarchy. |
| new AccessModifierTraversal( |
| appView, |
| immediateSubtypingInfo, |
| this, |
| AccessModifierNamingState.createInitialNamingState( |
| appView, stronglyConnectedComponent, nonProgramMethodsCollection)) |
| .run(ListUtils.sort(stronglyConnectedComponent, Comparator.comparing(DexClass::getType))); |
| } |
| |
| private void installLens(ExecutorService executorService, Timing timing) |
| throws ExecutionException { |
| if (!lensBuilder.isEmpty()) { |
| appView.rewriteWithLens(lensBuilder.build(appView), executorService, timing); |
| } |
| } |
| |
| // Publicizing of classes and members. |
| |
| void processClass( |
| DexProgramClass clazz, |
| AccessModifierNamingState namingState, |
| BottomUpTraversalState traversalState) { |
| publicizeClass(clazz, traversalState); |
| publicizeFields(clazz, traversalState); |
| publicizeMethods(clazz, namingState, traversalState); |
| } |
| |
| private void publicizeClass(DexProgramClass clazz, BottomUpTraversalState traversalState) { |
| if (isAccessModificationAllowed(clazz, traversalState) && !clazz.getAccessFlags().isPublic()) { |
| clazz.getAccessFlags().promoteToPublic(); |
| } |
| |
| // Update inner class attribute. |
| // TODO(b/285494837): Carry-over from the legacy access modifier. We should never publicize |
| // items unconditionally, but account for keep info. |
| InnerClassAttribute attr = clazz.getInnerClassAttributeForThisClass(); |
| if (attr != null) { |
| int accessFlags = ((attr.getAccess() | ACC_PUBLIC) & ~ACC_PRIVATE) & ~ACC_PROTECTED; |
| clazz.replaceInnerClassAttributeForThisClass( |
| new InnerClassAttribute( |
| accessFlags, attr.getInner(), attr.getOuter(), attr.getInnerName())); |
| } |
| } |
| |
| private void publicizeFields(DexProgramClass clazz, BottomUpTraversalState traversalState) { |
| clazz.forEachProgramField(field -> publicizeField(field, traversalState)); |
| } |
| |
| private void publicizeField(ProgramField field, BottomUpTraversalState traversalState) { |
| if (isAccessModificationAllowed(field, traversalState) && !field.getAccessFlags().isPublic()) { |
| field.getAccessFlags().promoteToPublic(); |
| } |
| } |
| |
| private void publicizeMethods( |
| DexProgramClass clazz, |
| AccessModifierNamingState namingState, |
| BottomUpTraversalState traversalState) { |
| // Create a local naming state to keep track of the methods present on the current class. |
| // Start by reserving the pinned method signatures on the current class. |
| BiMap<DexMethod, DexMethod> localNamingState = HashBiMap.create(); |
| clazz.forEachProgramMethod( |
| method -> { |
| if (!method.getDefinition().isInitializer() && !isRenamingAllowed(method)) { |
| localNamingState.put(method.getReference(), method.getReference()); |
| } |
| }); |
| clazz |
| .getMethodCollection() |
| .<ProgramMethod>replaceClassAndMethods( |
| method -> publicizeMethod(method, localNamingState, namingState, traversalState)); |
| } |
| |
| @SuppressWarnings("ReferenceEquality") |
| private DexEncodedMethod publicizeMethod( |
| ProgramMethod method, |
| BiMap<DexMethod, DexMethod> localNamingState, |
| AccessModifierNamingState namingState, |
| BottomUpTraversalState traversalState) { |
| MethodAccessFlags accessFlags = method.getAccessFlags(); |
| if (accessFlags.isPublic() || !isAccessModificationAllowed(method, traversalState)) { |
| return commitMethod(method, localNamingState, namingState); |
| } |
| |
| if (method.getDefinition().isClassInitializer()) { |
| return commitMethod(method, localNamingState, namingState); |
| } |
| |
| if (method.getDefinition().isInstanceInitializer() |
| || (accessFlags.isPackagePrivate() |
| && !traversalState.hasIllegalOverrideOfPackagePrivateMethod(method)) |
| || accessFlags.isProtected()) { |
| accessFlags.promoteToPublic(); |
| return commitMethod(method, localNamingState, namingState); |
| } |
| |
| if (accessFlags.isPrivate()) { |
| if (isRenamingAllowed(method)) { |
| accessFlags |
| .promoteToPublic() |
| .applyIf( |
| !method.getHolder().isInterface() && accessFlags.belongsToVirtualPool(), |
| MethodAccessFlags::promoteToFinal); |
| return commitMethod(method, localNamingState, namingState); |
| } |
| assert localNamingState.containsKey(method.getReference()); |
| assert localNamingState.get(method.getReference()) == method.getReference(); |
| if (namingState.isFree(method.getMethodSignature())) { |
| accessFlags |
| .promoteToPublic() |
| .applyIf( |
| !method.getHolder().isInterface() && accessFlags.belongsToVirtualPool(), |
| MethodAccessFlags::promoteToFinal); |
| namingState.addBlockedMethodSignature(method.getMethodSignature()); |
| } |
| return commitMethod(method, method.getReference()); |
| } |
| |
| // TODO(b/279126633): Add support for publicizing package-private methods by renaming. |
| assert accessFlags.isPackagePrivate(); |
| assert traversalState.hasIllegalOverrideOfPackagePrivateMethod(method); |
| return commitMethod(method, localNamingState, namingState); |
| } |
| |
| @SuppressWarnings("ReferenceEquality") |
| private DexMethod getAndReserveNewMethodReference( |
| ProgramMethod method, |
| BiMap<DexMethod, DexMethod> localNamingState, |
| AccessModifierNamingState namingState) { |
| if (method.getDefinition().isInitializer()) { |
| return method.getReference(); |
| } |
| if (!isRenamingAllowed(method)) { |
| assert localNamingState.containsKey(method.getReference()); |
| assert localNamingState.get(method.getReference()) == method.getReference(); |
| assert method.getAccessFlags().isPrivate() |
| || method |
| .getMethodSignature() |
| .equals(namingState.getReservedSignature(method.getMethodSignature())); |
| return method.getReference(); |
| } |
| DexItemFactory dexItemFactory = appView.dexItemFactory(); |
| if (method.getAccessFlags().isPrivate()) { |
| // Find a fresh method name and reserve it for the current class. |
| DexMethod newMethodReference = |
| dexItemFactory.createFreshMethodNameWithoutHolder( |
| method.getName().toString(), |
| method.getProto(), |
| method.getHolderType(), |
| candidate -> |
| !localNamingState.containsValue(candidate) |
| && namingState.isFree(candidate.getSignature())); |
| localNamingState.put(method.getReference(), newMethodReference); |
| return newMethodReference; |
| } |
| // Check if a mapping already exists for this method signature. |
| if (!method.getAccessFlags().isPromotedFromPrivateToPublic()) { |
| DexMethodSignature reservedSignature = |
| namingState.getReservedSignature(method.getMethodSignature()); |
| if (reservedSignature != null) { |
| return reservedSignature.withHolder(method, appView.dexItemFactory()); |
| } |
| } |
| // Find a fresh method name and block/reserve it globally. |
| DexMethod newMethodReference = |
| dexItemFactory.createFreshMethodNameWithoutHolder( |
| method.getName().toString(), |
| method.getProto(), |
| method.getHolderType(), |
| candidate -> |
| !localNamingState.containsValue(candidate) |
| && namingState.isFree(candidate.getSignature())); |
| if (method.getAccessFlags().belongsToVirtualPool()) { |
| if (method.getAccessFlags().isPromotedFromPrivateToPublic()) { |
| namingState.addBlockedMethodSignature(newMethodReference.getSignature()); |
| } else { |
| namingState.addRenaming(method.getMethodSignature(), newMethodReference.getSignature()); |
| } |
| } |
| return newMethodReference; |
| } |
| |
| private boolean isAccessModificationAllowed( |
| ProgramDefinition definition, BottomUpTraversalState traversalState) { |
| if (!appView.getKeepInfo(definition).isAccessModificationAllowed(options)) { |
| if (!options.getAccessModifierOptions().isForceModifyingPackagePrivateAndProtectedMethods() |
| || !definition.isMethod() |
| || definition.getAccessFlags().isPrivate()) { |
| return false; |
| } |
| } |
| if (!appView.getKeepInfo(definition).isAccessModificationAllowedForTesting(options)) { |
| return false; |
| } |
| if (isFailedResolutionTarget(definition)) { |
| return false; |
| } |
| return definition.isClass() |
| || options.getAccessModifierOptions().canPollutePublicApi() |
| || !traversalState.isKeptOrHasKeptSubclass; |
| } |
| |
| private boolean isFailedResolutionTarget(ProgramDefinition definition) { |
| if (definition.isClass()) { |
| return appView.appInfo().isFailedClassResolutionTarget(definition.asClass().getType()); |
| } |
| if (definition.isField()) { |
| return appView.appInfo().isFailedFieldResolutionTarget(definition.asField().getReference()); |
| } |
| assert definition.isMethod(); |
| return appView.appInfo().isFailedMethodResolutionTarget(definition.asMethod().getReference()); |
| } |
| |
| private boolean isRenamingAllowed(ProgramMethod method) { |
| KeepMethodInfo keepInfo = appView.getKeepInfo(method); |
| return keepInfo.isOptimizationAllowed(options) && keepInfo.isShrinkingAllowed(options); |
| } |
| |
| private DexEncodedMethod commitMethod( |
| ProgramMethod method, |
| BiMap<DexMethod, DexMethod> localNamingState, |
| AccessModifierNamingState namingState) { |
| return commitMethod( |
| method, getAndReserveNewMethodReference(method, localNamingState, namingState)); |
| } |
| |
| @SuppressWarnings("ReferenceEquality") |
| private DexEncodedMethod commitMethod(ProgramMethod method, DexMethod newMethodReference) { |
| DexProgramClass holder = method.getHolder(); |
| if (newMethodReference != method.getReference()) { |
| lensBuilder.recordMove(method.getReference(), newMethodReference); |
| method = |
| new ProgramMethod( |
| holder, |
| method |
| .getDefinition() |
| .toTypeSubstitutedMethodAsInlining(newMethodReference, appView.dexItemFactory())); |
| } |
| if (method.getAccessFlags().isPromotedFromPrivateToPublic() |
| && method.getAccessFlags().belongsToVirtualPool()) { |
| lensBuilder.addPublicizedPrivateVirtualMethod(method.getHolder(), newMethodReference); |
| method.getDefinition().setLibraryMethodOverride(OptionalBool.FALSE); |
| } |
| return method.getDefinition(); |
| } |
| } |