Allow class merging in presence of annotations

Bug: 179019716
Change-Id: Iada4c17529ca417d456007750f9cd46daeaf5268
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinker.java b/src/main/java/com/android/tools/r8/ResourceShrinker.java
index b01f9a3..77abde6 100644
--- a/src/main/java/com/android/tools/r8/ResourceShrinker.java
+++ b/src/main/java/com/android/tools/r8/ResourceShrinker.java
@@ -240,7 +240,7 @@
       Stream<DexAnnotation> classAnnotations = classDef.annotations().stream();
       Stream<DexAnnotation> fieldAnnotations =
           Streams.stream(classDef.fields())
-              .filter(DexEncodedField::hasAnnotation)
+              .filter(DexEncodedField::hasAnnotations)
               .flatMap(f -> f.annotations().stream());
       Stream<DexAnnotation> methodAnnotations =
           Streams.stream(classDef.methods())
diff --git a/src/main/java/com/android/tools/r8/graph/DexDefinition.java b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
index c3e6998..8eeda6b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDefinition.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
@@ -21,6 +21,10 @@
     this.annotations = annotations;
   }
 
+  public boolean hasAnnotations() {
+    return !annotations().isEmpty();
+  }
+
   public DexAnnotationSet annotations() {
     return annotations;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 8c501db..1a2b411 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -246,10 +246,6 @@
     return accessFlags.isVolatile();
   }
 
-  public boolean hasAnnotation() {
-    return !annotations().isEmpty();
-  }
-
   public boolean hasExplicitStaticValue() {
     assert accessFlags.isStatic();
     return staticValue != null;
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index aa724b4..a4ccb8e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -543,7 +543,7 @@
 
   private boolean hasAnnotations(DexEncodedField[] fields) {
     synchronized (fields) {
-      return Arrays.stream(fields).anyMatch(DexEncodedField::hasAnnotation);
+      return Arrays.stream(fields).anyMatch(DexEncodedField::hasAnnotations);
     }
   }
 
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 0603b48..9be5702 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexDefinition;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -239,6 +240,16 @@
     }
   }
 
+  private void mergeAnnotations() {
+    assert group.getClasses().stream().filter(DexDefinition::hasAnnotations).count() <= 1;
+    for (DexProgramClass clazz : group.getSources()) {
+      if (clazz.hasAnnotations()) {
+        group.getTarget().setAnnotations(clazz.annotations());
+        break;
+      }
+    }
+  }
+
   private void mergeInterfaces() {
     DexTypeList previousInterfaces = group.getTarget().getInterfaces();
     Set<DexType> interfaces = Sets.newLinkedHashSet(previousInterfaces);
@@ -261,6 +272,7 @@
     fixAccessFlags();
     appendClassIdField();
 
+    mergeAnnotations();
     mergeInterfaces();
 
     mergeVirtualMethods();
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 c1d531d..b700953 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -13,13 +13,14 @@
 import com.android.tools.r8.horizontalclassmerging.policies.DontMergeSynchronizedClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.LimitGroups;
 import com.android.tools.r8.horizontalclassmerging.policies.MinimizeFieldCasts;
-import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotations;
+import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotationClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.NoClassAnnotationCollisions;
 import com.android.tools.r8.horizontalclassmerging.policies.NoClassInitializerWithObservableSideEffects;
-import com.android.tools.r8.horizontalclassmerging.policies.NoClassesOrMembersWithAnnotations;
 import com.android.tools.r8.horizontalclassmerging.policies.NoDirectRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoEnums;
 import com.android.tools.r8.horizontalclassmerging.policies.NoIndirectRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInnerClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceFieldAnnotations;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKeepRules;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinMetadata;
@@ -119,36 +120,44 @@
   }
 
   private List<Policy> getPolicies(RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
-    return ImmutableList.of(
-        new NotMatchedByNoHorizontalClassMerging(appView),
-        new SameInstanceFields(appView),
-        new NoInterfaces(),
-        new NoAnnotations(),
-        new NoEnums(appView),
-        new CheckAbstractClasses(appView),
-        new SyntheticItemsPolicy(appView),
-        new NoClassesOrMembersWithAnnotations(appView),
-        new NoInnerClasses(),
-        new NoClassInitializerWithObservableSideEffects(),
-        new NoNativeMethods(),
-        new NoKeepRules(appView),
-        new NoKotlinMetadata(),
-        new NoServiceLoaders(appView),
-        new NotVerticallyMergedIntoSubtype(appView),
-        new NoDirectRuntimeTypeChecks(runtimeTypeCheckInfo),
-        new NoIndirectRuntimeTypeChecks(appView, runtimeTypeCheckInfo),
-        new PreventMethodImplementation(appView),
-        new DontInlinePolicy(appView),
-        new PreventMergeIntoDifferentMainDexGroups(appView),
-        new AllInstantiatedOrUninstantiated(appView),
-        new SameParentClass(),
-        new SameNestHost(appView),
-        new PreserveMethodCharacteristics(appView),
-        new SameFeatureSplit(appView),
-        new RespectPackageBoundaries(appView),
-        new DontMergeSynchronizedClasses(appView),
-        new MinimizeFieldCasts(),
-        new LimitGroups(appView));
+    List<SingleClassPolicy> singleClassPolicies =
+        ImmutableList.of(
+            new NotMatchedByNoHorizontalClassMerging(appView),
+            new NoAnnotationClasses(),
+            new NoEnums(appView),
+            new NoInnerClasses(),
+            new NoInstanceFieldAnnotations(),
+            new NoInterfaces(),
+            new NoClassInitializerWithObservableSideEffects(),
+            new NoNativeMethods(),
+            new NoKeepRules(appView),
+            new NoKotlinMetadata(),
+            new NoServiceLoaders(appView),
+            new NotVerticallyMergedIntoSubtype(appView),
+            new NoDirectRuntimeTypeChecks(runtimeTypeCheckInfo),
+            new DontInlinePolicy(appView));
+    List<MultiClassPolicy> multiClassPolicies =
+        ImmutableList.of(
+            new SameInstanceFields(appView),
+            new NoClassAnnotationCollisions(),
+            new CheckAbstractClasses(appView),
+            new SyntheticItemsPolicy(appView),
+            new NoIndirectRuntimeTypeChecks(appView, runtimeTypeCheckInfo),
+            new PreventMethodImplementation(appView),
+            new PreventMergeIntoDifferentMainDexGroups(appView),
+            new AllInstantiatedOrUninstantiated(appView),
+            new SameParentClass(),
+            new SameNestHost(appView),
+            new PreserveMethodCharacteristics(appView),
+            new SameFeatureSplit(appView),
+            new RespectPackageBoundaries(appView),
+            new DontMergeSynchronizedClasses(appView),
+            new MinimizeFieldCasts(),
+            new LimitGroups(appView));
+    return ImmutableList.<Policy>builder()
+        .addAll(singleClassPolicies)
+        .addAll(multiClassPolicies)
+        .build();
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
index 9e4ebf5..4b9ad6a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -30,6 +30,11 @@
     this.classes = new LinkedList<>();
   }
 
