Don't insert visibility bridges in contexts without access.

Fix: b/427887773
Change-Id: I62cb8e15abc99933653cb2ec585caf2bb0915e14
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolution.java b/src/main/java/com/android/tools/r8/graph/MethodResolution.java
index 9a75cfe..fad11a2 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodResolution.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodResolution.java
@@ -509,7 +509,7 @@
    * Section 5.4.3.4 of the JVM Spec</a>.
    *
    * <p>The resolved method is not the method that will actually be invoked. Which methods gets
-   * invoked depends on the invoke instruction used. However, it is always save to rewrite any
+   * invoked depends on the invoke instruction used. However, it is always safe to rewrite any
    * invoke on the given descriptor to a corresponding invoke on the resolved descriptor, as the
    * resolved method is used as basis for dispatch.
    */
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index e18fa27..7fcedfc 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -321,40 +321,58 @@
         originalClass.accessFlags.isPublic() ? null : method.holder.getPackageDescriptor();
     if (packageDescriptor == null
         || !packageDescriptor.equals(target.getHolderType().getPackageDescriptor())) {
-      DexProgramClass bridgeHolder =
+      DexClass bridgeHolder =
           findHolderForVisibilityBridge(originalClass, target.getHolder(), packageDescriptor);
       assert bridgeHolder != null;
-      bridges.accept(bridgeHolder, method, target);
-      return target.getReference().withHolder(bridgeHolder.getType(), appView.dexItemFactory());
+      if (bridgeHolder.isClasspathClass()) {
+        // Intentionally empty. We do not need to insert a bridge on a classpath class.
+      } else if (bridgeHolder.isLibraryClass()) {
+        // Caution is needed when member rebinding into the library.
+        // This is handled by validMemberRebindingTargetForNonProgramMethod.
+        return null;
+      } else {
+        // Insert a bridge on this program class.
+        bridges.accept(bridgeHolder.asProgramClass(), method, target);
+      }
+      return target.getReference().withHolder(bridgeHolder, appView.dexItemFactory());
     }
     return target.getReference();
   }
 
