Respect package boundaries in unused interface removal
Bug: 178045782
Change-Id: I5c2866e0431d41c1e2e72c8530986c39c28735d5
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeList.java b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
index 076322a..07410c0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeList.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
@@ -54,6 +54,10 @@
return values.isEmpty() ? DexTypeList.empty() : new DexTypeList(values);
}
+ public DexType get(int index) {
+ return values[index];
+ }
+
public DexTypeList keepIf(Predicate<DexType> predicate) {
DexType[] filtered = ArrayUtils.filter(DexType[].class, values, predicate);
if (filtered != values) {
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 997ef55..7578839 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1758,7 +1758,7 @@
KeepReason reason = KeepReason.reachableFromLiveType(clazz.type);
- for (DexType iface : clazz.interfaces.values) {
+ for (DexType iface : clazz.getInterfaces()) {
markInterfaceTypeAsLiveViaInheritanceClause(iface, clazz);
}
@@ -1846,31 +1846,38 @@
|| rootSet.noUnusedInterfaceRemoval.contains(type)
|| mode.isMainDexTracing()) {
markTypeAsLive(clazz, implementer);
- } else {
- if (liveTypes.contains(clazz)) {
- // The interface is already live, so make sure to report this implements-edge.
- graphReporter.reportClassReferencedFrom(clazz, implementer);
- } else {
- // No need to mark the type as live. If an interface type is only reachable via the
- // inheritance clause of another type it can simply be removed from the inheritance clause.
- // The interface is needed if it has a live default interface method or field, though.
- // Therefore, we record that this implemented-by edge has not been reported, such that we
- // can report it in the future if one its members becomes live.
- WorkList<DexProgramClass> worklist = WorkList.newIdentityWorkList();
- worklist.addIfNotSeen(clazz);
- while (worklist.hasNext()) {
- DexProgramClass current = worklist.next();
- if (liveTypes.contains(current)) {
- continue;
- }
- Set<DexProgramClass> implementors =
- unusedInterfaceTypes.computeIfAbsent(current, ignore -> Sets.newIdentityHashSet());
- if (implementors.add(implementer)) {
- for (DexType iface : current.interfaces.values) {
- DexProgramClass definition = getProgramClassOrNull(iface, current);
- if (definition != null) {
- worklist.addIfNotSeen(definition);
- }
+ return;
+ }
+
+ if (liveTypes.contains(clazz)) {
+ // The interface is already live, so make sure to report this implements-edge.
+ graphReporter.reportClassReferencedFrom(clazz, implementer);
+ return;
+ }
+
+ // No need to mark the type as live. If an interface type is only reachable via the
+ // inheritance clause of another type it can simply be removed from the inheritance clause.
+ // The interface is needed if it has a live default interface method or field, though.
+ // Therefore, we record that this implemented-by edge has not been reported, such that we
+ // can report it in the future if one its members becomes live.
+ WorkList<DexProgramClass> worklist = WorkList.newIdentityWorkList();
+ worklist.addIfNotSeen(clazz);
+ while (worklist.hasNext()) {
+ DexProgramClass current = worklist.next();
+ if (liveTypes.contains(current)) {
+ continue;
+ }
+ Set<DexProgramClass> implementors =
+ unusedInterfaceTypes.computeIfAbsent(current, ignore -> Sets.newIdentityHashSet());
+ if (implementors.add(implementer)) {
+ for (DexType iface : current.getInterfaces()) {
+ DexProgramClass definition = getProgramClassOrNull(iface, current);
+ if (definition != null) {
+ if (definition.isPublic()
+ || implementer.getType().isSamePackage(definition.getType())) {
+ worklist.addIfNotSeen(definition);
+ } else {
+ markTypeAsLive(current, implementer);
}
}
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalPackageBoundaryTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalPackageBoundaryTest.java
new file mode 100644
index 0000000..9c4046d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalPackageBoundaryTest.java
@@ -0,0 +1,91 @@
+// 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.ir.optimize.unusedinterfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ir.optimize.unusedinterfaces.testclasses.UnusedInterfaceRemovalPackageBoundaryTestClasses;
+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.Parameters;
+
+@RunWith(Parameterized.class)
+public class UnusedInterfaceRemovalPackageBoundaryTest extends TestBase {
+
+ private static final Class<?> I_CLASS = UnusedInterfaceRemovalPackageBoundaryTestClasses.getI();
+ private static final Class<?> J_CLASS = UnusedInterfaceRemovalPackageBoundaryTestClasses.J.class;
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public UnusedInterfaceRemovalPackageBoundaryTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass(), UnusedInterfaceRemovalPackageBoundaryTestClasses.class)
+ .addKeepMainRule(TestClass.class)
+ .addKeepClassRules(I_CLASS)
+ .enableNeverClassInliningAnnotations()
+ .enableNoVerticalClassMergingAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .noMinification()
+ .compile()
+ .inspect(
+ inspector -> {
+ ClassSubject iClassSubject = inspector.clazz(I_CLASS);
+ assertThat(iClassSubject, isPresent());
+
+ ClassSubject jClassSubject = inspector.clazz(J_CLASS);
+ assertThat(jClassSubject, isPresent());
+
+ ClassSubject kClassSubject = inspector.clazz(K.class);
+ assertThat(kClassSubject, isAbsent());
+
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertEquals(1, aClassSubject.getDexProgramClass().getInterfaces().size());
+ assertEquals(
+ jClassSubject.getDexProgramClass().getType(),
+ aClassSubject.getDexProgramClass().getInterfaces().get(0));
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("A");
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ new A();
+ }
+ }
+
+ @NoVerticalClassMerging
+ interface K extends UnusedInterfaceRemovalPackageBoundaryTestClasses.J {}
+
+ @NeverClassInline
+ static class A implements K {
+
+ A() {
+ System.out.println("A");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/testclasses/UnusedInterfaceRemovalPackageBoundaryTestClasses.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/testclasses/UnusedInterfaceRemovalPackageBoundaryTestClasses.java
new file mode 100644
index 0000000..0b623b2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/testclasses/UnusedInterfaceRemovalPackageBoundaryTestClasses.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2021, 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.testclasses;
+
+import com.android.tools.r8.NoVerticalClassMerging;
+
+public class UnusedInterfaceRemovalPackageBoundaryTestClasses {
+
+ @NoVerticalClassMerging
+ interface I {}
+
+ @NoVerticalClassMerging
+ public interface J extends I {}
+
+ public static Class<?> getI() {
+ return I.class;
+ }
+}