+  public MergeGroup(DexProgramClass clazz) {
+    this();
+    add(clazz);
+  }
+
   public MergeGroup(Collection<DexProgramClass> classes) {
     this();
     addAll(classes);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotations.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotationClasses.java
similarity index 88%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotations.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotationClasses.java
index 9190247..1effb8b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotations.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotationClasses.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 
-public class NoAnnotations extends SingleClassPolicy {
+public class NoAnnotationClasses extends SingleClassPolicy {
   @Override
   public boolean canMerge(DexProgramClass program) {
     return !program.isAnnotation();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassAnnotationCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassAnnotationCollisions.java
new file mode 100644
index 0000000..9d3d730
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassAnnotationCollisions.java
@@ -0,0 +1,44 @@
+// 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.policies;
+
+import static com.android.tools.r8.utils.IteratorUtils.createCircularIterator;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+public class NoClassAnnotationCollisions extends MultiClassPolicy {
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    // Create a new merge group for each class that has annotations.
+    List<MergeGroup> newGroups = new LinkedList<>();
+    for (DexProgramClass clazz : group) {
+      if (clazz.hasAnnotations()) {
+        newGroups.add(new MergeGroup(clazz));
+      }
+    }
+
+    // If there were at most one class with annotations, then just return the original merge group.
+    if (newGroups.size() <= 1) {
+      return ImmutableList.of(group);
+    }
+
+    // Otherwise, fill up the new merge groups with the classes that do not have annotations.
+    Iterator<MergeGroup> newGroupsIterator = createCircularIterator(newGroups);
+    for (DexProgramClass clazz : group) {
+      if (!clazz.hasAnnotations()) {
+        newGroupsIterator.next().add(clazz);
+      }
+    }
+    return removeTrivialGroups(newGroups);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassesOrMembersWithAnnotations.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassesOrMembersWithAnnotations.java
deleted file mode 100644
index f0b2a6e..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassesOrMembersWithAnnotations.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.policies;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
-
-public class NoClassesOrMembersWithAnnotations extends SingleClassPolicy {
-
-  private final HorizontalClassMergerOptions options;
-
-  public NoClassesOrMembersWithAnnotations(AppView<AppInfoWithLiveness> appView) {
-    this.options = appView.options().horizontalClassMergerOptions();
-  }
-
-  @Override
-  public boolean canMerge(DexProgramClass program) {
-    return !program.hasClassOrMemberAnnotations();
-  }
-
-  @Override
-  public boolean shouldSkipPolicy() {
-    // TODO(b/179019716): Add support for merging in presence of annotations.
-    return options.skipNoClassesOrMembersWithAnnotationsPolicyForTesting;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotations.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFieldAnnotations.java
similarity index 62%
copy from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotations.java
copy to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFieldAnnotations.java
index 9190247..610af7e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAnnotations.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFieldAnnotations.java
@@ -4,12 +4,19 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 
-public class NoAnnotations extends SingleClassPolicy {
+public class NoInstanceFieldAnnotations extends SingleClassPolicy {
+
   @Override
   public boolean canMerge(DexProgramClass program) {
-    return !program.isAnnotation();
+    for (DexEncodedField instanceField : program.instanceFields()) {
+      if (instanceField.hasAnnotations()) {
+        return false;
+      }
+    }
+    return 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 c9ee155..cf7e877 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1163,9 +1163,6 @@
     public int syntheticArgumentCount = 3;
     public int maxGroupSize = 30;
 
-    // TODO(b/179019716): Add support for merging in presence of annotations.
-    public boolean skipNoClassesOrMembersWithAnnotationsPolicyForTesting = false;
-
     public void disable() {
       enable = false;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
index 93b7e4a..3ecdc4f 100644
--- a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.google.common.collect.Iterables;
 import java.util.Iterator;
 import java.util.ListIterator;
 import java.util.NoSuchElementException;
@@ -16,6 +17,27 @@
 
 public class IteratorUtils {
 
+  public static <T> Iterator<T> createCircularIterator(Iterable<T> iterable) {
+    assert !Iterables.isEmpty(iterable);
+    return new Iterator<T>() {
+
+      private Iterator<T> iterator = iterable.iterator();
+
+      @Override
+      public boolean hasNext() {
+        return true;
+      }
+
+      @Override
+      public T next() {
+        if (!iterator.hasNext()) {
+          iterator = iterable.iterator();
+        }
+        return iterator.next();
+      }
+    };
+  }
+
   public static <T> int countRemaining(Iterator<T> iterator) {
     IntBox counter = new IntBox();
     iterator.forEachRemaining(ignore -> counter.increment());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
index 74d0ad6..441934e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
@@ -5,6 +5,7 @@
 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;
@@ -33,6 +34,9 @@
         .addOptionsModification(
             options ->
                 options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
+            inspector -> inspector.assertIsCompleteMergeGroup(A.class, C.class))
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -45,7 +49,8 @@
               assertThat(codeInspector.clazz(MethodAnnotation.class), isPresent());
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B.class), isPresent());
-              assertThat(codeInspector.clazz(C.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(C.class), notIf(isPresent(), enableHorizontalClassMerging));
             });
   }
 
@@ -57,6 +62,7 @@
   @Target({ElementType.METHOD})
   public @interface MethodAnnotation {}
 
+  @TypeAnnotation
   @NeverClassInline
   public static class A {
     public A() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsWithMissingAnnotationsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsWithMissingAnnotationsTest.java
index ef269dc..49b90e7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsWithMissingAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsWithMissingAnnotationsTest.java
@@ -32,7 +32,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public UnusedAnnotatedArgumentsWithMissingAnnotationsTest(TestParameters parameters) {
@@ -72,17 +72,20 @@
             dumpAnnotation2(),
             dumpAnnotation3())
         .addKeepMainRule("Test")
-        .addKeepRules("-keep @interface Annotation?")
-        .addKeepAttributes("RuntimeVisibleParameterAnnotations")
+        .addKeepRules(
+            "-keep @interface Annotation?",
+            "-neverclassinline class *",
+            "-nohorizontalclassmerging class Test$Inner?")
+        .addKeepRuntimeVisibleParameterAnnotations()
         .enableProguardTestOptions()
-        .addKeepRules("-neverclassinline class *")
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .compile()
-        .inspect(inspector -> {
-          checkClass(inspector.clazz("Test$Inner1"), "Annotation1");
-          checkClass(inspector.clazz("Test$Inner2"), "Annotation2");
-          checkClass(inspector.clazz("Test$Inner2"), "Annotation2");
-        })
+        .inspect(
+            inspector -> {
+              checkClass(inspector.clazz("Test$Inner1"), "Annotation1");
+              checkClass(inspector.clazz("Test$Inner2"), "Annotation2");
+              checkClass(inspector.clazz("Test$Inner2"), "Annotation2");
+            })
         .run(parameters.getRuntime(), "Test")
         .assertSuccessWithOutput(expectedOutput);
   }
diff --git a/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java b/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java
index d9b9a3c..218e037 100644
--- a/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java
+++ b/src/test/java/com/android/tools/r8/naming/b139991218/TestRunner.java
@@ -69,11 +69,6 @@
             options -> {
               options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
               options.enableClassInlining = false;
-
-              // TODO(b/179019716): Add support for merging in presence of annotations.
-              options.horizontalClassMergerOptions()
-                      .skipNoClassesOrMembersWithAnnotationsPolicyForTesting =
-                  true;
             })
         .addHorizontallyMergedClassesInspector(
             inspector ->