-  private DexProgramClass findHolderForVisibilityBridge(
-      DexClass originalClass, DexClass targetClass, String packageDescriptor) {
-    if (originalClass == targetClass || originalClass.isNotProgramClass()) {
+  private DexClass findHolderForVisibilityBridge(
+      DexClass currentClass, DexClass resolvedHolder, String initialResolutionPackage) {
+    if (currentClass == resolvedHolder) {
       return null;
     }
-    DexProgramClass newHolder = null;
+    DexClass newHolder = null;
     // Recurse through supertype chain.
-    if (appView.appInfo().isSubtype(originalClass.superType, targetClass.type)) {
-      DexClass superClass = appView.definitionFor(originalClass.superType);
-      newHolder = findHolderForVisibilityBridge(superClass, targetClass, packageDescriptor);
+    if (appView.appInfo().isSubtype(currentClass.superType, resolvedHolder.type)) {
+      DexClass superClass = appView.definitionFor(currentClass.superType);
+      newHolder =
+          findHolderForVisibilityBridge(superClass, resolvedHolder, initialResolutionPackage);
     } else {
-      for (DexType iface : originalClass.interfaces.values) {
-        if (appView.appInfo().isSubtype(iface, targetClass.type)) {
+      for (DexType iface : currentClass.interfaces.values) {
+        if (appView.appInfo().isSubtype(iface, resolvedHolder.type)) {
           DexClass interfaceClass = appView.definitionFor(iface);
-          newHolder = findHolderForVisibilityBridge(interfaceClass, targetClass, packageDescriptor);
+          newHolder =
+              findHolderForVisibilityBridge(
+                  interfaceClass, resolvedHolder, initialResolutionPackage);
         }
       }
     }
     if (newHolder != null) {
       // A supertype fulfills the visibility requirements.
       return newHolder;
-    } else if (originalClass.accessFlags.isPublic()
-        || originalClass.type.getPackageDescriptor().equals(packageDescriptor)) {
-      // This class is visible. Return it if it is a program class, otherwise null.
-      return originalClass.asProgramClass();
+    } else {
+      boolean hasAccessToTarget =
+          AccessControl.isClassAccessible(resolvedHolder, currentClass, appView).isTrue();
+      boolean isVisibleToCallers =
+          currentClass.isPublic()
+              || currentClass.type.getPackageDescriptor().equals(initialResolutionPackage);
+      if (hasAccessToTarget && isVisibleToCallers) {
+        // This class is visible and can access the target class.
+        return currentClass;
+      }
     }
     return null;
   }
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
new file mode 100644
index 0000000..e87a04d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
@@ -0,0 +1,143 @@
+// Copyright (c) 2025, 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.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.memberrebinding.testclasses.MemberRebindingClasspathSplitTestClasses;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+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 MemberRebindingClasspathSplitTest extends TestBase {
+  @Parameter(0)
+  public TestParameters parameters;
+
+  private static class TestConfig {
+    private final String desc;
+
+    private final ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath;
+    private final ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath;
+    private final ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath;
+
+    private TestConfig(
+        String desc,
+        ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath,
+        ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath,
+        ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath) {
+      this.desc = desc;
+      this.addToClasspath = addToClasspath;
+      this.addToProgrampath = addToProgrampath;
+      this.addToRunClasspath = addToRunClasspath;
+    }
+
+    @Override
+    public String toString() {
+      return desc;
+    }
+  }
+
+  @Parameter(1)
+  public TestConfig split;
+
+  @Parameters(name = "{0}, classpathsplit: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        ImmutableList.of(
+            new TestConfig(
+                "Both A and B on classpath",
+                b -> {
+                  b.addClasspathClasses(
+                      MemberRebindingClasspathSplitTestClasses.getA(),
+                      MemberRebindingClasspathSplitTestClasses.getB());
+                },
+                b -> {},
+                b -> {
+                  b.addRunClasspathClasses(
+                      MemberRebindingClasspathSplitTestClasses.getA(),
+                      MemberRebindingClasspathSplitTestClasses.getB());
+                }),
+            new TestConfig(
+                "Both A and B on classpath, m() bridge removed from B",
+                b -> {
+                  b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                  b.addClasspathClassFileData(
+                      transformer(MemberRebindingClasspathSplitTestClasses.getB())
+                          .removeMethodsWithName("m")
+                          .transform());
+                },
+                b -> {},
+                b -> {
+                  b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                  b.addRunClasspathClassFileData(
+                      transformer(MemberRebindingClasspathSplitTestClasses.getB())
+                          .removeMethodsWithName("m")
+                          .transform());
+                }),
+            new TestConfig(
+                "A on classpath and B on programpath",
+                b -> {
+                  b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                },
+                b -> {
+                  b.addProgramClasses(MemberRebindingClasspathSplitTestClasses.getB());
+                },
+                b -> {
+                  b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                }),
+            new TestConfig(
+                "A on classpath and B on programpath, m() bridge removed from B",
+                b -> {
+                  b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                },
+                b -> {
+                  b.addProgramClassFileData(
+                      transformer(MemberRebindingClasspathSplitTestClasses.getB())
+                          .removeMethodsWithName("m")
+                          .transform());
+                },
+                b -> {
+                  b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                })));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters)
+        .addInnerClasses(getClass())
+        .apply(split.addToClasspath)
+        .apply(split.addToProgrampath)
+        .allowStdoutMessages()
+        .addDontObfuscate()
+        .addDontOptimize()
+        .addKeepRules("-keep class " + Main.class.getTypeName() + " { void main(...); }")
+        .compile()
+        .apply(split.addToRunClasspath)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "A");
+  }
+
+  public static class C extends MemberRebindingClasspathSplitTestClasses.B {
+    public String superm() {
+      return super.m();
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] strArr) {
+      C c = new C();
+      System.out.println(c.m());
+      System.out.println(c.superm());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/testclasses/MemberRebindingClasspathSplitTestClasses.java b/src/test/java/com/android/tools/r8/memberrebinding/testclasses/MemberRebindingClasspathSplitTestClasses.java
new file mode 100644
index 0000000..5c0d27c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/testclasses/MemberRebindingClasspathSplitTestClasses.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2025, 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.testclasses;
+
+public class MemberRebindingClasspathSplitTestClasses {
+  public static Class<?> getA() {
+    return A.class;
+  }
+
+  public static Class<?> getB() {
+    return B.class;
+  }
+
+  static class A {
+    public String m() {
+      return "A";
+    }
+  }
+
+  public static class B extends A {}
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java b/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java
index 9da5d33..c11e2a0 100644
--- a/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java
@@ -31,8 +31,6 @@
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/427887773)
-    assumeFalse(parameters.isRandomPartialCompilation());
     testForR8(parameters)
         .addInnerClasses(Regress426351560TestClasses.class, getClass())
         .addKeepRules("-keep class " + Main.class.getTypeName() + " { *; }")
diff --git a/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java b/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java
index 67019be..76b7c51 100644
--- a/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java
@@ -40,13 +40,7 @@
           .compile()
           .addRunClasspathClasses(Regress426351560TestClasses.getClasses())
           .run(parameters.getRuntime(), Main.class)
-          .applyIf(
-              parameters.isDexRuntime()
-                  && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N),
-              rr -> rr.assertFailureWithErrorThatThrows(IllegalAccessError.class),
-              // for jdk based tests: java.lang.VerifyError: Bad invokespecial instruction:
-              // interface method reference is in an indirect superinterface.
-              rr -> rr.assertFailureWithErrorThatThrows(VerifyError.class));
+          .assertSuccessWithOutputLines("Hello, world!");
     } catch (CompilationFailedException e) {
       if (parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.N)) {
         assertTrue(e.getCause() instanceof AssertionError);