Keep redundant interfaces if class and interface are kept.
Bug: b/318787479
Change-Id: I417d3a914246dc066ff36b96ca64a77a17e0804e
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 57726f6..e8f037e 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.shaking;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DefaultInstanceInitializerCode;
import com.android.tools.r8.graph.DexClass;
@@ -140,8 +142,9 @@
retainReachableInterfacesFrom(type, reachableInterfaces);
}
if (!reachableInterfaces.isEmpty()) {
+ boolean pinnedHolder = !appView.getKeepInfo(clazz).isOptimizationAllowed(appView.options());
removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
- clazz.superType, reachableInterfaces);
+ pinnedHolder, clazz.superType, reachableInterfaces, clazz);
}
if (reachableInterfaces.isEmpty()) {
clazz.interfaces = DexTypeList.empty();
@@ -151,7 +154,7 @@
}
private void removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
- DexType type, Set<DexType> interfaces) {
+ boolean pinnedRoot, DexType type, Set<DexType> interfaces, DexProgramClass context) {
DexClass clazz = appView.definitionFor(type);
if (clazz == null) {
return;
@@ -162,13 +165,22 @@
return;
}
for (DexType itf : clazz.interfaces) {
+ if (pinnedRoot) {
+ DexProgramClass itfClass = asProgramClassOrNull(appView.definitionFor(itf, context));
+ if (itfClass == null
+ || !appView.getKeepInfo(itfClass).isOptimizationAllowed(appView.options())) {
+ // If root-holder and interface are pinned then retain the interface on the root-holder.
+ continue;
+ }
+ }
if (interfaces.remove(itf) && interfaces.isEmpty()) {
return;
}
}
if (clazz.superType != null) {
assert !interfaces.isEmpty();
- removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(clazz.superType, interfaces);
+ removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
+ pinnedRoot, clazz.superType, interfaces, context);
}
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java
new file mode 100644
index 0000000..21f61ce
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2024, 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.ir.optimize.unusedinterfaces;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/* Regression test for b/318787479 */
+@RunWith(Parameterized.class)
+public class UnusedButKeptInterfaceTest extends TestBase {
+
+ static final String EXPECTED = StringUtils.lines("A: I", "B:", "C: I");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public UnusedButKeptInterfaceTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ testForRuntime(parameters)
+ .addInnerClasses(UnusedButKeptInterfaceTest.class)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(UnusedButKeptInterfaceTest.class)
+ .addKeepMainRule(TestClass.class)
+ .addKeepClassRules(I.class, A.class, B.class, C.class)
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ interface I {}
+
+ static class A implements I {}
+
+ static class B extends A {}
+
+ static class C extends A implements I {}
+
+ static class TestClass {
+
+ static String innerName(Class<?> clazz) {
+ String typeName = clazz.getName();
+ return typeName.substring(typeName.lastIndexOf('$') + 1);
+ }
+
+ public static void main(String[] args) {
+ for (Class<?> c : Arrays.asList(A.class, B.class, C.class)) {
+ System.out.print(innerName(c) + ":");
+ for (Class<?> iface : c.getInterfaces()) {
+ System.out.print(" " + innerName(iface));
+ }
+ System.out.println();
+ }
+ }
+ }
+}