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();
+    }
+  }
+}