Static, horizontal class merging

Change-Id: I8216de0e6aa9531649b88d693ebe75b2d463efc8
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index fd57823..bd63ae5 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -166,6 +166,7 @@
       options.enableMinification = false;
       options.enableInlining = false;
       options.enableClassInlining = false;
+      options.enableHorizontalClassMerging = false;
       options.enableVerticalClassMerging = false;
       options.enableClassStaticizer = false;
       options.outline.enabled = false;
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 2441034..e0f330d 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -251,6 +251,8 @@
     internal.enableInlining = false;
     assert internal.enableClassInlining;
     internal.enableClassInlining = false;
+    assert internal.enableHorizontalClassMerging;
+    internal.enableHorizontalClassMerging = false;
     assert internal.enableVerticalClassMerging;
     internal.enableVerticalClassMerging = false;
     assert internal.enableClassStaticizer;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index b7c538f..94921b7 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -44,6 +44,7 @@
 import com.android.tools.r8.shaking.ReasonPrinter;
 import com.android.tools.r8.shaking.RootSetBuilder;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
+import com.android.tools.r8.shaking.StaticClassMerger;
 import com.android.tools.r8.shaking.TreePruner;
 import com.android.tools.r8.shaking.VerticalClassMerger;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -353,6 +354,14 @@
           timing.end();
         }
         appView.setGraphLense(new MemberRebindingAnalysis(appViewWithLiveness, options).run());
