Remove static visibility bridges if possible

Bug: 197490166
Change-Id: Id56690625669c2772edadbd246a8a9b05055ee7a
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 91f6172..2ab471a 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -473,7 +473,7 @@
           // We can now remove visibility bridges. Note that we do not need to update the
           // invoke-targets here, as the existing invokes will simply dispatch to the now
           // visible super-method. MemberRebinding, if run, will then dispatch it correctly.
-          new VisibilityBridgeRemover(appView.withLiveness()).run();
+          new VisibilityBridgeRemover(appView.withLiveness()).run(executorService);
         }
       }
 
@@ -720,7 +720,7 @@
       // Remove unneeded visibility bridges that have been inserted for member rebinding.
       // This can only be done if we have AppInfoWithLiveness.
       if (appView.appInfo().hasLiveness()) {
-        new VisibilityBridgeRemover(appView.withLiveness()).run();
+        new VisibilityBridgeRemover(appView.withLiveness()).run(executorService);
       } else {
         // If we don't have AppInfoWithLiveness here, it must be because we are not shrinking. When
         // we are not shrinking, we can't move visibility bridges. In principle, though, it would be
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 c8b99a4..dc8e0bb 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -465,7 +465,7 @@
     }
     assert potentialHolder.isInterface();
     for (DexEncodedMethod virtualMethod : potentialHolder.virtualMethods()) {
-      if (virtualMethod.getReference().hasSameProtoAndName(method.getReference())
+      if (virtualMethod.getReference().match(method.getReference())
           && virtualMethod.accessFlags.isSameVisibility(method.accessFlags)) {
         return true;
       }
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index 5eaf805..c2eeffa 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -218,14 +218,6 @@
     return false;
   }
 
-  /**
-   * Returns true if the other method has the same name and prototype (including signature and
-   * return type), false otherwise.
-   */
-  public boolean hasSameProtoAndName(DexMethod other) {
-    return name == other.name && proto == other.proto;
-  }
-
   @Override
   public boolean match(DexMethod method) {
     return match(method.getProto(), method.getName());
diff --git a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
index f073314..a990959 100644
--- a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
@@ -3,18 +3,22 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize;
 
+import static com.android.tools.r8.utils.ThreadUtils.processItems;
+
 import com.android.tools.r8.graph.AppView;
 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.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.ForEachable;
-import com.android.tools.r8.utils.IntBox;
 import com.google.common.collect.Sets;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
 public class VisibilityBridgeRemover {
 
@@ -24,50 +28,14 @@
     this.appView = appView;
   }
 
-  private void removeUnneededVisibilityBridgesFromClass(DexProgramClass clazz) {
-    DexEncodedMethod[] newDirectMethods =
-        removeUnneededVisibilityBridges(
-            clazz::forEachProgramDirectMethod, clazz.getMethodCollection().numberOfDirectMethods());
-    if (newDirectMethods != null) {
-      clazz.setDirectMethods(newDirectMethods);
-    }
-    DexEncodedMethod[] newVirtualMethods =
-        removeUnneededVisibilityBridges(
-            clazz::forEachProgramVirtualMethod,
-            clazz.getMethodCollection().numberOfVirtualMethods());
-    if (newVirtualMethods != null) {
-      clazz.setVirtualMethods(newVirtualMethods);
-    }
-  }
-
-  private DexEncodedMethod[] removeUnneededVisibilityBridges(
-      ForEachable<ProgramMethod> methods, int size) {
-    Set<DexEncodedMethod> methodsToBeRemoved = Sets.newIdentityHashSet();
-    methods.forEach(
-        method -> {
-          if (isUnneededVisibilityBridge(method)) {
-            methodsToBeRemoved.add(method.getDefinition());
-          }
-        });
-    if (!methodsToBeRemoved.isEmpty()) {
-      DexEncodedMethod[] newMethods = new DexEncodedMethod[size - methodsToBeRemoved.size()];
-      IntBox i = new IntBox(0);
-      methods.forEach(
-          method -> {
-            if (!methodsToBeRemoved.contains(method.getDefinition())) {
-              newMethods[i.getAndIncrement()] = method.getDefinition();
-            }
-          });
-      return newMethods;
-    }
-    return null;
-  }
-
   private boolean isUnneededVisibilityBridge(ProgramMethod method) {
+    // Clean-up the predicate check.
     if (appView.appInfo().isPinned(method.getReference())) {
       return false;
     }
     DexEncodedMethod definition = method.getDefinition();
+    // TODO(b/198133259): Extend to definitions that are not defined as bridges.
+    // TODO(b/197490164): Remove if method is abstract.
     if (!definition.isBridge() || definition.isAbstract()) {
       return false;
     }
@@ -75,33 +43,67 @@
         new InvokeSingleTargetExtractor(appView.dexItemFactory());
     method.registerCodeReferences(targetExtractor);
     DexMethod target = targetExtractor.getTarget();
-    InvokeKind kind = targetExtractor.getKind();
     // javac-generated visibility forward bridge method has same descriptor (name, signature and
     // return type).
-    if (target != null && target.hasSameProtoAndName(method.getReference())) {
-      assert !definition.isPrivate() && !definition.isInstanceInitializer();
-      if (kind == InvokeKind.SUPER) {
-        // This is a visibility forward, so check for the direct target.
-        DexEncodedMethod targetMethod =
-            appView.appInfo().unsafeResolveMethodDueToDexFormat(target).getSingleTarget();
-        if (targetMethod != null && targetMethod.accessFlags.isPublic()) {
-          if (Log.ENABLED) {
-            Log.info(
-                getClass(),
-                "Removing visibility forwarding %s -> %s",
-                method,
-                targetMethod.getReference());
-          }
-          return true;
-        }
-      }
+    if (target == null || !target.match(method.getReference())) {
+      return false;
     }
+    assert !definition.isPrivate() && !definition.isInstanceInitializer();
+    if (!isTargetingSuperMethod(method, targetExtractor.getKind(), target)) {
+      return false;
+    }
+    // This is a visibility forward, so check for the direct target.
+    DexEncodedMethod targetMethod =
+        appView.appInfo().unsafeResolveMethodDueToDexFormat(target).getSingleTarget();
+    if (targetMethod == null || !targetMethod.accessFlags.isPublic()) {
+      return false;
+    }
+    if (Log.ENABLED) {
+      Log.info(
+          getClass(),
+          "Removing visibility forwarding %s -> %s",
+          method,
+          targetMethod.getReference());
+    }
+    return true;
+  }
+
+  private boolean isTargetingSuperMethod(ProgramMethod method, InvokeKind kind, DexMethod target) {
+    if (kind == InvokeKind.SUPER) {
+      return true;
+    }
+    if (kind == InvokeKind.STATIC) {
+      return appView.appInfo().isStrictSubtypeOf(method.getHolderType(), target.holder);
+    }
+    assert false : "Unexpected invoke-kind for visibility bridge";
     return false;
   }
 
-  public void run() {
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      removeUnneededVisibilityBridgesFromClass(clazz);
-    }
+  public void run(ExecutorService executorService) throws ExecutionException {
+    // Collect all visibility bridges to remove.
+    ConcurrentHashMap<DexProgramClass, Set<DexEncodedMethod>> visibilityBridgesToRemove =
+        new ConcurrentHashMap<>();
+    processItems(
+        appView.appInfo().classes(),
+        clazz -> {
+          Set<DexEncodedMethod> bridgesToRemoveForClass = Sets.newIdentityHashSet();
+          clazz.forEachProgramMethod(
+              method -> {
+                if (isUnneededVisibilityBridge(method)) {
+                  bridgesToRemoveForClass.add(method.getDefinition());
+                }
+              });
+          if (!bridgesToRemoveForClass.isEmpty()) {
+            visibilityBridgesToRemove.put(clazz, bridgesToRemoveForClass);
+          }
+        },
+        executorService);
+    // Remove all bridges found.
+    PrunedItems.Builder builder = PrunedItems.builder();
+    visibilityBridgesToRemove.forEach(
+        (clazz, methods) -> {
+          clazz.getMethodCollection().removeMethods(methods);
+          methods.forEach(method -> builder.addRemovedMethod(method.getReference()));
+        });
   }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index be31b16..acc8410 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -1048,6 +1048,10 @@
       keepInfo.mutate(
           keepInfo -> keepInfo.removeKeepInfoForPrunedItems(prunedItems.getRemovedClasses()));
     }
+    if (!prunedItems.getRemovedMethods().isEmpty()) {
+      keepInfo.mutate(
+          keepInfo -> keepInfo.removeKeepInfoForPrunedItems(prunedItems.getRemovedMethods()));
+    }
     return new AppInfoWithLiveness(this, prunedItems);
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 0dcddda..c260f3e 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -244,10 +244,20 @@
       this.methodRuleInstances = methodRuleInstances;
     }
 
-    public void removeKeepInfoForPrunedItems(Set<DexType> removedClasses) {
-      keepClassInfo.keySet().removeIf(removedClasses::contains);
-      keepFieldInfo.keySet().removeIf(field -> removedClasses.contains(field.getHolderType()));
-      keepMethodInfo.keySet().removeIf(method -> removedClasses.contains(method.getHolderType()));
+    public void removeKeepInfoForPrunedItems(Set<? extends DexReference> removedReferences) {
+      keepClassInfo.keySet().removeIf(removedReferences::contains);
+      keepFieldInfo
+          .keySet()
+          .removeIf(
+              field ->
+                  (removedReferences.contains(field)
+                      || removedReferences.contains(field.getHolderType())));
+      keepMethodInfo
+          .keySet()
+          .removeIf(
+              method ->
+                  (removedReferences.contains(method)
+                      || removedReferences.contains(method.getHolderType())));
     }
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java
index 46727f2..75679e5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java
@@ -9,16 +9,19 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ir.optimize.inliner.testclasses.InliningIntoVisibilityBridgeTestClasses;
 import com.android.tools.r8.ir.optimize.inliner.testclasses.InliningIntoVisibilityBridgeTestClasses.InliningIntoVisibilityBridgeTestClassB;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -28,14 +31,17 @@
 @RunWith(Parameterized.class)
 public class InliningIntoVisibilityBridgeTest extends TestBase {
 
+  private final TestParameters parameters;
   private final boolean neverInline;
 
-  @Parameters(name = "Never inline: {0}")
-  public static Boolean[] data() {
-    return BooleanUtils.values();
+  @Parameters(name = "{0}, never inline: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
   }
 
-  public InliningIntoVisibilityBridgeTest(boolean neverInline) {
+  public InliningIntoVisibilityBridgeTest(TestParameters parameters, boolean neverInline) {
+    this.parameters = parameters;
     this.neverInline = neverInline;
   }
 
@@ -44,7 +50,7 @@
     String expectedOutput = StringUtils.lines("Hello world", "Hello world", "Hello world");
 
     R8TestRunResult result =
-        testForR8(Backend.DEX)
+        testForR8(parameters.getBackend())
             .addInnerClasses(InliningIntoVisibilityBridgeTest.class)
             .addInnerClasses(InliningIntoVisibilityBridgeTestClasses.class)
             .addKeepMainRule(TestClass.class)
@@ -54,8 +60,9 @@
             .applyIf(!neverInline, R8TestBuilder::enableForceInliningAnnotations)
             .enableNoVerticalClassMergingAnnotations()
             .enableProguardTestOptions()
+            .setMinApi(parameters.getApiLevel())
             .compile()
-            .run(TestClass.class)
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expectedOutput);
 
     // Verify that A.method() is only there if there is an explicit -neverinline rule.
@@ -75,9 +82,11 @@
       assertThat(classSubject, isPresent());
 
       MethodSubject methodSubject = classSubject.uniqueMethodWithName("method");
-      assertThat(methodSubject, isPresentAndRenamed());
-      assertEquals(neverInline, methodSubject.isBridge());
-      assertEquals(neverInline, methodSubject.isSynthetic());
+      if (!neverInline) {
+        assertThat(methodSubject, isPresentAndRenamed());
+        assertFalse(methodSubject.isBridge());
+        assertFalse(methodSubject.isSynthetic());
+      }
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveStaticBridgeTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveStaticBridgeTest.java
index b0a7e59..8bdaa09 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveStaticBridgeTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveStaticBridgeTest.java
@@ -4,10 +4,9 @@
 
 package com.android.tools.r8.memberrebinding;
 
-import static com.android.tools.r8.utils.codeinspector.Matchers.isBridge;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -16,7 +15,6 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -57,10 +55,7 @@
             inspector -> {
               ClassSubject clazz = inspector.clazz(B.class);
               assertThat(clazz, isPresent());
-              // TODO(b/197490166): The inserted bridge should be removed.
-              assertEquals(1, clazz.allMethods().size());
-              FoundMethodSubject foundMethodSubject = clazz.allMethods().get(0);
-              assertThat(foundMethodSubject, isBridge());
+              assertTrue(clazz.allMethods().isEmpty());
             });
   }