blob: 0e533137a11a0cd2954e1fbda2511c1be538b535 [file] [log] [blame]
// Copyright (c) 2021, 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.utils;
import static com.android.tools.r8.graph.DexLibraryClass.asLibraryClassOrNull;
import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.androidapi.ComputedApiLevel;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMember;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.LibraryClass;
import com.android.tools.r8.graph.LibraryDefinition;
import com.android.tools.r8.graph.LibraryMethod;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.Set;
public class AndroidApiLevelUtils {
public static boolean isApiSafeForInlining(
ProgramMethod caller, ProgramMethod inlinee, InternalOptions options) {
return isApiSafeForInlining(
caller, inlinee, options, NopWhyAreYouNotInliningReporter.getInstance());
}
@SuppressWarnings("ReferenceEquality")
public static boolean isApiSafeForInlining(
ProgramMethod caller,
ProgramMethod inlinee,
InternalOptions options,
WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
if (!options.apiModelingOptions().isApiCallerIdentificationEnabled()) {
return true;
}
if (caller.getHolderType() == inlinee.getHolderType()) {
return true;
}
ComputedApiLevel callerApiLevelForCode = caller.getDefinition().getApiLevelForCode();
if (callerApiLevelForCode.isUnknownApiLevel()) {
whyAreYouNotInliningReporter.reportCallerHasUnknownApiLevel();
return false;
}
// For inlining we only measure if the code has invokes into the library.
ComputedApiLevel inlineeApiLevelForCode = inlinee.getDefinition().getApiLevelForCode();
if (!caller
.getDefinition()
.getApiLevelForCode()
.isGreaterThanOrEqualTo(inlineeApiLevelForCode)) {
whyAreYouNotInliningReporter.reportInlineeHigherApiCall(
callerApiLevelForCode, inlineeApiLevelForCode);
return false;
}
return true;
}
public static ComputedApiLevel getApiReferenceLevelForMerging(
AndroidApiLevelCompute apiLevelCompute, DexProgramClass clazz) {
// The api level of a class is the max level of it's members, super class and interfaces.
return getMembersApiReferenceLevelForMerging(
clazz, apiLevelCompute.computeApiLevelForDefinition(clazz.allImmediateSupertypes()));
}
public static ComputedApiLevel getMembersApiReferenceLevelForMerging(
DexProgramClass clazz, ComputedApiLevel memberLevel) {
// Based on b/138781768#comment57 there is almost no penalty for having an unknown reference
// as long as we are not invoking or accessing a field on it. Therefore we can disregard static
// types of fields and only consider method code api levels.
for (DexEncodedMethod method : clazz.methods()) {
if (method.hasCode()) {
ComputedApiLevel apiLevelForCode = method.getApiLevelForCode();
if (apiLevelForCode.isNotSetApiLevel()) {
return ComputedApiLevel.notSet();
}
memberLevel = memberLevel.max(apiLevelForCode);
}
if (memberLevel.isUnknownApiLevel()) {
return memberLevel;
}
}
return memberLevel;
}
public static boolean isApiSafeForMemberRebinding(
LibraryMethod method,
DexMethod original,
AndroidApiLevelCompute androidApiLevelCompute,
InternalOptions options) {
if (!androidApiLevelCompute.isEnabled()) {
assert !options.apiModelingOptions().enableLibraryApiModeling;
return false;
}
assert options.apiModelingOptions().enableLibraryApiModeling;
ComputedApiLevel apiLevel =
androidApiLevelCompute.computeApiLevelForLibraryReference(
method.getReference(), ComputedApiLevel.unknown());
if (apiLevel.isUnknownApiLevel()) {
return false;
}
ComputedApiLevel apiLevelOfOriginal =
androidApiLevelCompute.computeApiLevelForLibraryReference(
original, ComputedApiLevel.unknown());
if (apiLevelOfOriginal.isUnknownApiLevel()) {
return false;
}
return apiLevelOfOriginal.max(apiLevel).isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
}
public static boolean isApiSafeForReference(DexType type, AppView<?> appView) {
DexItemFactory dexItemFactory = appView.dexItemFactory();
DexType baseType = type.toBaseType(dexItemFactory);
if (baseType.isPrimitiveType()) {
return true;
}
DexClass baseClass = appView.definitionFor(baseType);
if (baseClass == null) {
// This could be a library class that is only available on newer api levels.
return false;
}
if (!baseClass.isLibraryClass()) {
// Program and classpath classes are not api level dependent.
return true;
}
LibraryClass baseLibraryClass = baseClass.asLibraryClass();
return isApiSafeForReference(baseLibraryClass, appView);
}
public static boolean isApiSafeForReference(LibraryDefinition definition, AppView<?> appView) {
return isApiSafeForReference(
definition, appView.apiLevelCompute(), appView.options(), appView.dexItemFactory());
}
@SuppressWarnings("ReferenceEquality")
private static boolean isApiSafeForReference(
LibraryDefinition definition,
AndroidApiLevelCompute androidApiLevelCompute,
InternalOptions options,
DexItemFactory factory) {
if (!options.apiModelingOptions().isApiLibraryModelingEnabled()) {
return factory.libraryTypesAssumedToBePresent.contains(definition.getContextType());
}
ComputedApiLevel apiLevel =
androidApiLevelCompute.computeApiLevelForLibraryReference(
definition.getReference(), ComputedApiLevel.unknown());
return apiLevel.isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
}
private static boolean isApiSafeForReference(
LibraryDefinition newDefinition, LibraryDefinition oldDefinition, AppView<?> appView) {
assert appView.options().apiModelingOptions().isApiLibraryModelingEnabled();
assert !isApiSafeForReference(newDefinition, appView)
: "Clients should first check if the definition is present on all apis since the min api";
AndroidApiLevelCompute androidApiLevelCompute = appView.apiLevelCompute();
ComputedApiLevel apiLevel =
androidApiLevelCompute.computeApiLevelForLibraryReference(
newDefinition.getReference(), ComputedApiLevel.unknown());
if (apiLevel.isUnknownApiLevel()) {
return false;
}
ComputedApiLevel apiLevelOfOriginal =
androidApiLevelCompute.computeApiLevelForLibraryReference(
oldDefinition.getReference(), ComputedApiLevel.unknown());
return apiLevel.isLessThanOrEqualTo(apiLevelOfOriginal).isTrue();
}
@SuppressWarnings({"MixedMutabilityReturnType", "ReferenceEquality"})
public static boolean isApiSafeForTypeStrengthening(
DexType newType, DexType oldType, AppView<? extends AppInfoWithClassHierarchy> appView) {
// Type strengthening only applies to reference types.
assert newType.isReferenceType();
assert oldType.isReferenceType();
assert newType != oldType;
DexItemFactory dexItemFactory = appView.dexItemFactory();
DexType newBaseType = newType.toBaseType(dexItemFactory);
if (newBaseType.isPrimitiveType()) {
// Array of primitives is available on all api levels.
return true;
}
assert newBaseType.isClassType();
DexClass newBaseClass = appView.definitionFor(newBaseType);
if (newBaseClass == null) {
// This could be a library class that is only available on newer api levels.
return false;
}
if (!newBaseClass.isLibraryClass()) {
// Program and classpath classes are not api level dependent.
return true;
}
if (!appView.options().apiModelingOptions().isApiLibraryModelingEnabled()) {
// Conservatively bail out if we don't have api modeling.
return false;
}
LibraryClass newBaseLibraryClass = newBaseClass.asLibraryClass();
if (isApiSafeForReference(newBaseLibraryClass, appView)) {
// Library class is present on all api levels since min api.
return true;
}
// Check if the new library class is present since the api level of the old type.
DexType oldBaseType = oldType.toBaseType(dexItemFactory);
assert oldBaseType.isClassType();
LibraryClass oldBaseLibraryClass = asLibraryClassOrNull(appView.definitionFor(oldBaseType));
return oldBaseLibraryClass != null
&& isApiSafeForReference(newBaseLibraryClass, oldBaseLibraryClass, appView);
}
public static Pair<DexClass, ComputedApiLevel> findAndComputeApiLevelForLibraryDefinition(
AppView<?> appView,
AppInfoWithClassHierarchy appInfo,
DexClass holder,
DexMember<?, ?> reference) {
AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
if (holder.isLibraryClass()) {
return Pair.create(
holder,
apiLevelCompute.computeApiLevelForLibraryReference(
reference, ComputedApiLevel.unknown()));
}
// The API database do not allow for resolving into it (since that is not stable), and it is
// therefore designed in a way where all members of classes can be queried on any sub-type with
// the api level for where it is reachable. It is therefore sufficient for us, to figure out if
// an instruction is a library call, to either find a program definition or to find the library
// frontier.
// Scan through the type hierarchy to find the first library class or program definition.
DexClass firstClassWithReferenceOrLibraryClass =
firstLibraryClassOrProgramClassWithDefinition(appInfo, holder, reference);
if (firstClassWithReferenceOrLibraryClass == null) {
return Pair.create(null, ComputedApiLevel.unknown());
}
if (!firstClassWithReferenceOrLibraryClass.isLibraryClass()) {
return Pair.create(firstClassWithReferenceOrLibraryClass, appView.computedMinApiLevel());
}
ComputedApiLevel apiLevel =
apiLevelCompute.computeApiLevelForLibraryReference(
reference.withHolder(
firstClassWithReferenceOrLibraryClass.getType(), appView.dexItemFactory()),
ComputedApiLevel.unknown());
if (apiLevel.isKnownApiLevel()) {
return Pair.create(firstClassWithReferenceOrLibraryClass, apiLevel);
}
// We were unable to find a definition in the class hierarchy, check all interfaces for a
// definition or the library interfaces for the first interface definition.
Set<DexClass> firstLibraryInterfaces =
findAllFirstLibraryInterfacesOrProgramClassWithDefinition(appInfo, holder, reference);
if (firstLibraryInterfaces.size() == 1) {
DexClass firstClass = firstLibraryInterfaces.iterator().next();
if (!firstClass.isLibraryClass()) {
return Pair.create(firstClass, appView.computedMinApiLevel());
}
}
DexClass foundClass = null;
ComputedApiLevel minApiLevel = ComputedApiLevel.unknown();
for (DexClass libraryInterface : firstLibraryInterfaces) {
assert libraryInterface.isLibraryClass();
ComputedApiLevel libraryIfaceApiLevel =
apiLevelCompute.computeApiLevelForLibraryReference(
reference.withHolder(
firstClassWithReferenceOrLibraryClass.getType(), appView.dexItemFactory()),
ComputedApiLevel.unknown());
if (minApiLevel.isGreaterThan(libraryIfaceApiLevel)) {
minApiLevel = libraryIfaceApiLevel;
foundClass = libraryInterface;
}
}
return Pair.create(foundClass, minApiLevel);
}
private static DexClass firstLibraryClassOrProgramClassWithDefinition(
AppInfoWithClassHierarchy appInfo, DexClass originalClass, DexMember<?, ?> reference) {
if (originalClass.isLibraryClass()) {
return originalClass;
}
return WorkList.newIdentityWorkList(originalClass)
.run(
(clazz, workList) -> {
if (clazz.isLibraryClass()) {
return TraversalContinuation.doBreak(clazz);
} else if (clazz.lookupMember(reference) != null) {
return TraversalContinuation.doBreak(clazz);
} else if (clazz.getSuperType() != null) {
appInfo
.contextIndependentDefinitionForWithResolutionResult(clazz.getSuperType())
.forEachClassResolutionResult(workList::addIfNotSeen);
}
return TraversalContinuation.doContinue();
})
.asBreakOrDefault(null)
.getValue();
}
@SuppressWarnings("MixedMutabilityReturnType")
private static Set<DexClass> findAllFirstLibraryInterfacesOrProgramClassWithDefinition(
AppInfoWithClassHierarchy appInfo, DexClass originalClass, DexMember<?, ?> reference) {
Set<DexClass> interfaces = Sets.newLinkedHashSet();
WorkList<DexClass> workList = WorkList.newIdentityWorkList(originalClass);
while (workList.hasNext()) {
DexClass clazz = workList.next();
if (clazz.isLibraryClass()) {
if (clazz.isInterface()) {
interfaces.add(clazz);
}
} else if (clazz.lookupMember(reference) != null) {
return Collections.singleton(clazz);
} else {
clazz.forEachImmediateSupertype(
superType ->
appInfo
.contextIndependentDefinitionForWithResolutionResult(superType)
.forEachClassResolutionResult(workList::addIfNotSeen));
}
}
return interfaces;
}
/**
* A lot of functionality has already been outlined in androidx. The ordinary pattern for manual
* outlining is to create a class with the name ApiXXImpl where XX is the api level. This method
* will check the context to see if it matches this pattern in androidx and extract the api level
* for comparison with the computed api level.
*/
public static boolean isOutlinedAtSameOrLowerLevel(
DexProgramClass clazz, ComputedApiLevel apiLevel) {
assert apiLevel.isKnownApiLevel();
if (!clazz.getType().getDescriptor().startsWith("Landroidx/")) {
return false;
}
String simpleName = clazz.getSimpleName();
int apiIndex = simpleName.indexOf("Api");
if (apiIndex < 0) {
return false;
}
int endApiIndex = apiIndex += 3;
int implIndex = simpleName.indexOf("Impl");
if (implIndex < 0 || implIndex < endApiIndex || (implIndex - endApiIndex) != 2) {
return false;
}
String apiLevelAsString = simpleName.substring(endApiIndex, implIndex);
if (!StringUtils.onlyContainsDigits(apiLevelAsString)) {
return false;
}
int apiLevelAsInt = Integer.parseInt(apiLevelAsString);
if (apiLevelAsInt < 10 || apiLevelAsInt > AndroidApiLevel.LATEST.getLevel()) {
return false;
}
return apiLevel.asKnownApiLevel().getApiLevel().getLevel() <= apiLevelAsInt;
}
public static boolean isApiLevelLessThanOrEqualToG(ComputedApiLevel apiLevel) {
return apiLevel.isKnownApiLevel()
&& apiLevel.asKnownApiLevel().getApiLevel().isLessThanOrEqualTo(AndroidApiLevel.G);
}
}