+        if (options.enableHorizontalClassMerging) {
+          StaticClassMerger staticClassMerger = new StaticClassMerger(appViewWithLiveness);
+          appView.setGraphLense(staticClassMerger.run());
+          appViewWithLiveness.setAppInfo(
+              appViewWithLiveness
+                  .appInfo()
+                  .rewrittenWithLense(application.asDirect(), appView.graphLense()));
+        }
         if (options.enableVerticalClassMerging) {
           timing.begin("ClassMerger");
           VerticalClassMerger verticalClassMerger =
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 7b24770..b5eafcd 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -648,6 +648,7 @@
             ? LineNumberOptimization.ON
             : LineNumberOptimization.OFF;
 
+    assert internal.enableHorizontalClassMerging || !proguardConfiguration.isOptimizing();
     assert internal.enableVerticalClassMerging || !proguardConfiguration.isOptimizing();
     if (internal.debug) {
       internal.proguardConfiguration.getKeepAttributes().lineNumberTable = true;
@@ -656,6 +657,7 @@
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       internal.enableInlining = false;
       internal.enableClassInlining = false;
+      internal.enableHorizontalClassMerging = false;
       internal.enableVerticalClassMerging = false;
       internal.enableClassStaticizer = false;
       // TODO(zerny): Should we support outlining in debug mode? b/62937285
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index 626e5cc..afbb13f 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -72,6 +72,10 @@
     return visibilityOrdinal() > other.visibilityOrdinal();
   }
 
+  public boolean isAtLeastAsVisibleAs(AccessFlags other) {
+    return visibilityOrdinal() >= other.visibilityOrdinal();
+  }
+
   private int visibilityOrdinal() {
     // public > protected > package > private
     if (isPublic()) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 22a771b..0ae75c5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -157,6 +157,11 @@
     return compilationState != CompilationState.NOT_PROCESSED;
   }
 
+  public boolean isInitializer() {
+    checkIfObsolete();
+    return isInstanceInitializer() || isClassInitializer();
+  }
+
   public boolean isInstanceInitializer() {
     checkIfObsolete();
     return accessFlags.isConstructor() && !accessFlags.isStatic();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index c7cd614..e188f29 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.desugar.CovariantReturnTypeAnnotationTransformer;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
@@ -732,10 +733,8 @@
     printC1VisualizerHeader(method);
     printMethod(code, "Initial IR (SSA)");
 
-    DexClass holder = definitionFor(method.method.holder);
-    if (method.getCode() != null && method.getCode().isJarCode()
-        && holder.hasKotlinInfo()) {
-      computeKotlinNotNullParamHints(feedback, holder.getKotlinInfo(), method, code);
+    if (method.getCode() != null && method.getCode().isJarCode()) {
+      computeKotlinNotNullParamHints(feedback, method, code);
     }
 
     if (options.canHaveArtStringNewInitBug()) {
@@ -907,11 +906,19 @@
   }
 
   private void computeKotlinNotNullParamHints(
-      OptimizationFeedback feedback, KotlinInfo kotlinInfo, DexEncodedMethod method, IRCode code) {
+      OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
+    DexMethod originalSignature = graphLense().getOriginalMethodSignature(method.method);
+    DexClass originalHolder = definitionFor(originalSignature.holder);
+    if (!originalHolder.hasKotlinInfo()) {
+      return;
+    }
+
     // Use non-null parameter hints in Kotlin metadata if available.
+    KotlinInfo kotlinInfo = originalHolder.getKotlinInfo();
     if (kotlinInfo.hasNonNullParameterHints()) {
-      BitSet hintFromMetadata = kotlinInfo.lookupNonNullParameterHint(
-          method.method.name.toString(), method.method.proto.toDescriptorString());
+      BitSet hintFromMetadata =
+          kotlinInfo.lookupNonNullParameterHint(
+              originalSignature.name.toString(), originalSignature.proto.toDescriptorString());
       if (hintFromMetadata != null) {
         if (hintFromMetadata.length() > 0) {
           feedback.setKotlinNotNullParamHints(method, hintFromMetadata);
@@ -934,12 +941,15 @@
         // to the mentioned intrinsic as the first argument. We do it only for
         // code coming from Java classfile, since after the method is rewritten
         // by R8 this call gets inlined.
-        if (!user.isInvokeStatic() ||
-            user.asInvokeMethod().getInvokedMethod() != checkParameterIsNotNull ||
-            user.inValues().indexOf(argument) != 0) {
+        if (!user.isInvokeStatic()) {
           continue;
         }
-        paramsCheckedForNull.set(index);
+        InvokeMethod invoke = user.asInvokeMethod();
+        DexMethod invokedMethod =
+            appView.graphLense().getOriginalMethodSignature(invoke.getInvokedMethod());
+        if (invokedMethod == checkParameterIsNotNull && user.inValues().indexOf(argument) == 0) {
+          paramsCheckedForNull.set(index);
+        }
       }
     }
     if (paramsCheckedForNull.length() > 0) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 2a74cad..1660374 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -75,7 +75,7 @@
   }
 
   public boolean isBlackListed(DexEncodedMethod method) {
-    return blackList.contains(method.method)
+    return blackList.contains(appView.graphLense().getOriginalMethodSignature(method.method))
         || appView.appInfo().neverInline.contains(method.method)
         || TwrCloseResourceRewriter.isSynthesizedCloseResourceMethod(method.method, converter);
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
new file mode 100644
index 0000000..5d88374
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -0,0 +1,386 @@
+// 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.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
+import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.VerticalClassMerger.IllegalAccessDetector;
+import com.android.tools.r8.utils.FieldSignatureEquivalence;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * This optimization merges all classes that only have static members and private virtual methods.
+ *
+ * <p>If a merge candidate does not access any package-private or protected members, then it is
+ * merged into a global representative. Otherwise it is merged into a representative for its
+ * package. If no such representatives exist, then the merge candidate is promoted to be the
+ * representative.
+ *
+ * <p>Note that, when merging a merge candidate X into Y, this optimization merely moves the members
+ * of X into Y -- it does not change all occurrences of X in the program into Y. This makes the
+ * optimization more applicable, because it would otherwise not be possible to merge two classes if
+ * they inherited from, say, X' and Y' (since multiple inheritance is not allowed).
+ */
+public class StaticClassMerger {
+
+  private final AppView<? extends AppInfoWithLiveness> appView;
+
+  private final Map<String, DexProgramClass> representatives = new HashMap<>();
+
+  private DexProgramClass globalRepresentative = null;
+
+  private final BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
+  private final BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
+
+  public StaticClassMerger(AppView<? extends AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  // TODO(christofferqa): Share top-down traversal with vertical class merger.
+  public GraphLense run() {
+    // Visit classes in top-down order.
+    Deque<DexProgramClass> worklist = new ArrayDeque<>();
+    Set<DexProgramClass> seenBefore = new HashSet<>();
+
+    Iterator<DexProgramClass> classIterator =
+        appView.appInfo().app.classesWithDeterministicOrder().iterator();
+
+    // Visit the program classes in a top-down order according to the class hierarchy.
+    while (classIterator.hasNext() || !worklist.isEmpty()) {
+      if (worklist.isEmpty()) {
+        // Add the ancestors of this class (including the class itself) to the worklist in such a
+        // way that all super types of the class come before the class itself.
+        addAncestorsToWorklist(classIterator.next(), worklist, seenBefore);
+        if (worklist.isEmpty()) {
+          continue;
+        }
+      }
+
+      DexProgramClass clazz = worklist.removeFirst();
+      if (!seenBefore.add(clazz)) {
+        continue;
+      }
+
+      if (satisfiesMergeCriteria(clazz)) {
+        merge(clazz);
+      }
+    }
+
+    BiMap<DexField, DexField> originalFieldSignatures = fieldMapping.inverse();
+    BiMap<DexMethod, DexMethod> originalMethodSignatures = methodMapping.inverse();
+    return new NestedGraphLense(
+        ImmutableMap.of(),
+        methodMapping,
+        fieldMapping,
+        originalFieldSignatures,
+        originalMethodSignatures,
+        appView.graphLense(),
+        appView.dexItemFactory());
+  }
+
+  private void addAncestorsToWorklist(
+      DexProgramClass clazz, Deque<DexProgramClass> worklist, Set<DexProgramClass> seenBefore) {
+    if (seenBefore.contains(clazz)) {
+      return;
+    }
+
+    worklist.addFirst(clazz);
+
+    // Add super classes to worklist.
+    if (clazz.superType != null) {
+      DexClass definition = appView.appInfo().definitionFor(clazz.superType);
+      if (definition != null && definition.isProgramClass()) {
+        addAncestorsToWorklist(definition.asProgramClass(), worklist, seenBefore);
+      }
+    }
+
+    // Add super interfaces to worklist.
+    for (DexType interfaceType : clazz.interfaces.values) {
+      DexClass definition = appView.appInfo().definitionFor(interfaceType);
+      if (definition != null && definition.isProgramClass()) {
+        addAncestorsToWorklist(definition.asProgramClass(), worklist, seenBefore);
+      }
+    }
+  }
+
+  public boolean satisfiesMergeCriteria(DexProgramClass clazz) {
+    if (clazz.accessFlags.isInterface()) {
+      return false;
+    }
+    if (clazz.staticFields().length + clazz.directMethods().length + clazz.virtualMethods().length
+        == 0) {
+      return false;
+    }
+    if (clazz.instanceFields().length > 0) {
+      return false;
+    }
+    if (Arrays.stream(clazz.staticFields())
+        .anyMatch(field -> appView.appInfo().isPinned(field.field))) {
+      return false;
+    }
+    if (Arrays.stream(clazz.directMethods()).anyMatch(DexEncodedMethod::isInitializer)) {
+      return false;
+    }
+    if (!Arrays.stream(clazz.virtualMethods()).allMatch(DexEncodedMethod::isPrivateMethod)) {
+      return false;
+    }
+    if (Streams.stream(clazz.methods())
+        .anyMatch(
+            method ->
+                method.accessFlags.isNative()
+                    || appView.appInfo().isPinned(method.method)
+                    // TODO(christofferqa): Remove the invariant that the graph lense should not
+                    // modify any methods from the sets alwaysInline and noSideEffects.
+                    || appView.appInfo().alwaysInline.contains(method.method)
+                    || appView.appInfo().noSideEffects.keySet().contains(method))) {
+      return false;
+    }
+    return true;
+  }
+
+  public boolean merge(DexProgramClass clazz) {
+    assert satisfiesMergeCriteria(clazz);
+
+    String pkg = clazz.type.getPackageDescriptor();
+    DexProgramClass representativeForPackage = representatives.get(pkg);
+
+    if (mayMergeAcrossPackageBoundaries(clazz)) {
+      if (globalRepresentative != null) {
+        // Merge this class into the global representative.
+        moveMembersFromSourceToTarget(clazz, globalRepresentative);
+        return true;
+      }
+
+      // Make the current class the global representative as well as the representative for this
+      // package.
+      if (Log.ENABLED) {
+        Log.info(getClass(), "Making %s a global representative", clazz.type.toSourceString(), pkg);
+        Log.info(getClass(), "Making %s a representative for %s", clazz.type.toSourceString(), pkg);
+      }
+      globalRepresentative = clazz;
+      representatives.put(pkg, clazz);
+
+      if (representativeForPackage != null) {
+        // If there was a previous representative for this package, we can merge it into the current
+        // class that has just become the global representative.
+        moveMembersFromSourceToTarget(representativeForPackage, clazz);
+        return true;
+      }
+
+      // No merge.
+      return false;
+    }
+
+    if (representativeForPackage != null) {
+      if (clazz.accessFlags.isMoreVisibleThan(representativeForPackage.accessFlags)) {
+        // Use `clazz` as a representative for this package instead.
+        representatives.put(pkg, clazz);
+        moveMembersFromSourceToTarget(representativeForPackage, clazz);
+        return true;
+      }
+
+      // Merge current class into the representative for this package.
+      moveMembersFromSourceToTarget(clazz, representativeForPackage);
+      return true;
+    }
+
+    // No merge.
+    if (Log.ENABLED) {
+      Log.info(getClass(), "Making %s a representative for %s", clazz.type.toSourceString(), pkg);
+    }
+    representatives.put(pkg, clazz);
+    return false;
+  }
+
+  private boolean mayMergeAcrossPackageBoundaries(DexProgramClass clazz) {
+    // Check that the class is public. Otherwise, accesses to `clazz` from within its current
+    // package may become illegal.
+    if (!clazz.accessFlags.isPublic()) {
+      return false;
+    }
+    // Check that all of the members are private or public.
+    if (!Arrays.stream(clazz.directMethods())
+        .allMatch(method -> method.accessFlags.isPrivate() || method.accessFlags.isPublic())) {
+      return false;
+    }
+    if (!Arrays.stream(clazz.staticFields())
+        .allMatch(method -> method.accessFlags.isPrivate() || method.accessFlags.isPublic())) {
+      return false;
+    }
+
+    // Note that a class is only considered a candidate if it has no instance fields and all of its
+    // virtual methods are private. Therefore, we don't need to consider check if there are any
+    // package-private or protected instance fields or virtual methods here.
+    assert Arrays.stream(clazz.instanceFields()).count() == 0;
+    assert Arrays.stream(clazz.virtualMethods()).allMatch(method -> method.accessFlags.isPrivate());
+
+    // Check that no methods access package-private or protected members.
+    IllegalAccessDetector registry = new IllegalAccessDetector(appView.appInfo(), clazz);
+    for (DexEncodedMethod method : clazz.methods()) {
+      method.registerCodeReferences(registry);
+      if (registry.foundIllegalAccess()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private void moveMembersFromSourceToTarget(
+      DexProgramClass sourceClass, DexProgramClass targetClass) {
+    if (Log.ENABLED) {
+      Log.info(
+          getClass(),
+          "Merging %s into %s",
+          sourceClass.type.toSourceString(),
+          targetClass.type.toSourceString());
+    }
+
+    assert targetClass.accessFlags.isAtLeastAsVisibleAs(sourceClass.accessFlags);
+    assert sourceClass.instanceFields().length == 0;
+    assert targetClass.instanceFields().length == 0;
+
+    // Move members from source to target.
+    targetClass.setDirectMethods(
+        mergeMethods(sourceClass.directMethods(), targetClass.directMethods(), targetClass));
+    targetClass.setVirtualMethods(
+        mergeMethods(sourceClass.virtualMethods(), targetClass.virtualMethods(), targetClass));
+    targetClass.setStaticFields(
+        mergeFields(sourceClass.staticFields(), targetClass.staticFields(), targetClass));
+
+    // Cleanup source.
+    sourceClass.setDirectMethods(DexEncodedMethod.EMPTY_ARRAY);
+    sourceClass.setVirtualMethods(DexEncodedMethod.EMPTY_ARRAY);
+    sourceClass.setStaticFields(DexEncodedField.EMPTY_ARRAY);
+  }
+
+  private DexEncodedMethod[] mergeMethods(
+      DexEncodedMethod[] sourceMethods,
+      DexEncodedMethod[] targetMethods,
+      DexProgramClass targetClass) {
+    DexEncodedMethod[] result = new DexEncodedMethod[sourceMethods.length + targetMethods.length];
+
+    // Move all target methods to result.
+    System.arraycopy(targetMethods, 0, result, 0, targetMethods.length);
+
+    // Move source methods to result one by one, renaming them if needed.
+    MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get();
+    Set<Wrapper<DexMethod>> existingMethods =
+        Arrays.stream(targetMethods)
+            .map(targetMethod -> equivalence.wrap(targetMethod.method))
+            .collect(Collectors.toSet());
+
+    Predicate<DexMethod> availableMethodSignatures =
+        method -> !existingMethods.contains(equivalence.wrap(method));
+
+    int i = targetMethods.length;
+    for (DexEncodedMethod sourceMethod : sourceMethods) {
+      DexEncodedMethod sourceMethodAfterMove =
+          renameMethodIfNeeded(sourceMethod, targetClass, availableMethodSignatures);
+      result[i++] = sourceMethodAfterMove;
+
+      DexMethod originalMethod =
+          methodMapping.inverse().getOrDefault(sourceMethod.method, sourceMethod.method);
+      methodMapping.put(originalMethod, sourceMethodAfterMove.method);
+    }
+
+    return result;
+  }
+
+  private DexEncodedField[] mergeFields(
+      DexEncodedField[] sourceFields, DexEncodedField[] targetFields, DexProgramClass targetClass) {
+    DexEncodedField[] result = new DexEncodedField[sourceFields.length + targetFields.length];
+
+    // Move all target fields to result.
+    System.arraycopy(targetFields, 0, result, 0, targetFields.length);
+
+    // Move source fields to result one by one, renaming them if needed.
+    FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
+    Set<Wrapper<DexField>> existingFields =
+        Arrays.stream(targetFields)
+            .map(targetField -> equivalence.wrap(targetField.field))
+            .collect(Collectors.toSet());
+
+    Predicate<DexField> availableFieldSignatures =
+        field -> !existingFields.contains(equivalence.wrap(field));
+
+    int i = targetFields.length;
+    for (DexEncodedField sourceField : sourceFields) {
+      DexEncodedField sourceFieldAfterMove =
+          renameFieldIfNeeded(sourceField, targetClass, availableFieldSignatures);
+      result[i++] = sourceFieldAfterMove;
+
+      DexField originalField =
+          fieldMapping.inverse().getOrDefault(sourceField.field, sourceField.field);
+      fieldMapping.put(originalField, sourceFieldAfterMove.field);
+    }
+
+    return result;
+  }
+
+  private DexEncodedMethod renameMethodIfNeeded(
+      DexEncodedMethod method,
+      DexProgramClass targetClass,
+      Predicate<DexMethod> availableMethodSignatures) {
+    assert !method.accessFlags.isConstructor();
+    DexString oldName = method.method.name;
+    DexMethod newSignature =
+        appView.dexItemFactory().createMethod(targetClass.type, method.method.proto, oldName);
+    if (!availableMethodSignatures.test(newSignature)) {
+      int count = 1;
+      do {
+        DexString newName = appView.dexItemFactory().createString(oldName.toSourceString() + count);
+        newSignature =
+            appView.dexItemFactory().createMethod(targetClass.type, method.method.proto, newName);
+        count++;
+      } while (!availableMethodSignatures.test(newSignature));
+    }
+    return method.toTypeSubstitutedMethod(newSignature);
+  }
+
+  private DexEncodedField renameFieldIfNeeded(
+      DexEncodedField field,
+      DexProgramClass targetClass,
+      Predicate<DexField> availableFieldSignatures) {
+    DexString oldName = field.field.name;
+    DexField newSignature =
+        appView.dexItemFactory().createField(targetClass.type, field.field.type, oldName);
+    if (!availableFieldSignatures.test(newSignature)) {
+      int count = 1;
+      do {
+        DexString newName = appView.dexItemFactory().createString(oldName.toSourceString() + count);
+        newSignature =
+            appView.dexItemFactory().createField(targetClass.type, field.field.type, newName);
+        count++;
+      } while (!availableFieldSignatures.test(newSignature));
+    }
+    return field.toTypeSubstitutedField(newSignature);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index d41bd69..2824718 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfo.ResolutionResult;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Code;
@@ -18,7 +19,6 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
@@ -477,7 +477,7 @@
     // Check that all accesses from [source] to classes or members from the current package of
     // [source] will continue to work. This is guaranteed if the methods of [source] do not access
     // any private or protected classes or members from the current package of [source].
-    IllegalAccessDetector registry = new IllegalAccessDetector(options.itemFactory, source);
+    IllegalAccessDetector registry = new IllegalAccessDetector(appInfo, source);
     for (DexEncodedMethod method : source.methods()) {
       method.registerCodeReferences(registry);
       if (registry.foundIllegalAccess()) {
@@ -1334,7 +1334,7 @@
   private class TreeFixer {
 
     private final Builder lense = GraphLense.builder();
-    Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
+    private final Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
 
     private GraphLense fixupTypeReferences(GraphLense graphLense) {
       // Globally substitute merged class types in protos and holders.
@@ -1345,7 +1345,7 @@
         clazz.setStaticFields(substituteTypesIn(clazz.staticFields()));
         clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields()));
       }
-      // Record type renamings so instanceof and checkcast checks are also fixed.
+      // Record type renamings so check-cast and instance-of checks are also fixed.
       for (DexType type : mergedClasses.keySet()) {
         DexType fixed = fixupType(type);
         lense.map(type, fixed);
@@ -1649,12 +1649,16 @@
 
   // Searches for a reference to a non-public class, field or method declared in the same package
   // as [source].
-  private class IllegalAccessDetector extends UseRegistry {
-    private boolean foundIllegalAccess = false;
-    private DexClass source;
+  public static class IllegalAccessDetector extends UseRegistry {
 
-    public IllegalAccessDetector(DexItemFactory factory, DexClass source) {
-      super(factory);
+    private boolean foundIllegalAccess = false;
+
+    private final AppInfo appInfo;
+    private final DexClass source;
+
+    public IllegalAccessDetector(AppInfo appInfo, DexClass source) {
+      super(appInfo.dexItemFactory);
+      this.appInfo = appInfo;
       this.source = source;
     }
 
@@ -1664,7 +1668,8 @@
 
     private boolean checkFieldReference(DexField field) {
       if (!foundIllegalAccess) {
-        if (field.clazz.isSamePackage(source.type)) {
+        DexType baseType = field.clazz.toBaseType(appInfo.dexItemFactory);
+        if (baseType.isClassType() && baseType.isSamePackage(source.type)) {
           checkTypeReference(field.clazz);
           checkTypeReference(field.type);
 
@@ -1679,7 +1684,8 @@
 
     private boolean checkMethodReference(DexMethod method) {
       if (!foundIllegalAccess) {
-        if (method.holder.isSamePackage(source.type)) {
+        DexType baseType = method.holder.toBaseType(appInfo.dexItemFactory);
+        if (baseType.isClassType() && baseType.isSamePackage(source.type)) {
           checkTypeReference(method.holder);
           checkTypeReference(method.proto.returnType);
           for (DexType type : method.proto.parameters.values) {
@@ -1696,8 +1702,9 @@
 
     private boolean checkTypeReference(DexType type) {
       if (!foundIllegalAccess) {
-        if (type.isClassType() && type.isSamePackage(source.type)) {
-          DexClass clazz = appInfo.definitionFor(type);
+        DexType baseType = type.toBaseType(appInfo.dexItemFactory);
+        if (baseType.isClassType() && baseType.isSamePackage(source.type)) {
+          DexClass clazz = appInfo.definitionFor(baseType);
           if (clazz == null || !clazz.accessFlags.isPublic()) {
             foundIllegalAccess = true;
           }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 65d1e3a..c76841d 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -79,6 +79,7 @@
     itemFactory = proguardConfiguration.getDexItemFactory();
     // -dontoptimize disables optimizations by flipping related flags.
     if (!proguardConfiguration.isOptimizing()) {
+      enableHorizontalClassMerging = false;
       enableVerticalClassMerging = false;
       enableDevirtualization = false;
       enableNonNullTracking = false;
@@ -97,6 +98,7 @@
   public boolean passthroughDexCode = false;
 
   // Optimization-related flags. These should conform to -dontoptimize.
+  public boolean enableHorizontalClassMerging = true;
   public boolean enableVerticalClassMerging = true;
   public boolean enableDevirtualization = true;
   public boolean enableNonNullTracking = true;
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 6d5a158..7397c39 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -53,7 +53,15 @@
   }
 
   public T addProgramClassesAndInnerClasses(Collection<Class<?>> classes) throws IOException {
-    return addProgramFiles(getFilesForClassesAndInnerClasses(classes));
+    return addProgramClasses(classes).addInnerClasses(classes);
+  }
+
+  public T addInnerClasses(Class<?>... classes) throws IOException {
+    return addInnerClasses(Arrays.asList(classes));
+  }
+
+  public T addInnerClasses(Collection<Class<?>> classes) throws IOException {
+    return addProgramFiles(getFilesForInnerClasses(classes));
   }
 
   public abstract T addLibraryFiles(Collection<Path> files);
@@ -74,15 +82,14 @@
     return ListUtils.map(classes, ToolHelper::getClassFileForTestClass);
   }
 
-  static Collection<Path> getFilesForClassesAndInnerClasses(Collection<Class<?>> classes)
-      throws IOException {
+  static Collection<Path> getFilesForInnerClasses(Collection<Class<?>> classes) throws IOException {
     Set<Path> paths = new HashSet<>();
     for (Class clazz : classes) {
       Path path = ToolHelper.getClassFileForTestClass(clazz);
       String prefix = path.toString().replace(CLASS_EXTENSION, "$");
       paths.addAll(
           ToolHelper.getClassFilesForTestDirectory(
-              path.getParent(), p -> p.equals(path) || p.toString().startsWith(prefix)));
+              path.getParent(), p -> p.toString().startsWith(prefix)));
     }
     return paths;
   }
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
new file mode 100644
index 0000000..2d2788c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
@@ -0,0 +1,87 @@
+// 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 org.junit.Assert.assertEquals;
+
+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 com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.util.List;
+import java.util.stream.Collectors;
+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 StaticClassMergerTest extends TestBase {
+
+  static class StaticMergeCandidateA {
+
+    @NeverInline
+    public static String m() {
+      return "StaticMergeCandidateA.m()";
+    }
+  }
+
+  static class StaticMergeCandidateB {
+
+    @NeverInline
+    public static String m() {
+      return "StaticMergeCandidateB.m()";
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(StaticMergeCandidateA.m());
+      System.out.print(StaticMergeCandidateB.m());
+    }
+  }
+
+  private Backend backend;
+
+  public StaticClassMergerTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Parameters(name = "Backend: {0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  @Test
+  public void testStaticClassIsRemoved() throws Exception {
+    String expected =
+        StringUtils.joinLines("StaticMergeCandidateA.m()", "StaticMergeCandidateB.m()");
+
+    testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
+
+    CodeInspector inspector =
+        testForR8(backend)
+            .addInnerClasses(StaticClassMergerTest.class)
+            .addKeepMainRule(TestClass.class)
+            .addOptionsModification(options -> options.enableMinification = false)
+            .enableInliningAnnotations()
+            .run(TestClass.class)
+            .assertSuccessWithOutput(expected)
+            .inspector();
+
+    // Check that one of the two static merge candidates has been removed
+    List<FoundClassSubject> classes =
+        inspector.allClasses().stream()
+            .filter(clazz -> clazz.getOriginalName().contains("StaticMergeCandidate"))
+            .collect(Collectors.toList());
+    assertEquals(1, classes.size());
+
+    // Check that the remaining static merge candidate has two methods.
+    FoundClassSubject remaining = classes.get(0);
+    assertEquals(2, remaining.allMethods().size());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
similarity index 99%
rename from src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
rename to src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
index 7a1f831..29f4605 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
@@ -61,7 +61,7 @@
 // TODO(christofferqa): Add tests to check that statically typed invocations on method handles
 // continue to work after class merging.
 @RunWith(VmTestRunner.class)
-public class ClassMergingTest extends TestBase {
+public class VerticalClassMergerTest extends TestBase {
 
   private static final Path CF_DIR =
       Paths.get(ToolHelper.BUILD_DIR).resolve("test/examples/classes/classmerging");
@@ -1154,7 +1154,7 @@
 
     public void test(AndroidApp app, Path proguardMapPath) throws Throwable {
       Path appPath =
-          File.createTempFile("app", ".zip", ClassMergingTest.this.temp.getRoot()).toPath();
+          File.createTempFile("app", ".zip", VerticalClassMergerTest.this.temp.getRoot()).toPath();
       app.writeToZip(appPath, OutputMode.DexIndexed);
 
       DexDebugTestConfig config = new DexDebugTestConfig(appPath);
diff --git a/src/test/java/com/android/tools/r8/naming/b116840216/ReserveOuterClassNameTest.java b/src/test/java/com/android/tools/r8/naming/b116840216/ReserveOuterClassNameTest.java
index b1881ee..b694949 100644
--- a/src/test/java/com/android/tools/r8/naming/b116840216/ReserveOuterClassNameTest.java
+++ b/src/test/java/com/android/tools/r8/naming/b116840216/ReserveOuterClassNameTest.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
@@ -84,7 +85,7 @@
         Origin.unknown());
 
     ToolHelper.allowTestProguardOptions(builder);
-    AndroidApp processedApp = ToolHelper.runR8(builder.build());
+    AndroidApp processedApp = ToolHelper.runR8(builder.build(), this::configure);
 
     CodeInspector inspector = new CodeInspector(processedApp);
     ClassSubject mainSubject = inspector.clazz(mainClass);
@@ -109,6 +110,11 @@
     assertThat(foo, isRenamed());
   }
 
+  private void configure(InternalOptions options) {
+    // Disable horizontal class merging to avoid that all members of Outer are moved to Outer$Inner
+    // or vice versa (both Outer and Outer$Inner are merge candidates for the static class merger).
+    options.enableHorizontalClassMerging = false;
+  }
 
   @Test
   public void test_keepOuterName() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
index f37d6c6..1b7ebe9 100644
--- a/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
@@ -61,7 +62,13 @@
             .addLibraryFiles(library);
     ToolHelper.allowTestProguardOptions(builder);
     builder.addProguardConfiguration(proguardConfiguration, Origin.unknown());
-    return new CodeInspector(ToolHelper.runR8(builder.build()));
+    return new CodeInspector(ToolHelper.runR8(builder.build(), this::configure));
+  }
+
+  private void configure(InternalOptions options) {
+    // Disable horizontal class merging to prevent that A and C are merged (both classes are
+    // candidates for the static class merger).
+    options.enableHorizontalClassMerging = false;
   }
 
   @Test