Support merging of classes with static fields

Bug: 163311975

Change-Id: I58d4641525289b315d2d98dd73f1a7716db35f55
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index dc680a8..13c4ac4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -1845,6 +1845,23 @@
             holder);
     return method;
   }
+  /**
+   * Tries to find a method name for insertion into the class {@code target} of the form
+   * baseName$holder$n, where {@code baseName} and {@code holder} are supplied by the user, and
+   * {@code n} is picked to be the first number so that {@code isFresh.apply(method)} returns {@code
+   * true}.
+   *
+   * @param holder indicates where the method originates from.
+   */
+  public DexField createFreshFieldName(
+      DexField template, DexType holder, Predicate<DexField> isFresh) {
+    DexField field =
+        createFreshMember(
+            name -> Optional.of(template.withName(name, this)).filter(isFresh),
+            template.name.toSourceString(),
+            holder);
+    return field;
+  }
 
   public DexMethod createInstanceInitializerWithFreshProto(
       DexMethod method, DexType extraType, Predicate<DexMethod> isFresh) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index f99999a..d441d2c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -49,6 +49,7 @@
   private final FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder;
 
   private final Reference2IntMap<DexType> classIdentifiers = new Reference2IntOpenHashMap<>();
+  private final ClassStaticFieldsMerger classStaticFieldsMerger;
   private final Collection<VirtualMethodMerger> virtualMethodMergers;
   private final Collection<ConstructorMerger> constructorMergers;
   private final DexField classIdField;
@@ -73,6 +74,7 @@
     this.constructorMergers = constructorMergers;
 
     this.dexItemFactory = appView.dexItemFactory();
+    this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, target);
 
     buildClassIdentifierMap();
   }
@@ -115,6 +117,8 @@
           }
         });
 
+    classStaticFieldsMerger.addFields(toMerge);
+
     // Clear the members of the class to be merged since they have now been moved to the target.
     toMerge.setVirtualMethods(null);
     toMerge.setDirectMethods(null);
@@ -161,6 +165,10 @@
     target.appendInstanceField(encodedField);
   }
 
