Merge "Do not merge static methods to interfaces"
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index 3a2f4e2..d1e0606 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -13,7 +13,6 @@
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
-import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.android.tools.r8.shaking.VerticalClassMerger.IllegalAccessDetector;
@@ -127,13 +126,11 @@
}
public GraphLense run() {
- // Visit the program classes in a top-down order according to the class hierarchy.
- Iterable<DexProgramClass> classes = appView.appInfo().app.classesWithDeterministicOrder();
- TopDownClassHierarchyTraversal.visit(appView, classes, clazz -> {
+ for (DexProgramClass clazz : appView.appInfo().app.classesWithDeterministicOrder()) {
if (satisfiesMergeCriteria(clazz)) {
merge(clazz);
}
- });
+ }
if (Log.ENABLED) {
Log.info(
getClass(),
@@ -204,6 +201,11 @@
return true;
}
+ private boolean isValidRepresentative(DexProgramClass clazz) {
+ // Disallow interfaces from being representatives, since interface methods require desugaring.
+ return !clazz.isInterface();
+ }
+
private boolean merge(DexProgramClass clazz) {
assert satisfiesMergeCriteria(clazz);
@@ -216,8 +218,12 @@
private boolean mergeGlobally(DexProgramClass clazz, String pkg) {
Representative globalRepresentative = representatives.get(GLOBAL);
if (globalRepresentative == null) {
- // Make the current class the global representative.
- setRepresentative(GLOBAL, getOrCreateRepresentative(clazz, pkg));
+ if (isValidRepresentative(clazz)) {
+ // Make the current class the global representative.
+ setRepresentative(GLOBAL, getOrCreateRepresentative(clazz, pkg));
+ } else {
+ clearRepresentative(GLOBAL);
+ }
// Do not attempt to merge this class inside its own package, because that could lead to
// an increase in the global representative, which is not desirable.
@@ -227,8 +233,12 @@
globalRepresentative.include(clazz);
if (globalRepresentative.isFull()) {
- // Make the current class the global representative instead.
- setRepresentative(GLOBAL, getOrCreateRepresentative(clazz, pkg));
+ if (isValidRepresentative(clazz)) {
+ // Make the current class the global representative instead.
+ setRepresentative(GLOBAL, getOrCreateRepresentative(clazz, pkg));
+ } else {
+ clearRepresentative(GLOBAL);
+ }
// Do not attempt to merge this class inside its own package, because that could lead to
// an increase in the global representative, which is not desirable.
@@ -244,7 +254,8 @@
private boolean mergeInsidePackage(DexProgramClass clazz, String pkg) {
Representative packageRepresentative = representatives.get(pkg);
if (packageRepresentative != null) {
- if (clazz.accessFlags.isMoreVisibleThan(packageRepresentative.clazz.accessFlags)) {
+ if (isValidRepresentative(clazz)
+ && clazz.accessFlags.isMoreVisibleThan(packageRepresentative.clazz.accessFlags)) {
// Use `clazz` as a representative for this package instead.
Representative newRepresentative = getOrCreateRepresentative(clazz, pkg);
newRepresentative.include(packageRepresentative.clazz);
@@ -271,7 +282,9 @@
}
// We were unable to use the current representative for this package (if any).
- setRepresentative(pkg, getOrCreateRepresentative(clazz, pkg));
+ if (isValidRepresentative(clazz)) {
+ setRepresentative(pkg, getOrCreateRepresentative(clazz, pkg));
+ }
return false;
}
@@ -288,6 +301,7 @@
}
private void setRepresentative(String pkg, Representative representative) {
+ assert isValidRepresentative(representative.clazz);
if (Log.ENABLED) {
if (pkg.equals(GLOBAL)) {
Log.info(
@@ -305,6 +319,17 @@
representatives.put(pkg, representative);
}
+ private void clearRepresentative(String pkg) {
+ if (Log.ENABLED) {
+ if (pkg.equals(GLOBAL)) {
+ Log.info(getClass(), "Removing the global representative");
+ } else {
+ Log.info(getClass(), "Removing the representative for package %s", pkg);
+ }
+ }
+ representatives.remove(pkg);
+ }
+
private boolean mayMergeAcrossPackageBoundaries(DexProgramClass clazz) {
// Check that the class is public. Otherwise, accesses to `clazz` from within its current
// package may become illegal.
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerInterfaceTest.java b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerInterfaceTest.java
new file mode 100644
index 0000000..3d6ed26
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerInterfaceTest.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2018, 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.classmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 StaticClassMergerInterfaceTest extends TestBase {
+
+ private final Backend backend;
+
+ @Parameters(name = "Backend: {0}")
+ public static Backend[] data() {
+ return Backend.values();
+ }
+
+ public StaticClassMergerInterfaceTest(Backend backend) {
+ this.backend = backend;
+ }
+
+ @Test
+ public void test() throws Exception {
+ String expectedOutput = StringUtils.lines("In A.a()", "In B.b()", "In C.c()");
+
+ CodeInspector inspector =
+ testForR8(backend)
+ .addInnerClasses(StaticClassMergerInterfaceTest.class)
+ .addKeepMainRule(TestClass.class)
+ .addKeepRules("-dontobfuscate")
+ .enableInliningAnnotations()
+ .enableClassInliningAnnotations()
+ .run(TestClass.class)
+ .assertSuccessWithOutput(expectedOutput)
+ .inspector();
+
+ // Check that A has not been merged into B. The static class merger visits classes in alpha-
+ // betical order. By the time A is processed, there is no merge representative and A is not
+ // a valid merge representative itself, because it is an interface.
+ assertThat(inspector.clazz(A.class), isPresent());
+
+ // By the time B is processed, there is no merge representative, so it should be present.
+ assertThat(inspector.clazz(B.class), isPresent());
+
+ // By the time C is processed, B should be merge candidate. Therefore, we should allow C.c() to
+ // be moved to B *although C is an interface*.
+ assertThat(inspector.clazz(C.class), not(isPresent()));
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ A.a();
+ B.b();
+ C.c();
+ }
+ }
+
+ @NeverClassInline
+ interface A {
+
+ @NeverInline
+ static void a() {
+ System.out.println("In A.a()");
+ }
+ }
+
+ @NeverClassInline
+ static class B {
+
+ @NeverInline
+ static void b() {
+ System.out.println("In B.b()");
+ }
+ }
+
+ @NeverClassInline
+ interface C {
+
+ @NeverInline
+ static void c() {
+ System.out.println("In C.c()");
+ }
+ }
+}