Towards tree shaking of library method overrides
Bug: 70160030
Change-Id: I80e45ecc2d7e489dc790b98d47ee27e0385007fd
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index a3c686e..800feb6 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -9,8 +9,10 @@
import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
import com.android.tools.r8.shaking.VerticalClassMerger.VerticallyMergedClasses;
import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
+import java.util.function.Predicate;
public class AppView<T extends AppInfo> implements DexDefinitionSupplier {
@@ -26,6 +28,8 @@
private GraphLense graphLense;
private final InternalOptions options;
private RootSet rootSet;
+
+ private Predicate<DexType> classesEscapingIntoLibrary = Predicates.alwaysTrue();
private Set<DexMethod> unneededVisibilityBridgeMethods = ImmutableSet.of();
private VerticallyMergedClasses verticallyMergedClasses;
@@ -70,6 +74,15 @@
this.appServices = appServices;
}
+ public boolean isClassEscapingIntoLibrary(DexType type) {
+ assert type.isClassType();
+ return classesEscapingIntoLibrary.test(type);
+ }
+
+ public void setClassesEscapingIntoLibrary(Predicate<DexType> classesEscapingIntoLibrary) {
+ this.classesEscapingIntoLibrary = classesEscapingIntoLibrary;
+ }
+
@Override
public final DexDefinition definitionFor(DexReference reference) {
return appInfo().definitionFor(reference);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/DefaultEscapeAnalysisConfiguration.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/DefaultEscapeAnalysisConfiguration.java
index 162120b..6ec09bb 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/escape/DefaultEscapeAnalysisConfiguration.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/DefaultEscapeAnalysisConfiguration.java
@@ -21,7 +21,10 @@
@Override
public boolean isLegitimateEscapeRoute(
- AppView<?> appView, Instruction escapeRoute, DexMethod context) {
+ AppView<?> appView,
+ EscapeAnalysis escapeAnalysis,
+ Instruction escapeRoute,
+ DexMethod context) {
return false;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
index 84aeadc..b77c1f9 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
@@ -137,7 +137,7 @@
}
}
}
- if (!configuration.isLegitimateEscapeRoute(appView, user, code.method.method)
+ if (!configuration.isLegitimateEscapeRoute(appView, this, user, code.method.method)
&& isDirectlyEscaping(user, code.method.method, arguments)) {
if (stoppingCriterion.test(user)) {
return true;
@@ -194,7 +194,7 @@
return false;
}
- protected boolean isValueOfInterestOrAlias(Value value) {
+ public boolean isValueOfInterestOrAlias(Value value) {
return trackedValues.contains(value);
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisConfiguration.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisConfiguration.java
index 6443dd9..ea3a7e3 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisConfiguration.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysisConfiguration.java
@@ -10,5 +10,9 @@
public interface EscapeAnalysisConfiguration {
- boolean isLegitimateEscapeRoute(AppView<?> appView, Instruction escapeRoute, DexMethod context);
+ boolean isLegitimateEscapeRoute(
+ AppView<?> appView,
+ EscapeAnalysis escapeAnalysis,
+ Instruction escapeRoute,
+ DexMethod context);
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 2559faa..bbea0ac 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -547,6 +547,10 @@
assert graphLenseForIR == appView.graphLense();
}
+ if (libraryMethodOverrideAnalysis != null) {
+ libraryMethodOverrideAnalysis.finish();
+ }
+
// Second inlining pass for dealing with double inline callers.
if (inliner != null) {
printPhase("Double caller inlining");
@@ -612,8 +616,13 @@
identifierNameStringMarker.decoupleIdentifierNameStringsInFields();
}
- if (Log.ENABLED && uninstantiatedTypeOptimization != null) {
- uninstantiatedTypeOptimization.logResults();
+ if (Log.ENABLED) {
+ if (libraryMethodOverrideAnalysis != null) {
+ libraryMethodOverrideAnalysis.logResults();
+ }
+ if (uninstantiatedTypeOptimization != null) {
+ uninstantiatedTypeOptimization.logResults();
+ }
}
// Check if what we've added to the application builder as synthesized classes are same as
@@ -1133,23 +1142,28 @@
classStaticizer.examineMethodCode(method, code);
}
- // Compute optimization info summary for the current method unless it is pinned (in that case,
- // we should not be making any assumptions about the behavior of the method).
- if (appView.enableWholeProgramOptimizations()
- && !appView.appInfo().withLiveness().isPinned(method.method)) {
- codeRewriter.identifyClassInlinerEligibility(method, code, feedback);
- codeRewriter.identifyParameterUsages(method, code, feedback);
- codeRewriter.identifyReturnsArgument(method, code, feedback);
- codeRewriter.identifyTrivialInitializer(method, code, feedback);
-
- if (options.enableInlining && inliner != null) {
- codeRewriter.identifyInvokeSemanticsForInlining(method, code, appView, feedback);
+ if (appView.enableWholeProgramOptimizations()) {
+ if (libraryMethodOverrideAnalysis != null) {
+ libraryMethodOverrideAnalysis.analyze(code);
}
- computeDynamicReturnType(feedback, method, code);
- computeInitializedClassesOnNormalExit(feedback, method, code);
- computeMayHaveSideEffects(feedback, method, code);
- computeNonNullParamOrThrow(feedback, method, code);
+ // Compute optimization info summary for the current method unless it is pinned
+ // (in that case we should not be making any assumptions about the behavior of the method).
+ if (!appView.appInfo().withLiveness().isPinned(method.method)) {
+ codeRewriter.identifyClassInlinerEligibility(method, code, feedback);
+ codeRewriter.identifyParameterUsages(method, code, feedback);
+ codeRewriter.identifyReturnsArgument(method, code, feedback);
+ codeRewriter.identifyTrivialInitializer(method, code, feedback);
+
+ if (options.enableInlining && inliner != null) {
+ codeRewriter.identifyInvokeSemanticsForInlining(method, code, appView, feedback);
+ }
+
+ computeDynamicReturnType(feedback, method, code);
+ computeInitializedClassesOnNormalExit(feedback, method, code);
+ computeMayHaveSideEffects(feedback, method, code);
+ computeNonNullParamOrThrow(feedback, method, code);
+ }
}
previous =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 069c729..6362631 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -424,7 +424,10 @@
@Override
public boolean isLegitimateEscapeRoute(
- AppView<?> appView, Instruction escapeRoute, DexMethod context) {
+ AppView<?> appView,
+ EscapeAnalysis escapeAnalysis,
+ Instruction escapeRoute,
+ DexMethod context) {
if (escapeRoute.isReturn() || escapeRoute.isThrow() || escapeRoute.isStaticPut()) {
return false;
}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index e2e709c..492e4cd 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -91,6 +91,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -1333,13 +1334,14 @@
private void markVirtualMethodAsReachable(
DexMethod method, boolean interfaceInvoke, KeepReason reason) {
- markVirtualMethodAsReachable(method, interfaceInvoke, reason, null);
+ markVirtualMethodAsReachable(method, interfaceInvoke, reason, (x, y) -> true, null);
}
private void markVirtualMethodAsReachable(
DexMethod method,
boolean interfaceInvoke,
KeepReason reason,
+ BiPredicate<DexProgramClass, DexEncodedMethod> possibleTargetsFilter,
Consumer<DexEncodedMethod> possibleTargetsConsumer) {
if (!virtualTargetsMarkedAsReachable.add(method)) {
return;
@@ -1389,6 +1391,10 @@
continue;
}
+ if (!possibleTargetsFilter.test(clazz.asProgramClass(), encodedPossibleTarget)) {
+ continue;
+ }
+
// TODO(b/120959039): The reachable.add test might be hiding other paths to the method.
SetWithReason<DexEncodedMethod> reachable =
reachableVirtualMethods.computeIfAbsent(
@@ -1804,10 +1810,52 @@
encodedMethod.method,
clazz.isInterface(),
KeepReason.isLibraryMethod(),
+ this::shouldMarkLibraryMethodOverrideAsReachable,
DexEncodedMethod::setLibraryMethodOverride);
}
}
+ 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 processNewlyLiveMethod(DexEncodedMethod method, KeepReason reason) {
if (liveMethods.add(method, reason)) {
collectProguardCompatibilityRule(reason);
diff --git a/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java b/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
index 7a55b0c..0416259 100644
--- a/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
+++ b/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
@@ -4,45 +4,83 @@
package com.android.tools.r8.shaking;
-import com.android.tools.r8.graph.AppInfoWithSubtyping;
import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.BottomUpClassHierarchyTraversal;
+import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexType;
-import java.util.IdentityHashMap;
-import java.util.Map;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.ir.analysis.escape.EscapeAnalysis;
+import com.android.tools.r8.ir.analysis.escape.EscapeAnalysisConfiguration;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.logging.Log;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import java.util.Collections;
+import java.util.Set;
public class LibraryMethodOverrideAnalysis {
- private final AppView<? extends AppInfoWithSubtyping> appView;
+ private final AppView<AppInfoWithLiveness> appView;
- // A map that for each type specifies if the type may have a method that overrides a library
- // method (conservatively).
- //
- // Note that we intentionally use a Map instead of a Set, such that we can distinguish the "may
- // definitely not have a library method override" (which arises when a type is mapped to FALSE)
- // from the "unknown" case (which arises when there is a type that is not a key in the map).
- private final Map<DexType, Boolean> mayHaveLibraryMethodOverride = new IdentityHashMap<>();
+ // Note: Set is accessed concurrently and must be thread-safe.
+ private final Set<DexType> nonEscapingClassesWithLibraryMethodOverrides;
- public LibraryMethodOverrideAnalysis(AppView<? extends AppInfoWithSubtyping> appView) {
+ // Maps each instruction type to the number of times that kind of instruction has caused a type to
+ // escape.
+ private final Object2IntMap<Class<?>> escapeDebuggingCounters =
+ Log.ENABLED ? new Object2IntLinkedOpenHashMap<>() : null;
+
+ public LibraryMethodOverrideAnalysis(AppView<AppInfoWithLiveness> appView) {
this.appView = appView;
- initializeMayHaveLibraryMethodOverride();
+ this.nonEscapingClassesWithLibraryMethodOverrides =
+ Collections.synchronizedSet(
+ getInitialNonEscapingClassesWithLibraryMethodOverrides(appView));
}
- private void initializeMayHaveLibraryMethodOverride() {
- BottomUpClassHierarchyTraversal.forProgramClasses(appView)
+ private static Set<DexType> getInitialNonEscapingClassesWithLibraryMethodOverrides(
+ AppView<AppInfoWithLiveness> appView) {
+ Set<DexType> initialNonEscapingClassesWithLibraryMethodOverrides =
+ getClassesWithLibraryMethodOverrides(appView);
+
+ // Remove all types that are pinned from the initial set of non-escaping classes.
+ DexReference.filterDexType(appView.appInfo().pinnedItems.stream())
+ .forEach(initialNonEscapingClassesWithLibraryMethodOverrides::remove);
+
+ return initialNonEscapingClassesWithLibraryMethodOverrides;
+ }
+
+ private static Set<DexType> getClassesWithLibraryMethodOverrides(
+ AppView<AppInfoWithLiveness> appView) {
+ Set<DexType> classesWithLibraryMethodOverrides = Sets.newIdentityHashSet();
+ TopDownClassHierarchyTraversal.forProgramClasses(appView)
.visit(
appView.appInfo().classes(),
- clazz ->
- mayHaveLibraryMethodOverride.put(
- clazz.type,
- mayHaveLibraryMethodOverrideDirectly(clazz)
- || mayHaveLibraryMethodOverrideIndirectly(clazz)));
+ clazz -> {
+ if (hasLibraryMethodOverrideDirectlyOrIndirectly(
+ clazz, classesWithLibraryMethodOverrides)) {
+ classesWithLibraryMethodOverrides.add(clazz.type);
+ }
+ });
+ return classesWithLibraryMethodOverrides;
}
- private boolean mayHaveLibraryMethodOverrideDirectly(DexProgramClass clazz) {
+ private static boolean hasLibraryMethodOverrideDirectlyOrIndirectly(
+ DexProgramClass clazz, Set<DexType> classesWithLibraryMethodOverrides) {
+ return hasLibraryMethodOverrideDirectly(clazz)
+ || hasLibraryMethodOverrideIndirectly(clazz, classesWithLibraryMethodOverrides);
+ }
+
+ private static boolean hasLibraryMethodOverrideDirectly(DexProgramClass clazz) {
for (DexEncodedMethod method : clazz.virtualMethods()) {
+ if (method.accessFlags.isAbstract()) {
+ continue;
+ }
if (method.isLibraryMethodOverride().isPossiblyTrue()) {
return true;
}
@@ -50,14 +88,141 @@
return false;
}
- private boolean mayHaveLibraryMethodOverrideIndirectly(DexProgramClass clazz) {
- for (DexType subtype : appView.appInfo().allImmediateSubtypes(clazz.type)) {
- // If we find a subtype that is not in the mapping, we conservatively record that the current
- // class could have a method that overrides a library method.
- if (mayHaveLibraryMethodOverride.getOrDefault(subtype, Boolean.TRUE)) {
+ private static boolean hasLibraryMethodOverrideIndirectly(
+ DexProgramClass clazz, Set<DexType> classesWithLibraryMethodOverrides) {
+ if (classesWithLibraryMethodOverrides.contains(clazz.superType)) {
+ return true;
+ }
+ for (DexType interfaceType : clazz.interfaces.values) {
+ if (classesWithLibraryMethodOverrides.contains(interfaceType)) {
return true;
}
}
return false;
}
+
+ public void analyze(IRCode code) {
+ if (nonEscapingClassesWithLibraryMethodOverrides.isEmpty()) {
+ // No need to run escape analysis since all types have already escaped.
+ return;
+ }
+
+ // Must be thread local.
+ EscapeAnalysis escapeAnalysis =
+ new EscapeAnalysis(appView, LibraryEscapeAnalysisConfiguration.getInstance());
+
+ for (Instruction instruction : code.instructions()) {
+ if (instruction.isNewInstance()) {
+ DexType type = instruction.asNewInstance().clazz;
+ DexClass clazz = appView.definitionFor(type);
+ if (clazz == null || !clazz.isProgramClass()) {
+ continue;
+ }
+
+ if (nonEscapingClassesWithLibraryMethodOverrides.contains(type)) {
+ // We need to remove this instance from the set of non-escaping classes if it escapes.
+ if (escapeAnalysis.isEscaping(code, instruction.outValue())) {
+ nonEscapingClassesWithLibraryMethodOverrides.remove(type);
+
+ if (Log.ENABLED) {
+ Set<Instruction> escapeRoutes =
+ escapeAnalysis.computeEscapeRoutes(code, instruction.outValue());
+ for (Instruction escapeRoute : escapeRoutes) {
+ Class<?> instructionClass = escapeRoute.getClass();
+ escapeDebuggingCounters.put(
+ instructionClass, escapeDebuggingCounters.getInt(instructionClass) + 1);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void finish() {
+ assert verifyNoUninstantiatedTypesEscapeIntoLibrary();
+ appView.setClassesEscapingIntoLibrary(
+ type -> !nonEscapingClassesWithLibraryMethodOverrides.contains(type));
+ }
+
+ private boolean verifyNoUninstantiatedTypesEscapeIntoLibrary() {
+ Set<DexType> classesWithLibraryMethodOverrides = getClassesWithLibraryMethodOverrides(appView);
+ for (DexProgramClass clazz : appView.appInfo().classes()) {
+ assert appView.appInfo().isInstantiatedDirectlyOrIndirectly(clazz.type)
+ || !classesWithLibraryMethodOverrides.contains(clazz.type)
+ || nonEscapingClassesWithLibraryMethodOverrides.contains(clazz.type);
+ }
+ return true;
+ }
+
+ public void logResults() {
+ assert Log.ENABLED;
+ Log.info(
+ getClass(),
+ "# classes with library method overrides: %s",
+ getClassesWithLibraryMethodOverrides(appView).size());
+ Log.info(
+ getClass(),
+ "# non-escaping classes with library method overrides: %s",
+ nonEscapingClassesWithLibraryMethodOverrides.size());
+ escapeDebuggingCounters
+ .keySet()
+ .forEach(
+ (instructionClass) ->
+ Log.info(
+ getClass(),
+ "# classes that escaped via %s: %s",
+ instructionClass.getSimpleName(),
+ escapeDebuggingCounters.getInt(instructionClass)));
+ }
+
+ static class LibraryEscapeAnalysisConfiguration implements EscapeAnalysisConfiguration {
+
+ private static final LibraryEscapeAnalysisConfiguration INSTANCE =
+ new LibraryEscapeAnalysisConfiguration();
+
+ private LibraryEscapeAnalysisConfiguration() {}
+
+ public static LibraryEscapeAnalysisConfiguration getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean isLegitimateEscapeRoute(
+ AppView<?> appView,
+ EscapeAnalysis escapeAnalysis,
+ Instruction escapeRoute,
+ DexMethod context) {
+ if (appView.appInfo().hasLiveness()) {
+ return isTrivialInitializerInvocation(
+ appView.withLiveness(), escapeAnalysis, escapeRoute, context);
+ }
+ return false;
+ }
+
+ private boolean isTrivialInitializerInvocation(
+ AppView<AppInfoWithLiveness> appView,
+ EscapeAnalysis escapeAnalysis,
+ Instruction instruction,
+ DexMethod context) {
+ // Allow trivial constructor calls.
+ if (instruction.isInvokeDirect()) {
+ InvokeDirect invoke = instruction.asInvokeDirect();
+ for (int i = 1; i < invoke.arguments().size(); i++) {
+ if (escapeAnalysis.isValueOfInterestOrAlias(invoke.arguments().get(i))) {
+ return false;
+ }
+ }
+
+ DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, context.holder);
+ if (singleTarget != null
+ && singleTarget.isInstanceInitializer()
+ && singleTarget.getOptimizationInfo().getTrivialInitializerInfo() != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
}
diff --git a/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
new file mode 100644
index 0000000..ca354f9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
@@ -0,0 +1,212 @@
+// Copyright (c) 2019, 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.librarymethodoverride;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.AssumeMayHaveSideEffects;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LibraryMethodOverrideTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().build();
+ }
+
+ public LibraryMethodOverrideTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(LibraryMethodOverrideTest.class)
+ .addKeepMainRule(TestClass.class)
+ .addOptionsModification(options -> options.enableTreeShakingOfLibraryMethodOverrides = true)
+ .enableClassInliningAnnotations()
+ .enableMergeAnnotations()
+ .enableSideEffectAnnotations()
+ .setMinApi(parameters.getRuntime())
+ .compile()
+ .inspect(this::verifyOutput)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines(
+ "EscapesDirectly",
+ "EscapesIndirectly",
+ "EscapesIndirectlyWithOverrideSub",
+ "DoesNotEscapeWithSubThatOverridesAndEscapesSub");
+ }
+
+ private void verifyOutput(CodeInspector inspector) {
+ List<Class<?>> nonEscapingClasses =
+ ImmutableList.of(
+ DoesNotEscape.class,
+ DoesNotEscapeWithSubThatDoesNotOverride.class,
+ DoesNotEscapeWithSubThatDoesNotOverrideSub.class,
+ DoesNotEscapeWithSubThatOverrides.class,
+ DoesNotEscapeWithSubThatOverridesSub.class,
+ DoesNotEscapeWithSubThatOverridesAndEscapes.class);
+ for (Class<?> nonEscapingClass : nonEscapingClasses) {
+ ClassSubject classSubject = inspector.clazz(nonEscapingClass);
+ assertThat(classSubject, isPresent());
+ assertThat(classSubject.uniqueMethodWithName("toString"), not(isPresent()));
+ }
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ System.out.println(new EscapesDirectly());
+ System.out.println(new EscapesIndirectlySub());
+ System.out.println(new EscapesIndirectlyWithOverrideSub());
+ System.out.println(new DoesNotEscapeWithSubThatOverridesAndEscapesSub());
+
+ new DoesNotEscape();
+ new DoesNotEscapeWithSubThatDoesNotOverride();
+ new DoesNotEscapeWithSubThatDoesNotOverrideSub();
+ new DoesNotEscapeWithSubThatOverrides();
+ new DoesNotEscapeWithSubThatOverridesSub();
+ new DoesNotEscapeWithSubThatOverridesAndEscapes();
+ }
+ }
+
+ static class EscapesDirectly {
+
+ @Override
+ public String toString() {
+ return "EscapesDirectly";
+ }
+ }
+
+ @NeverMerge
+ static class EscapesIndirectly {
+
+ @Override
+ public String toString() {
+ return "EscapesIndirectly";
+ }
+ }
+
+ static class EscapesIndirectlySub extends EscapesIndirectly {}
+
+ @NeverMerge
+ static class EscapesIndirectlyWithOverride {
+
+ @Override
+ public String toString() {
+ return "EscapesIndirectlyWithOverride";
+ }
+ }
+
+ static class EscapesIndirectlyWithOverrideSub extends EscapesIndirectlyWithOverride {
+
+ @Override
+ public String toString() {
+ return "EscapesIndirectlyWithOverrideSub";
+ }
+ }
+
+ @NeverClassInline
+ static class DoesNotEscape {
+
+ @AssumeMayHaveSideEffects
+ DoesNotEscape() {}
+
+ @Override
+ public String toString() {
+ return "DoesNotEscape";
+ }
+ }
+
+ @NeverClassInline
+ static class DoesNotEscapeWithSubThatDoesNotOverride {
+
+ @AssumeMayHaveSideEffects
+ DoesNotEscapeWithSubThatDoesNotOverride() {}
+
+ @Override
+ public String toString() {
+ return "DoesNotEscapeWithSubThatDoesNotOverride";
+ }
+ }
+
+ @NeverClassInline
+ static class DoesNotEscapeWithSubThatDoesNotOverrideSub
+ extends DoesNotEscapeWithSubThatDoesNotOverride {
+
+ @AssumeMayHaveSideEffects
+ DoesNotEscapeWithSubThatDoesNotOverrideSub() {}
+ }
+
+ @NeverClassInline
+ static class DoesNotEscapeWithSubThatOverrides {
+
+ @AssumeMayHaveSideEffects
+ DoesNotEscapeWithSubThatOverrides() {}
+
+ @Override
+ public String toString() {
+ return "DoesNotEscapeWithSubThatOverrides";
+ }
+ }
+
+ @NeverClassInline
+ static class DoesNotEscapeWithSubThatOverridesSub extends DoesNotEscapeWithSubThatOverrides {
+
+ @AssumeMayHaveSideEffects
+ DoesNotEscapeWithSubThatOverridesSub() {}
+
+ @Override
+ public String toString() {
+ return "DoesNotEscapeWithSubThatOverridesSub";
+ }
+ }
+
+ // Note that this class and its subclass is equivalent to DoesNotEscapeWithSubThatOverrides and
+ // DoesNotEscapeWithSubThatOverridesSub, respectively. The difference is that the class DoesNot-
+ // EscapeWithSubThatOverridesAndEscapesSub is escaping from main(), unlike DoesNotEscapeWithSub-
+ // ThatOverridesSub.
+ @NeverClassInline
+ static class DoesNotEscapeWithSubThatOverridesAndEscapes {
+
+ @AssumeMayHaveSideEffects
+ DoesNotEscapeWithSubThatOverridesAndEscapes() {}
+
+ @Override
+ public String toString() {
+ return "DoesNotEscapeWithSubThatOverridesAndEscapes";
+ }
+ }
+
+ @NeverClassInline
+ static class DoesNotEscapeWithSubThatOverridesAndEscapesSub
+ extends DoesNotEscapeWithSubThatOverridesAndEscapes {
+
+ @AssumeMayHaveSideEffects
+ DoesNotEscapeWithSubThatOverridesAndEscapesSub() {}
+
+ @Override
+ public String toString() {
+ return "DoesNotEscapeWithSubThatOverridesAndEscapesSub";
+ }
+ }
+}