Add tests for horizontal class merging
Change-Id: I18dc91d5728db09e02013d0526c052e99d5f3d6c
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 3a716e3..6922939 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -161,6 +161,7 @@
/** All types that *must* never be inlined due to a configuration directive (testing only). */
public final Set<DexType> neverClassInline;
+ private final Set<DexType> noUnusedInterfaceRemoval;
private final Set<DexType> noVerticalClassMerging;
private final Set<DexType> noHorizontalClassMerging;
private final Set<DexType> noStaticClassMerging;
@@ -228,6 +229,7 @@
Set<DexMethod> neverReprocess,
PredicateSet<DexType> alwaysClassInline,
Set<DexType> neverClassInline,
+ Set<DexType> noUnusedInterfaceRemoval,
Set<DexType> noVerticalClassMerging,
Set<DexType> noHorizontalClassMerging,
Set<DexType> noStaticClassMerging,
@@ -267,6 +269,7 @@
this.neverReprocess = neverReprocess;
this.alwaysClassInline = alwaysClassInline;
this.neverClassInline = neverClassInline;
+ this.noUnusedInterfaceRemoval = noUnusedInterfaceRemoval;
this.noVerticalClassMerging = noVerticalClassMerging;
this.noHorizontalClassMerging = noHorizontalClassMerging;
this.noStaticClassMerging = noStaticClassMerging;
@@ -309,6 +312,7 @@
Set<DexMethod> neverReprocess,
PredicateSet<DexType> alwaysClassInline,
Set<DexType> neverClassInline,
+ Set<DexType> noUnusedInterfaceRemoval,
Set<DexType> noVerticalClassMerging,
Set<DexType> noHorizontalClassMerging,
Set<DexType> noStaticClassMerging,
@@ -351,6 +355,7 @@
this.neverReprocess = neverReprocess;
this.alwaysClassInline = alwaysClassInline;
this.neverClassInline = neverClassInline;
+ this.noUnusedInterfaceRemoval = noUnusedInterfaceRemoval;
this.noVerticalClassMerging = noVerticalClassMerging;
this.noHorizontalClassMerging = noHorizontalClassMerging;
this.noStaticClassMerging = noStaticClassMerging;
@@ -398,6 +403,7 @@
previous.neverReprocess,
previous.alwaysClassInline,
previous.neverClassInline,
+ previous.noUnusedInterfaceRemoval,
previous.noVerticalClassMerging,
previous.noHorizontalClassMerging,
previous.noStaticClassMerging,
@@ -449,6 +455,7 @@
previous.neverReprocess,
previous.alwaysClassInline,
previous.neverClassInline,
+ previous.noUnusedInterfaceRemoval,
previous.noVerticalClassMerging,
previous.noHorizontalClassMerging,
previous.noStaticClassMerging,
@@ -537,6 +544,7 @@
this.neverReprocess = previous.neverReprocess;
this.alwaysClassInline = previous.alwaysClassInline;
this.neverClassInline = previous.neverClassInline;
+ this.noUnusedInterfaceRemoval = previous.noUnusedInterfaceRemoval;
this.noVerticalClassMerging = previous.noVerticalClassMerging;
this.noHorizontalClassMerging = previous.noHorizontalClassMerging;
this.noStaticClassMerging = previous.noStaticClassMerging;
@@ -1035,6 +1043,7 @@
lens.rewriteMethods(neverReprocess),
alwaysClassInline.rewriteItems(lens::lookupType),
lens.rewriteTypes(neverClassInline),
+ lens.rewriteTypes(noUnusedInterfaceRemoval),
lens.rewriteTypes(noVerticalClassMerging),
lens.rewriteTypes(noHorizontalClassMerging),
lens.rewriteTypes(noStaticClassMerging),
@@ -1405,6 +1414,11 @@
.shouldBreak();
}
+ /** All unused interface types that *must* never be pruned. */
+ public Set<DexType> getNoUnusedInterfaceRemovalSet() {
+ return noUnusedInterfaceRemoval;
+ }
+
/**
* All types that *must* never be merged vertically due to a configuration directive (testing
* only).
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 b96a14f..cef391b 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1723,7 +1723,9 @@
return;
}
- if (!appView.options().enableUnusedInterfaceRemoval || mode.isTracingMainDex()) {
+ if (!appView.options().enableUnusedInterfaceRemoval
+ || rootSet.noUnusedInterfaceRemoval.contains(type)
+ || mode.isTracingMainDex()) {
markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, implementer));
} else {
if (liveTypes.contains(clazz)) {
@@ -3181,6 +3183,7 @@
rootSet.neverReprocess,
rootSet.alwaysClassInline,
rootSet.neverClassInline,
+ rootSet.noUnusedInterfaceRemoval,
rootSet.noVerticalClassMerging,
rootSet.noHorizontalClassMerging,
rootSet.noStaticClassMerging,
diff --git a/src/main/java/com/android/tools/r8/shaking/NoUnusedInterfaceRemovalRule.java b/src/main/java/com/android/tools/r8/shaking/NoUnusedInterfaceRemovalRule.java
new file mode 100644
index 0000000..c14f3e8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/NoUnusedInterfaceRemovalRule.java
@@ -0,0 +1,83 @@
+// 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.shaking;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import java.util.List;
+
+public class NoUnusedInterfaceRemovalRule extends ProguardConfigurationRule {
+
+ public static final String RULE_NAME = "nounusedinterfaceremoval";
+
+ public static class Builder
+ extends ProguardConfigurationRule.Builder<NoUnusedInterfaceRemovalRule, Builder> {
+
+ private Builder() {
+ super();
+ }
+
+ @Override
+ public Builder self() {
+ return this;
+ }
+
+ @Override
+ public NoUnusedInterfaceRemovalRule build() {
+ return new NoUnusedInterfaceRemovalRule(
+ origin,
+ getPosition(),
+ source,
+ buildClassAnnotations(),
+ classAccessFlags,
+ negatedClassAccessFlags,
+ classTypeNegated,
+ classType,
+ classNames,
+ buildInheritanceAnnotations(),
+ inheritanceClassName,
+ inheritanceIsExtends,
+ memberRules);
+ }
+ }
+
+ private NoUnusedInterfaceRemovalRule(
+ Origin origin,
+ Position position,
+ String source,
+ List<ProguardTypeMatcher> classAnnotations,
+ ProguardAccessFlags classAccessFlags,
+ ProguardAccessFlags negatedClassAccessFlags,
+ boolean classTypeNegated,
+ ProguardClassType classType,
+ ProguardClassNameList classNames,
+ List<ProguardTypeMatcher> inheritanceAnnotations,
+ ProguardTypeMatcher inheritanceClassName,
+ boolean inheritanceIsExtends,
+ List<ProguardMemberRule> memberRules) {
+ super(
+ origin,
+ position,
+ source,
+ classAnnotations,
+ classAccessFlags,
+ negatedClassAccessFlags,
+ classTypeNegated,
+ classType,
+ classNames,
+ inheritanceAnnotations,
+ inheritanceClassName,
+ inheritanceIsExtends,
+ memberRules);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ String typeString() {
+ return RULE_NAME;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index d846f49..f71d027 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -469,6 +469,11 @@
configurationBuilder.addRule(rule);
return true;
}
+ if (acceptString(NoUnusedInterfaceRemovalRule.RULE_NAME)) {
+ ProguardConfigurationRule rule = parseNoUnusedInterfaceRemovalRule(optionStart);
+ configurationBuilder.addRule(rule);
+ return true;
+ }
if (acceptString(NoVerticalClassMergingRule.RULE_NAME)) {
ProguardConfigurationRule rule = parseNoVerticalClassMergingRule(optionStart);
configurationBuilder.addRule(rule);
@@ -751,6 +756,17 @@
return keepRuleBuilder.build();
}
+ private NoUnusedInterfaceRemovalRule parseNoUnusedInterfaceRemovalRule(Position start)
+ throws ProguardRuleParserException {
+ NoUnusedInterfaceRemovalRule.Builder keepRuleBuilder =
+ NoUnusedInterfaceRemovalRule.builder().setOrigin(origin).setStart(start);
+ parseClassSpec(keepRuleBuilder, false);
+ Position end = getPosition();
+ keepRuleBuilder.setSource(getSourceSnippet(contents, start, end));
+ keepRuleBuilder.setEnd(end);
+ return keepRuleBuilder.build();
+ }
+
private NoVerticalClassMergingRule parseNoVerticalClassMergingRule(Position start)
throws ProguardRuleParserException {
NoVerticalClassMergingRule.Builder keepRuleBuilder =
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 0798093..5199435 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -99,6 +99,7 @@
private final Set<DexMethod> neverReprocess = Sets.newIdentityHashSet();
private final PredicateSet<DexType> alwaysClassInline = new PredicateSet<>();
private final Set<DexType> neverClassInline = Sets.newIdentityHashSet();
+ private final Set<DexType> noUnusedInterfaceRemoval = Sets.newIdentityHashSet();
private final Set<DexType> noVerticalClassMerging = Sets.newIdentityHashSet();
private final Set<DexType> noHorizontalClassMerging = Sets.newIdentityHashSet();
private final Set<DexType> noStaticClassMerging = Sets.newIdentityHashSet();
@@ -246,6 +247,7 @@
|| rule instanceof WhyAreYouNotInliningRule) {
markMatchingMethods(clazz, memberKeepRules, rule, null, ifRule);
} else if (rule instanceof ClassInlineRule
+ || rule instanceof NoUnusedInterfaceRemovalRule
|| rule instanceof NoVerticalClassMergingRule
|| rule instanceof NoHorizontalClassMergingRule
|| rule instanceof NoStaticClassMergingRule
@@ -353,6 +355,7 @@
neverReprocess,
alwaysClassInline,
neverClassInline,
+ noUnusedInterfaceRemoval,
noVerticalClassMerging,
noHorizontalClassMerging,
noStaticClassMerging,
@@ -1238,6 +1241,9 @@
throw new Unreachable();
}
context.markAsUsed();
+ } else if (context instanceof NoUnusedInterfaceRemovalRule) {
+ noUnusedInterfaceRemoval.add(item.asDexClass().type);
+ context.markAsUsed();
} else if (context instanceof NoVerticalClassMergingRule) {
noVerticalClassMerging.add(item.asDexClass().type);
context.markAsUsed();
@@ -1751,6 +1757,7 @@
public final Set<DexMethod> reprocess;
public final Set<DexMethod> neverReprocess;
public final PredicateSet<DexType> alwaysClassInline;
+ public final Set<DexType> noUnusedInterfaceRemoval;
public final Set<DexType> noVerticalClassMerging;
public final Set<DexType> noHorizontalClassMerging;
public final Set<DexType> noStaticClassMerging;
@@ -1778,6 +1785,7 @@
Set<DexMethod> neverReprocess,
PredicateSet<DexType> alwaysClassInline,
Set<DexType> neverClassInline,
+ Set<DexType> noUnusedInterfaceRemoval,
Set<DexType> noVerticalClassMerging,
Set<DexType> noHorizontalClassMerging,
Set<DexType> noStaticClassMerging,
@@ -1812,6 +1820,7 @@
this.reprocess = reprocess;
this.neverReprocess = neverReprocess;
this.alwaysClassInline = alwaysClassInline;
+ this.noUnusedInterfaceRemoval = noUnusedInterfaceRemoval;
this.noVerticalClassMerging = noVerticalClassMerging;
this.noHorizontalClassMerging = noHorizontalClassMerging;
this.noStaticClassMerging = noStaticClassMerging;
@@ -1893,6 +1902,7 @@
}
public void pruneDeadItems(DexDefinitionSupplier definitions, Enqueuer enqueuer) {
+ pruneDeadReferences(noUnusedInterfaceRemoval, definitions, enqueuer);
pruneDeadReferences(noVerticalClassMerging, definitions, enqueuer);
pruneDeadReferences(noHorizontalClassMerging, definitions, enqueuer);
pruneDeadReferences(noStaticClassMerging, definitions, enqueuer);
diff --git a/src/test/java/com/android/tools/r8/NoUnusedInterfaceRemoval.java b/src/test/java/com/android/tools/r8/NoUnusedInterfaceRemoval.java
new file mode 100644
index 0000000..3cc8378
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/NoUnusedInterfaceRemoval.java
@@ -0,0 +1,10 @@
+// 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+public @interface NoUnusedInterfaceRemoval {}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index de56e20..9167acd 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -18,6 +18,7 @@
import com.android.tools.r8.shaking.CollectingGraphConsumer;
import com.android.tools.r8.shaking.NoHorizontalClassMergingRule;
import com.android.tools.r8.shaking.NoStaticClassMergingRule;
+import com.android.tools.r8.shaking.NoUnusedInterfaceRemovalRule;
import com.android.tools.r8.shaking.NoVerticalClassMergingRule;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
@@ -60,6 +61,7 @@
private boolean enableConstantArgumentAnnotations = false;
private boolean enableInliningAnnotations = false;
private boolean enableMemberValuePropagationAnnotations = false;
+ private boolean enableNoUnusedInterfaceRemovalAnnotations = false;
private boolean enableNoVerticalClassMergingAnnotations = false;
private boolean enableNoHorizontalClassMergingAnnotations = false;
private boolean enableNoStaticClassMergingAnnotations = false;
@@ -83,6 +85,7 @@
if (enableConstantArgumentAnnotations
|| enableInliningAnnotations
|| enableMemberValuePropagationAnnotations
+ || enableNoUnusedInterfaceRemovalAnnotations
|| enableNoVerticalClassMergingAnnotations
|| enableNoHorizontalClassMergingAnnotations
|| enableNoStaticClassMergingAnnotations
@@ -425,6 +428,15 @@
addInternalKeepRules(sb.toString());
}
+ public T enableNoUnusedInterfaceRemovalAnnotations() {
+ if (!enableNoUnusedInterfaceRemovalAnnotations) {
+ enableNoUnusedInterfaceRemovalAnnotations = true;
+ addInternalMatchInterfaceRule(
+ NoUnusedInterfaceRemovalRule.RULE_NAME, NoUnusedInterfaceRemoval.class);
+ }
+ return self();
+ }
+
public T enableNoVerticalClassMergingAnnotations() {
if (!enableNoVerticalClassMergingAnnotations) {
enableNoVerticalClassMergingAnnotations = true;
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 20ea852..1ac00bb 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -7,6 +7,7 @@
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.TestRuntime.CfVm;
import com.android.tools.r8.TestRuntime.NoneRuntime;
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.utils.AndroidApiLevel;
@@ -43,6 +44,10 @@
return runtime.isCf();
}
+ public boolean isCfRuntime(CfVm vm) {
+ return runtime.isCf() && runtime.asCf().getVm() == vm;
+ }
+
public boolean isNoneRuntime() {
return runtime == NoneRuntime.getInstance();
}
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index e4640ec..849e7d7 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -65,6 +65,13 @@
return assertSuccessWithOutputLines(Arrays.asList(expected));
}
+ public RR assertSuccessWithOutputLinesIf(boolean condition, String... expected) {
+ if (condition) {
+ return assertSuccessWithOutputLines(Arrays.asList(expected));
+ }
+ return self();
+ }
+
public RR assertSuccessWithOutputLines(List<String> expected) {
return assertSuccessWithOutput(StringUtils.lines(expected));
}
@@ -74,6 +81,13 @@
return assertFailure();
}
+ public RR assertFailureWithErrorThatMatchesIf(boolean condition, Matcher<String> matcher) {
+ if (condition) {
+ return assertFailureWithErrorThatMatches(matcher);
+ }
+ return self();
+ }
+
public RR assertFailureWithOutput(String expected) {
assertStdoutMatches(is(expected));
return assertFailure();
@@ -82,4 +96,12 @@
public RR assertFailureWithErrorThatThrows(Class<? extends Throwable> expectedError) {
return assertFailureWithErrorThatMatches(containsString(expectedError.getName()));
}
+
+ public RR assertFailureWithErrorThatThrowsIf(
+ boolean condition, Class<? extends Throwable> expectedError) {
+ if (condition) {
+ return assertFailureWithErrorThatThrows(expectedError);
+ }
+ return self();
+ }
}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndInterfaceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndInterfaceMethodCollisionTest.java
new file mode 100644
index 0000000..3b957c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndInterfaceMethodCollisionTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2020, 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.horizontal;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import org.junit.Test;
+
+public class PrivateAndInterfaceMethodCollisionTest extends HorizontalClassMergingTestBase {
+
+ public PrivateAndInterfaceMethodCollisionTest(
+ TestParameters parameters, boolean enableHorizontalClassMerging) {
+ super(parameters, enableHorizontalClassMerging);
+ }
+
+ @Test
+ public void test() throws Exception {
+ // TODO(b/167981556): Should always succeed.
+ boolean expectedToSucceed =
+ !enableHorizontalClassMerging
+ || parameters.isCfRuntime(CfVm.JDK11)
+ || parameters.isDexRuntime();
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+ .addHorizontallyMergedClassesInspectorIf(
+ enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .enableNoUnusedInterfaceRemovalAnnotations()
+ .enableNoVerticalClassMergingAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLinesIf(expectedToSucceed, "A.foo()", "B.bar()", "J.foo()")
+ .assertFailureWithErrorThatThrowsIf(!expectedToSucceed, IllegalAccessError.class);
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ new A().foo();
+ new B().bar();
+ new C().foo();
+ }
+ }
+
+ @NoUnusedInterfaceRemoval
+ @NoVerticalClassMerging
+ interface J {
+
+ @NeverInline
+ default void foo() {
+ System.out.println("J.foo()");
+ }
+ }
+
+ @NeverClassInline
+ static class A {
+
+ @NeverInline
+ private void foo() {
+ System.out.println("A.foo()");
+ }
+ }
+
+ @NoVerticalClassMerging
+ static class B {
+
+ // Only here to make sure that B is not made abstract as a result of tree shaking.
+ @NeverInline
+ public void bar() {
+ System.out.println("B.bar()");
+ }
+ }
+
+ @NeverClassInline
+ static class C extends B implements J {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndStaticMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndStaticMethodCollisionTest.java
new file mode 100644
index 0000000..df03d8a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndStaticMethodCollisionTest.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2020, 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.horizontal;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class PrivateAndStaticMethodCollisionTest extends HorizontalClassMergingTestBase {
+
+ public PrivateAndStaticMethodCollisionTest(
+ TestParameters parameters, boolean enableHorizontalClassMerging) {
+ super(parameters, enableHorizontalClassMerging);
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+ .addHorizontallyMergedClassesInspectorIf(
+ enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("A.foo()", "A.bar()", "B.foo()", "B.bar()");
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ new A().foo();
+ new A().bar();
+ new B().foo();
+ new B().bar();
+ }
+ }
+
+ @NeverClassInline
+ static class A {
+
+ @NeverInline
+ private static void foo() {
+ System.out.println("A.foo()");
+ }
+
+ @NeverInline
+ private void bar() {
+ System.out.println("A.bar()");
+ }
+ }
+
+ @NeverClassInline
+ static class B {
+
+ @NeverInline
+ private void foo() {
+ System.out.println("B.foo()");
+ }
+
+ @NeverInline
+ private static void bar() {
+ System.out.println("B.bar()");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndInterfaceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndInterfaceMethodCollisionTest.java
new file mode 100644
index 0000000..356acd7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndInterfaceMethodCollisionTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2020, 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.horizontal;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class StaticAndInterfaceMethodCollisionTest extends HorizontalClassMergingTestBase {
+
+ public StaticAndInterfaceMethodCollisionTest(
+ TestParameters parameters, boolean enableHorizontalClassMerging) {
+ super(parameters, enableHorizontalClassMerging);
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+ .addHorizontallyMergedClassesInspectorIf(
+ enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .enableNoUnusedInterfaceRemovalAnnotations()
+ .enableNoVerticalClassMergingAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("A.foo()", "A.baz()", "B.bar()", "J.foo()");
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ A.foo();
+ new A().baz();
+ new B().bar();
+ new C().foo();
+ }
+ }
+
+ @NoUnusedInterfaceRemoval
+ @NoVerticalClassMerging
+ interface J {
+
+ @NeverInline
+ default void foo() {
+ System.out.println("J.foo()");
+ }
+ }
+
+ @NeverClassInline
+ static class A {
+
+ @NeverInline
+ static void foo() {
+ System.out.println("A.foo()");
+ }
+
+ // Only here to make sure that A is not made abstract as a result of tree shaking.
+ @NeverInline
+ public void baz() {
+ System.out.println("A.baz()");
+ }
+ }
+
+ @NoVerticalClassMerging
+ static class B {
+
+ // Only here to make sure that B is not made abstract as a result of tree shaking.
+ @NeverInline
+ public void bar() {
+ System.out.println("B.bar()");
+ }
+ }
+
+ @NeverClassInline
+ static class C extends B implements J {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndVirtualMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndVirtualMethodCollisionTest.java
new file mode 100644
index 0000000..52b506f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndVirtualMethodCollisionTest.java
@@ -0,0 +1,82 @@
+// Copyright (c) 2020, 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.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.AssertUtils.assertFailsCompilationIf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class StaticAndVirtualMethodCollisionTest extends HorizontalClassMergingTestBase {
+
+ public StaticAndVirtualMethodCollisionTest(
+ TestParameters parameters, boolean enableHorizontalClassMerging) {
+ super(parameters, enableHorizontalClassMerging);
+ }
+
+ @Test
+ public void test() throws Exception {
+ // TODO(b/172415620): Handle static/virtual method collisions.
+ assertFailsCompilationIf(
+ enableHorizontalClassMerging,
+ () ->
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+ .addHorizontallyMergedClassesInspectorIf(
+ enableHorizontalClassMerging,
+ inspector -> inspector.assertMergedInto(B.class, A.class))
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("A.foo()", "A.bar()", "B.foo()", "B.bar()"),
+ e -> assertThat(e.getCause().getMessage(), containsString("Duplicate method")));
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ new A().foo();
+ new A().bar();
+ new B().foo();
+ new B().bar();
+ }
+ }
+
+ @NeverClassInline
+ static class A {
+
+ @NeverInline
+ public static void foo() {
+ System.out.println("A.foo()");
+ }
+
+ @NeverInline
+ public void bar() {
+ System.out.println("A.bar()");
+ }
+ }
+
+ @NeverClassInline
+ static class B {
+
+ @NeverInline
+ public void foo() {
+ System.out.println("B.foo()");
+ }
+
+ @NeverInline
+ public static void bar() {
+ System.out.println("B.bar()");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AssertUtils.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AssertUtils.java
new file mode 100644
index 0000000..c113b0c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AssertUtils.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2020, 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.utils.codeinspector;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.utils.ThrowingAction;
+import java.util.function.Consumer;
+
+public class AssertUtils {
+
+ public static <E extends Throwable> void assertFailsCompilationIf(
+ boolean condition, ThrowingAction<E> action) throws E {
+ assertFailsCompilationIf(condition, action, null);
+ }
+
+ public static <E extends Throwable> void assertFailsCompilationIf(
+ boolean condition, ThrowingAction<E> action, Consumer<Throwable> consumer) throws E {
+ assertThrowsIf(condition, CompilationFailedException.class, action, consumer);
+ }
+
+ public static <E extends Throwable> void assertThrowsIf(
+ boolean condition, Class<? extends Throwable> clazz, ThrowingAction<E> action) throws E {
+ assertThrowsIf(condition, clazz, action, null);
+ }
+
+ public static <E extends Throwable> void assertThrowsIf(
+ boolean condition,
+ Class<? extends Throwable> clazz,
+ ThrowingAction<E> action,
+ Consumer<Throwable> consumer)
+ throws E {
+ if (condition) {
+ try {
+ action.execute();
+ fail("Expected action to fail with an exception, but succeeded");
+ } catch (Throwable e) {
+ assertEquals(clazz, e.getClass());
+ if (consumer != null) {
+ consumer.accept(e);
+ }
+ }
+ } else {
+ action.execute();
+ }
+ }
+}