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";
+    }
+  }
+}