Add tests for class init change when adding visibility bridges

Bug: 182129249
Bug: 220668540
Bug: 220667525
Change-Id: I8a12a600068fca5e1e9215722ccccd2b2dd98035
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 433349e..f24cac2 100644
--- a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
@@ -80,6 +80,9 @@
 
   public void run(ExecutorService executorService) throws ExecutionException {
     // Collect all visibility bridges to remove.
+    if (!appView.options().enableVisibilityBridgeRemoval) {
+      return;
+    }
     ConcurrentHashMap<DexProgramClass, Set<DexEncodedMethod>> visibilityBridgesToRemove =
         new ConcurrentHashMap<>();
     processItems(
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 3fec28a..05f1434 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -298,6 +298,7 @@
   public boolean readDebugSetFileEvent = false;
   public boolean disableL8AnnotationRemoval =
       System.getProperty("com.android.tools.r8.disableL8AnnotationRemoval") != null;
+  public boolean enableVisibilityBridgeRemoval = true;
 
   public int callGraphLikelySpuriousCallEdgeThreshold = 50;
 
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/StaticFieldClassInitMemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/StaticFieldClassInitMemberRebindingTest.java
new file mode 100644
index 0000000..c31c93d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/StaticFieldClassInitMemberRebindingTest.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2022, 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.memberrebinding;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class StaticFieldClassInitMemberRebindingTest extends TestBase {
+
+  private static final String EXPECTED = "World!";
+  private static final String R8_EXPECTED = "Hello World!";
+
+  private final String NEW_A_DESCRIPTOR = "Lfoo/A;";
+  private final String NEW_B_DESCRIPTOR = "Lfoo/B;";
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClassFileData(
+            getMainWithRewrittenDescriptors(), getAWithPackageFoo(), getBWithRewrittenDescriptors())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(
+            getMainWithRewrittenDescriptors(), getAWithPackageFoo(), getBWithRewrittenDescriptors())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .addOptionsModification(options -> options.enableVisibilityBridgeRemoval = false)
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/220668540): R8 should not change class loading semantics.
+        .assertSuccessWithOutputLines(R8_EXPECTED);
+  }
+
+  private byte[] getAWithPackageFoo() throws Exception {
+    return transformer(A.class).setClassDescriptor(NEW_A_DESCRIPTOR).transform();
+  }
+
+  private byte[] getBWithRewrittenDescriptors() throws Exception {
+    return transformer(B.class)
+        .setClassDescriptor(NEW_B_DESCRIPTOR)
+        .setSuper(NEW_A_DESCRIPTOR)
+        .replaceClassDescriptorInMethodInstructions(descriptor(A.class), NEW_A_DESCRIPTOR)
+        .transform();
+  }
+
+  private byte[] getMainWithRewrittenDescriptors() throws Exception {
+    return transformer(Main.class)
+        .replaceClassDescriptorInMethodInstructions(descriptor(B.class), NEW_B_DESCRIPTOR)
+        .transform();
+  }
+
+  static class /* foo. */ A {
+
+    @NeverInline
+    public static void foo() {
+      System.out.println("World!");
+    }
+  }
+
+  public static class /* foo */ B extends A {
+
+    static {
+      System.out.print("Hello ");
+    }
+  }
+
+  public static class Main {
+
+    @NeverInline
+    public static void test() {
+      B.foo(); // Resolves to A.foo(), hence does not trigger B.<clinit>().
+    }
+
+    public static void main(String[] args) {
+      test();
+      if (System.currentTimeMillis() == 0) {
+        // Needed to ensure we do not remove B.<clinit>() in first round of treeshaking before
+        // running member rebinding analysis.
+        new B();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/RemoveCallToStaticInitTest.java b/src/test/java/com/android/tools/r8/shaking/RemoveCallToStaticInitTest.java
new file mode 100644
index 0000000..0d1fb3d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/RemoveCallToStaticInitTest.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2022, 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;
+
+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.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RemoveCallToStaticInitTest extends TestBase {
+
+  private static final String EXPECTED = "Hello World!";
+  private static final String R8_EXPECTED = "World!";
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(A.class, Main.class)
+        .addProgramClassFileData(getBWithBridge())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, Main.class)
+        .addProgramClassFileData(getBWithBridge())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/220667525): R8 should emit EXPECTED
+        .assertSuccessWithOutputLines(R8_EXPECTED)
+        .inspect(
+            inspector -> {
+              ClassSubject clazz = inspector.clazz(B.class);
+              assertThat(clazz, isPresent());
+              // TODO(b/220667525): Should not remove bridge due to class init.
+              assertThat(clazz.uniqueMethodWithName("foo"), not(isPresent()));
+            });
+  }
+
+  private byte[] getBWithBridge() throws Exception {
+    return transformer(B.class)
+        .setAccessFlags(B.class.getMethod("foo"), MethodAccessFlags::setBridge)
+        .transform();
+  }
+
+  public static class A {
+
+    @NeverInline
+    public static void foo() {
+      System.out.println("World!");
+    }
+  }
+
+  public static class B extends A {
+
+    static {
+      System.out.print("Hello ");
+    }
+
+    @NeverInline
+    public static /* bridge */ void foo() {
+      A.foo();
+    }
+  }
+
+  public static class Main {
+
+    @NeverInline
+    public static void test() {
+      B.foo();
+    }
+
+    public static void main(String[] args) {
+      test();
+    }
+  }
+}