+  void mergeStaticFields() {
+    classStaticFieldsMerger.merge(target);
+  }
+
   public void mergeGroup(SyntheticArgumentClass syntheticArgumentClass) {
     appendClassIdField();
 
@@ -171,6 +179,7 @@
 
     mergeConstructors(syntheticArgumentClass);
     mergeVirtualMethods();
+    mergeStaticFields();
   }
 
   public static class Builder {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
new file mode 100644
index 0000000..ce4b93e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
@@ -0,0 +1,60 @@
+// 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerGraphLens.Builder;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class ClassStaticFieldsMerger {
+  private final Builder lensBuilder;
+  private final DexProgramClass target;
+  private final Map<DexField, DexEncodedField> targetFields = new LinkedHashMap<>();
+  private final DexItemFactory dexItemFactory;
+  private final AppView<?> appView;
+
+  public ClassStaticFieldsMerger(
+      AppView<?> appView,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      DexProgramClass target) {
+    this.appView = appView;
+    this.lensBuilder = lensBuilder;
+
+    this.target = target;
+    // Add mappings for all target fields.
+    target.staticFields().forEach(field -> targetFields.put(field.getReference(), field));
+
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
+  private final boolean isFresh(DexField fieldReference) {
+    return !targetFields.containsKey(fieldReference);
+  }
+
+  private void addField(DexEncodedField field) {
+    DexField oldFieldReference = field.getReference();
+    DexField templateReference = field.getReference().withHolder(target.type, dexItemFactory);
+    DexField newFieldReference =
+        dexItemFactory.createFreshFieldName(templateReference, field.holder(), this::isFresh);
+
+    field = field.toTypeSubstitutedField(newFieldReference);
+    targetFields.put(newFieldReference, field);
+
+    lensBuilder.mapField(oldFieldReference, newFieldReference);
+  }
+
+  public void addFields(DexProgramClass toMerge) {
+    toMerge.staticFields().forEach(this::addField);
+  }
+
+  public void merge(DexProgramClass clazz) {
+    clazz.setStaticFields(targetFields.values().toArray(DexEncodedField.EMPTY_ARRAY));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index 5dc81b1..225f8fe 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -17,8 +17,8 @@
 import com.android.tools.r8.horizontalclassmerging.policies.NoClassesOrMembersWithAnnotations;
 import com.android.tools.r8.horizontalclassmerging.policies.NoClassesWithInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.NoEnums;
-import com.android.tools.r8.horizontalclassmerging.policies.NoFields;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInnerClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceFields;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKeepRules;
 import com.android.tools.r8.horizontalclassmerging.policies.NoNativeMethods;
@@ -59,7 +59,7 @@
     List<Policy> policies =
         ImmutableList.of(
             new NotMatchedByNoHorizontalClassMerging(appView),
-            new NoFields(),
+            new NoInstanceFields(),
             // TODO(b/166071504): Allow merging of classes that implement interfaces.
             new NoInterfaces(),
             new NoClassesWithInterfaces(),
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index 867725b..8dadc71 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -123,6 +123,10 @@
     }
 
     public Builder mapField(DexField from, DexField to) {
+      DexField previousFrom = fieldMap.inverse().remove(from);
+      if (previousFrom != null) {
+        from = previousFrom;
+      }
       fieldMap.put(from, to);
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFields.java
similarity index 78%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoFields.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFields.java
index 9ba49a4..326e94a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoFields.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFields.java
@@ -7,10 +7,9 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 
-public class NoFields extends SingleClassPolicy {
+public class NoInstanceFields extends SingleClassPolicy {
   @Override
   public boolean canMerge(DexProgramClass program) {
-    // TODO(b/165498187): remove this policy
-    return !program.hasFields();
+    return !program.hasInstanceFields();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithStaticFields.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithStaticFields.java
new file mode 100644
index 0000000..f00e07e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithStaticFields.java
@@ -0,0 +1,67 @@
+// 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.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class ClassesWithStaticFields extends HorizontalClassMergingTestBase {
+  public ClassesWithStaticFields(TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("a: 0", "b: 0", "a: 1", "b: 1")
+        .inspect(
+            codeInspector -> {
+              assertThat(codeInspector.clazz(A.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(B.class), notIf(isPresent(), enableHorizontalClassMerging));
+            });
+  }
+
+  @NeverClassInline
+  public static class A {
+    public static int counter;
+
+    public A() {
+      System.out.println("a: " + counter++);
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    public static int counter;
+
+    public B() {
+      System.out.println("b: " + counter++);
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A();
+      new B();
+      new A();
+      new B();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/GenericStaticFieldTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/GenericStaticFieldTest.java
new file mode 100644
index 0000000..9b86b43
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/GenericStaticFieldTest.java
@@ -0,0 +1,73 @@
+// 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.TestParameters;
+import org.junit.Test;
+
+public class GenericStaticFieldTest extends HorizontalClassMergingTestBase {
+  public GenericStaticFieldTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-keepattributes Signatures")
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        // .addHorizontallyMergedClassesInspectorIf(
+        //    enableHorizontalClassMerging, inspector ->
+        // inspector.assertMergedInto(EmptyClassTest.B.class, EmptyClassTest.A.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("false", "false")
+        .inspect(codeInspector -> {});
+  }
+
+  @NeverClassInline
+  public static class A<Z extends Object> {
+    public A(Z z) {
+      if (System.currentTimeMillis() < 0) {
+        z.toString();
+      }
+    }
+  }
+
+  @NeverClassInline
+  public static class B {}
+
+  @NeverClassInline
+  public static class Foo {
+    public static A<Object> field;
+    public static A<B> field2;
+
+    public Foo() {
+      field = new A<Object>(new Object());
+    }
+  }
+
+  @NeverClassInline
+  public static class Foo2 {
+    public static A<Object> field;
+
+    public Foo2() {
+      field = new A<Object>(new Object());
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      Foo foo = new Foo();
+      Foo2 foo2 = new Foo2();
+      System.out.println(foo.field.equals(System.out));
+      System.out.println(foo2.field.equals(System.out));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java
index a02bed3..36e56f2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java
@@ -5,7 +5,9 @@
 package com.android.tools.r8.ir.optimize.staticizer.movetohost;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class CandidateOkSideEffects {
   @NeverInline
   public String foo() {
diff --git a/src/test/java/com/android/tools/r8/naming/ReservedFieldNameInSuperClassTest.java b/src/test/java/com/android/tools/r8/naming/ReservedFieldNameInSuperClassTest.java
index e7f32e3..3c264ae 100644
--- a/src/test/java/com/android/tools/r8/naming/ReservedFieldNameInSuperClassTest.java
+++ b/src/test/java/com/android/tools/r8/naming/ReservedFieldNameInSuperClassTest.java
@@ -11,6 +11,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assume.assumeFalse;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
@@ -50,6 +51,7 @@
                 reserveName
                     ? "-keepclassmembernames class " + A.class.getTypeName() + "{ <fields>; }"
                     : "")
+            .enableNoHorizontalClassMergingAnnotations()
             .run(TestClass.class)
             .assertSuccessWithOutput(expectedOutput)
             .inspector();
@@ -78,6 +80,7 @@
         .addLibraryClasses(A.class)
         .addLibraryFiles(runtimeJar(Backend.DEX))
         .addKeepMainRule(TestClass.class)
+        .enableNoHorizontalClassMergingAnnotations()
         .compile()
         .addRunClasspathFiles(testForD8().addProgramClasses(A.class).compile().writeToZip())
         .run(TestClass.class)
@@ -128,6 +131,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class ASub2 extends A {
 
     static String bar;