Reland "[ApiModel] Outline calls to holders where definition is in library"
This reverts commit ad39dcb28b565277e9600ad0909e8f86cb79e95d.
Bug: b/254510678
Change-Id: I3813f9101e540cfc475edc5513ce674260ae9b4f
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index 369e60c..1536cd8 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -191,9 +191,9 @@
* given type is *not* visited. The function indicates if traversal should continue or break. The
* result of the traversal is BREAK iff the function returned BREAK.
*/
- public TraversalContinuation<?, ?> traverseSuperTypes(
+ public <B> TraversalContinuation<B, ?> traverseSuperTypes(
final DexClass clazz,
- TriFunction<DexType, DexClass, Boolean, TraversalContinuation<?, ?>> fn) {
+ TriFunction<DexType, DexClass, Boolean, TraversalContinuation<B, ?>> fn) {
// We do an initial zero-allocation pass over the class super chain as it does not require a
// worklist/seen-set. Only if the traversal is not aborted and there actually are interfaces,
// do we continue traversal over the interface types. This is assuming that the second pass
@@ -206,7 +206,7 @@
if (currentClass.superType == null) {
break;
}
- TraversalContinuation<?, ?> stepResult =
+ TraversalContinuation<B, ?> stepResult =
fn.apply(currentClass.superType, currentClass, false);
if (stepResult.shouldBreak()) {
return stepResult;
@@ -226,7 +226,7 @@
while (currentClass != null) {
for (DexType iface : currentClass.interfaces.values) {
if (seen.add(iface)) {
- TraversalContinuation<?, ?> stepResult = fn.apply(iface, currentClass, true);
+ TraversalContinuation<B, ?> stepResult = fn.apply(iface, currentClass, true);
if (stepResult.shouldBreak()) {
return stepResult;
}
@@ -246,7 +246,7 @@
if (definition != null) {
for (DexType iface : definition.interfaces.values) {
if (seen.add(iface)) {
- TraversalContinuation<?, ?> stepResult = fn.apply(iface, definition, true);
+ TraversalContinuation<B, ?> stepResult = fn.apply(iface, definition, true);
if (stepResult.shouldBreak()) {
return stepResult;
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
index 4c3fc63..be663de 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
@@ -40,6 +40,8 @@
import com.android.tools.r8.ir.synthetic.InstanceOfSourceCode;
import com.android.tools.r8.synthesis.SyntheticMethodBuilder;
import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApiLevelUtils;
+import com.android.tools.r8.utils.Pair;
import com.android.tools.r8.utils.TraversalContinuation;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
@@ -119,23 +121,38 @@
return appView.computedMinApiLevel();
}
DexClass holder = appView.definitionFor(reference.getContextType());
- if (holder == null || !holder.isLibraryClass()) {
+ if (holder == null) {
return appView.computedMinApiLevel();
}
- ComputedApiLevel referenceApiLevel =
- apiLevelCompute.computeApiLevelForLibraryReference(reference, ComputedApiLevel.unknown());
+ Pair<DexClass, ComputedApiLevel> classAndApiLevel =
+ reference.isDexType()
+ ? Pair.create(
+ holder,
+ apiLevelCompute.computeApiLevelForLibraryReference(
+ reference, ComputedApiLevel.unknown()))
+ : AndroidApiLevelUtils.findAndComputeApiLevelForLibraryDefinition(
+ appView, appView.appInfoForDesugaring(), holder, reference.asDexMember());
+ ComputedApiLevel referenceApiLevel = classAndApiLevel.getSecond();
if (appView.computedMinApiLevel().isGreaterThanOrEqualTo(referenceApiLevel)
|| isApiLevelLessThanOrEqualTo9(referenceApiLevel)
|| referenceApiLevel.isUnknownApiLevel()) {
return appView.computedMinApiLevel();
}
+ assert referenceApiLevel.isKnownApiLevel();
+ DexClass firstLibraryClass = classAndApiLevel.getFirst();
+ if (firstLibraryClass == null || !firstLibraryClass.isLibraryClass()) {
+ assert false : "When computed a known api level we should always have a library class";
+ return appView.computedMinApiLevel();
+ }
// Check for protected or package private access flags before outlining.
- if (holder.isInterface() || instruction.isCheckCast() || instruction.isInstanceOf()) {
+ if (firstLibraryClass.isInterface()
+ || instruction.isCheckCast()
+ || instruction.isInstanceOf()) {
return referenceApiLevel;
} else {
DexEncodedMember<?, ?> definition =
simpleLookupInClassHierarchy(
- holder.asLibraryClass(),
+ firstLibraryClass.asLibraryClass(),
reference.isDexMethod()
? x -> x.lookupMethod(reference.asDexMethod())
: x -> x.lookupField(reference.asDexField()));
diff --git a/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java
index 662f9f4..d779378 100644
--- a/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/ComputeApiLevelUseRegistry.java
@@ -7,7 +7,9 @@
import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.androidapi.ComputedApiLevel;
import com.android.tools.r8.dex.code.CfOrDexInstruction;
+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.DexField;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexMethodHandle;
@@ -15,11 +17,13 @@
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.utils.AndroidApiLevelUtils;
import java.util.ListIterator;
public class ComputeApiLevelUseRegistry extends UseRegistry<ProgramMethod> {
protected final AppView<?> appView;
+ private final AppInfoWithClassHierarchy appInfoWithClassHierarchy;
private final AndroidApiLevelCompute apiLevelCompute;
private final boolean isEnabled;
private ComputedApiLevel maxApiReferenceLevel;
@@ -28,6 +32,7 @@
AppView<?> appView, ProgramMethod context, AndroidApiLevelCompute apiLevelCompute) {
super(appView, context);
this.appView = appView;
+ this.appInfoWithClassHierarchy = appView.appInfoForDesugaring();
this.apiLevelCompute = apiLevelCompute;
isEnabled = apiLevelCompute.isEnabled();
maxApiReferenceLevel = appView.computedMinApiLevel();
@@ -148,10 +153,28 @@
private void setMaxApiReferenceLevel(DexReference reference) {
if (isEnabled) {
- maxApiReferenceLevel =
- maxApiReferenceLevel.max(
- apiLevelCompute.computeApiLevelForLibraryReference(
- reference, apiLevelCompute.getPlatformApiLevelOrUnknown(appView)));
+ if (reference.isDexType()) {
+ maxApiReferenceLevel =
+ maxApiReferenceLevel.max(
+ apiLevelCompute.computeApiLevelForLibraryReference(
+ reference, apiLevelCompute.getPlatformApiLevelOrUnknown(appView)));
+ } else if (!reference.getContextType().isClassType()) {
+ maxApiReferenceLevel = maxApiReferenceLevel.max(appView.computedMinApiLevel());
+ } else {
+ DexClass holder = appView.definitionFor(reference.getContextType());
+ ComputedApiLevel referenceApiLevel = ComputedApiLevel.unknown();
+ if (holder != null) {
+ referenceApiLevel =
+ AndroidApiLevelUtils.findAndComputeApiLevelForLibraryDefinition(
+ appView, appInfoWithClassHierarchy, holder, reference.asDexMember())
+ .getSecond();
+ }
+ maxApiReferenceLevel =
+ maxApiReferenceLevel.max(
+ referenceApiLevel.isUnknownApiLevel()
+ ? apiLevelCompute.getPlatformApiLevelOrUnknown(appView)
+ : referenceApiLevel);
+ }
}
}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
index 7369593..0a8b899 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -13,6 +13,7 @@
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;
@@ -22,6 +23,9 @@
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 {
@@ -191,4 +195,109 @@
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;
+ }
+ WorkList<DexClass> workList = WorkList.newIdentityWorkList(originalClass);
+ while (workList.hasNext()) {
+ DexClass clazz = workList.next();
+ if (clazz.isLibraryClass()) {
+ return clazz;
+ } else if (clazz.lookupMember(reference) != null) {
+ return clazz;
+ } else if (clazz.getSuperType() != null) {
+ appInfo
+ .contextIndependentDefinitionForWithResolutionResult(clazz.getSuperType())
+ .forEachClassResolutionResult(workList::addIfNotSeen);
+ }
+ }
+ return null;
+ }
+
+ 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;
+ }
}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithDifferentApiLevelTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithDifferentApiLevelTest.java
index f859e30..3c2923d 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithDifferentApiLevelTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithDifferentApiLevelTest.java
@@ -20,6 +20,7 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.apimodel.ApiModelingTestHelper.ApiModelingMethodVerificationHelper;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.testing.AndroidBuildVersion;
import com.android.tools.r8.utils.AndroidApiLevel;
@@ -103,7 +104,7 @@
.apply(this::setupTestBuilder)
.compile()
.apply(this::setupRunEnvironment)
- .inspect(this::inspect)
+ .inspect(inspector -> inspect(inspector, false))
.run(parameters.getRuntime(), Main.class)
.apply(result -> checkOutput(result, false));
}
@@ -115,7 +116,7 @@
.apply(this::setupTestBuilder)
.compile()
.apply(this::setupRunEnvironment)
- .inspect(this::inspect)
+ .inspect(inspector -> inspect(inspector, false))
.run(parameters.getRuntime(), Main.class)
.apply(result -> checkOutput(result, false));
}
@@ -127,7 +128,7 @@
.addKeepMainRule(Main.class)
.addKeepClassAndMembersRules(ProgramJoiner.class)
.compile()
- .inspect(this::inspect)
+ .inspect(inspector -> inspect(inspector, true))
.apply(this::setupRunEnvironment)
.run(parameters.getRuntime(), Main.class)
.apply(result -> checkOutput(result, true));
@@ -155,17 +156,25 @@
result -> result.assertStderrMatches(not(containsString("This dex file is invalid"))));
}
- private void inspect(CodeInspector inspector) throws Exception {
- // TODO(b/254510678): We should outline the call to ProgramJoiner.foo.
- verifyThat(
+ private void inspect(CodeInspector inspector, boolean isR8) throws Exception {
+ ApiModelingMethodVerificationHelper verifyHelper =
+ verifyThat(
inspector,
parameters,
Reference.method(
- Reference.classFromClass(ProgramJoiner.class),
+ // TODO(b/254510678): Due to member rebinding, we rebind ProgramJoiner.foo() to
+ // LibraryClass.foo().
+ Reference.classFromClass(isR8 ? LibraryClass.class : ProgramJoiner.class),
"foo",
Collections.emptyList(),
- null))
- .isNotOutlinedFrom(Main.class.getDeclaredMethod("main", String[].class));
+ null));
+ if (isR8 && parameters.isCfRuntime()) {
+ verifyHelper.isOutlinedFromUntil(
+ Main.class.getDeclaredMethod("main", String[].class), classMethodApiLevel);
+ } else {
+ verifyHelper.isOutlinedFromUntilAlsoForCf(
+ Main.class.getDeclaredMethod("main", String[].class), classMethodApiLevel);
+ }
}
// Only present from api level 23.
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithSameApiLevelTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithSameApiLevelTest.java
index a0cc571..f6a0a49 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithSameApiLevelTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithSameApiLevelTest.java
@@ -20,6 +20,7 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.apimodel.ApiModelingTestHelper.ApiModelingMethodVerificationHelper;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.testing.AndroidBuildVersion;
import com.android.tools.r8.utils.AndroidApiLevel;
@@ -93,7 +94,7 @@
.apply(this::setupRunEnvironment)
.run(parameters.getRuntime(), Main.class)
.apply(result -> checkOutput(result, false))
- .inspect(this::inspect);
+ .inspect(inspector -> inspect(inspector, false));
}
@Test
@@ -105,7 +106,7 @@
.apply(this::setupRunEnvironment)
.run(parameters.getRuntime(), Main.class)
.apply(result -> checkOutput(result, false))
- .inspect(this::inspect);
+ .inspect(inspector -> inspect(inspector, false));
}
@Test
@@ -115,7 +116,7 @@
.addKeepMainRule(Main.class)
.addKeepClassAndMembersRules(ProgramJoiner.class)
.compile()
- .inspect(this::inspect)
+ .inspect(inspector -> inspect(inspector, true))
.apply(this::setupRunEnvironment)
.run(parameters.getRuntime(), Main.class)
.apply(result -> checkOutput(result, true));
@@ -137,17 +138,25 @@
result -> result.assertStderrMatches(not(containsString("This dex file is invalid"))));
}
- private void inspect(CodeInspector inspector) throws Exception {
- // TODO(b/254510678): We should outline the call to ProgramClass.foo.
- verifyThat(
+ private void inspect(CodeInspector inspector, boolean isR8) throws Exception {
+ ApiModelingMethodVerificationHelper verifyHelper =
+ verifyThat(
inspector,
parameters,
Reference.method(
- Reference.classFromClass(ProgramJoiner.class),
+ // TODO(b/254510678): Due to member rebinding, we rebind ProgramJoiner.foo() to
+ // LibraryClass.foo().
+ Reference.classFromClass(isR8 ? LibraryClass.class : ProgramJoiner.class),
"foo",
Collections.emptyList(),
- null))
- .isNotOutlinedFrom(Main.class.getDeclaredMethod("main", String[].class));
+ null));
+ if (isR8 && parameters.isCfRuntime()) {
+ verifyHelper.isOutlinedFromUntil(
+ Main.class.getDeclaredMethod("main", String[].class), mockApiLevel);
+ } else {
+ verifyHelper.isOutlinedFromUntilAlsoForCf(
+ Main.class.getDeclaredMethod("main", String[].class), mockApiLevel);
+ }
}
// Only present from api level 23.
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineSubTypeStaticReferenceTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineSubTypeStaticReferenceTest.java
index 3e56179..19971c3 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineSubTypeStaticReferenceTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineSubTypeStaticReferenceTest.java
@@ -7,9 +7,6 @@
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
-import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assume.assumeTrue;
import com.android.tools.r8.CompilationMode;
@@ -23,8 +20,8 @@
import com.android.tools.r8.testing.AndroidBuildVersion;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
import java.lang.reflect.Method;
+import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -133,13 +130,14 @@
Method otherMethod = Sub.class.getMethod("otherMethod");
Method libraryMethod = LibraryClass.class.getMethod("foo");
// TODO(b/254510678): R8 should not member-rebind to a potential non-existing method.
- if (isR8) {
- MethodSubject method = inspector.method(otherMethod);
- assertThat(method, isPresent());
- assertThat(method, invokesMethod(Reference.methodFromMethod(libraryMethod)));
- }
- // TODO(b/254510678): We should outline this up until library api level.
- verifyThat(inspector, parameters, libraryMethod).isNotOutlinedFrom(otherMethod);
+ verifyThat(
+ inspector,
+ parameters,
+ isR8
+ ? Reference.methodFromMethod(libraryMethod)
+ : Reference.method(
+ Reference.classFromClass(Sub.class), "foo", Collections.emptyList(), null))
+ .isOutlinedFromUntil(Sub.class.getDeclaredMethod("otherMethod"), libraryApiLevel);
}
private void checkResultOnBootClassPath(SingleTestRunResult<?> runResult) {
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
index e437943..1e45b00 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
@@ -447,6 +447,14 @@
}
}
+ void isOutlinedFromUntilAlsoForCf(Executable method, AndroidApiLevel apiLevel) {
+ if (parameters.getApiLevel().isLessThan(apiLevel)) {
+ isOutlinedFrom(method);
+ } else {
+ isNotOutlinedFrom(method);
+ }
+ }
+
void isOutlinedFrom(Executable method) {
// Check that the call is in a synthetic class.
List<FoundMethodSubject> outlinedMethod =