Generalize CovariantReturnTypeMultiHandler

The CovariantReturnTypeMultiHandler handles repeated instances of the
CovariantReturnType annotation on the same annotatable element. This
change generalizes the class so that it can work with any repeatable
annotation.

Bug: 119861512
Test: atest class2greylisttest, m -j20 framework
Change-Id: Id42ccc0a335f65701f81ec3fec201258e829f5ae
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
index 6305185..433c2c7 100644
--- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
+++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -17,7 +17,6 @@
 package com.android.class2greylist;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap.Builder;
 import com.google.common.collect.ImmutableSet;
@@ -35,16 +34,10 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
-import java.util.HashMap;
 import java.util.Set;
-import java.util.function.Predicate;
 
 /**
  * Build time tool for extracting a list of members from jar files that have the @UsedByApps
@@ -193,12 +186,31 @@
                 new UnsupportedAppUsageAnnotationHandler(
                     mStatus, mOutput, mPublicApis, TARGET_SDK_TO_LIST_MAP);
         GREYLIST_ANNOTATIONS.forEach(a -> builder.put(a, greylistAnnotationHandler));
+
+        CovariantReturnTypeHandler covariantReturnTypeHandler = new CovariantReturnTypeHandler(
+            mOutput, mPublicApis, FLAG_WHITELIST);
+
+        return addRepeatedAnnotationHandlers(builder, CovariantReturnTypeHandler.ANNOTATION_NAME,
+            CovariantReturnTypeHandler.REPEATED_ANNOTATION_NAME, covariantReturnTypeHandler)
+            .build();
+    }
+
+    /**
+     * Add a handler for an annotation as well as an handler for the container annotation that is
+     * used when the annotation is repeated.
+     *
+     * @param builder the builder for the map to which the handlers will be added.
+     * @param annotationName the name of the annotation.
+     * @param containerAnnotationName the name of the annotation container.
+     * @param handler the handler for the annotation.
+     */
+    private static Builder<String, AnnotationHandler> addRepeatedAnnotationHandlers(
+        Builder<String, AnnotationHandler> builder,
+        String annotationName, String containerAnnotationName,
+        AnnotationHandler handler) {
         return builder
-                .put(CovariantReturnTypeHandler.ANNOTATION_NAME,
-                        new CovariantReturnTypeHandler(mOutput, mPublicApis, FLAG_WHITELIST))
-                .put(CovariantReturnTypeMultiHandler.ANNOTATION_NAME,
-                        new CovariantReturnTypeMultiHandler(mOutput, mPublicApis, FLAG_WHITELIST))
-                .build();
+            .put(annotationName, handler)
+            .put(containerAnnotationName, new RepeatedAnnotationHandler(annotationName, handler));
     }
 
     private void main() throws IOException {
diff --git a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java
index b8de7e9..64d8997 100644
--- a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java
+++ b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java
@@ -30,6 +30,8 @@
 
     private static final String SHORT_NAME = "CovariantReturnType";
     public static final String ANNOTATION_NAME = "Ldalvik/annotation/codegen/CovariantReturnType;";
+    public static final String REPEATED_ANNOTATION_NAME =
+        "Ldalvik/annotation/codegen/CovariantReturnType$CovariantReturnTypes;";
 
     private static final String RETURN_TYPE = "returnType";
 
diff --git a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java
deleted file mode 100644
index f2bc525..0000000
--- a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.android.class2greylist;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
-
-import org.apache.bcel.classfile.AnnotationElementValue;
-import org.apache.bcel.classfile.AnnotationEntry;
-import org.apache.bcel.classfile.ArrayElementValue;
-import org.apache.bcel.classfile.ElementValue;
-import org.apache.bcel.classfile.ElementValuePair;
-
-import java.util.Set;
-
-/**
- * Handles {@code CovariantReturnType$CovariantReturnTypes} annotations, which
- * are generated by the compiler when multiple {@code CovariantReturnType}
- * annotations appear on a single method.
- *
- * <p>The enclosed annotations are passed to {@link CovariantReturnTypeHandler}.
- */
-public class CovariantReturnTypeMultiHandler extends AnnotationHandler {
-
-    public static final String ANNOTATION_NAME =
-            "Ldalvik/annotation/codegen/CovariantReturnType$CovariantReturnTypes;";
-
-    private static final String VALUE = "value";
-
-    private final CovariantReturnTypeHandler mWrappedHandler;
-    private final String mInnerAnnotationName;
-
-    public CovariantReturnTypeMultiHandler(AnnotationConsumer consumer, Set<String> publicApis,
-            String hiddenapiFlag) {
-        this(consumer, publicApis, hiddenapiFlag, CovariantReturnTypeHandler.ANNOTATION_NAME);
-    }
-
-    @VisibleForTesting
-    public CovariantReturnTypeMultiHandler(AnnotationConsumer consumer, Set<String> publicApis,
-            String hiddenapiFlag, String innerAnnotationName) {
-        mWrappedHandler = new CovariantReturnTypeHandler(consumer, publicApis, hiddenapiFlag);
-        mInnerAnnotationName = innerAnnotationName;
-    }
-
-    @Override
-    public void handleAnnotation(AnnotationEntry annotation, AnnotationContext context) {
-        // Verify that the annotation has the form we expect
-        ElementValuePair value = findValue(annotation);
-        if (value == null) {
-            context.reportError("No value found on CovariantReturnType$CovariantReturnTypes");
-            return;
-        }
-        Preconditions.checkArgument(value.getValue() instanceof ArrayElementValue);
-        ArrayElementValue array = (ArrayElementValue) value.getValue();
-
-        // call wrapped handler on each enclosed annotation:
-        for (ElementValue v : array.getElementValuesArray()) {
-            Preconditions.checkArgument(v instanceof AnnotationElementValue);
-            AnnotationElementValue aev = (AnnotationElementValue) v;
-            Preconditions.checkArgument(
-                    aev.getAnnotationEntry().getAnnotationType().equals(mInnerAnnotationName));
-            mWrappedHandler.handleAnnotation(aev.getAnnotationEntry(), context);
-        }
-    }
-
-    private ElementValuePair findValue(AnnotationEntry a) {
-        for (ElementValuePair property : a.getElementValuePairs()) {
-            if (property.getNameString().equals(VALUE)) {
-                return property;
-            }
-        }
-        // not found
-        return null;
-    }
-}
diff --git a/tools/class2greylist/src/com/android/class2greylist/RepeatedAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/RepeatedAnnotationHandler.java
new file mode 100644
index 0000000..61949e3
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/RepeatedAnnotationHandler.java
@@ -0,0 +1,57 @@
+package com.android.class2greylist;
+
+import com.google.common.base.Preconditions;
+import org.apache.bcel.classfile.AnnotationElementValue;
+import org.apache.bcel.classfile.AnnotationEntry;
+import org.apache.bcel.classfile.ArrayElementValue;
+import org.apache.bcel.classfile.ElementValue;
+import org.apache.bcel.classfile.ElementValuePair;
+
+/**
+ * Handles a repeated annotation container.
+ *
+ * <p>The enclosed annotations are passed to the {@link #mWrappedHandler}.
+ */
+public class RepeatedAnnotationHandler extends AnnotationHandler {
+
+    private static final String VALUE = "value";
+
+    private final AnnotationHandler mWrappedHandler;
+    private final String mInnerAnnotationName;
+
+    RepeatedAnnotationHandler(String innerAnnotationName, AnnotationHandler wrappedHandler) {
+        mWrappedHandler = wrappedHandler;
+        mInnerAnnotationName = innerAnnotationName;
+    }
+
+    @Override
+    public void handleAnnotation(AnnotationEntry annotation, AnnotationContext context) {
+        // Verify that the annotation has the form we expect
+        ElementValuePair value = findValue(annotation);
+        if (value == null) {
+            context.reportError("No value found on %s", annotation.getAnnotationType());
+            return;
+        }
+        Preconditions.checkArgument(value.getValue() instanceof ArrayElementValue);
+        ArrayElementValue array = (ArrayElementValue) value.getValue();
+
+        // call wrapped handler on each enclosed annotation:
+        for (ElementValue v : array.getElementValuesArray()) {
+            Preconditions.checkArgument(v instanceof AnnotationElementValue);
+            AnnotationElementValue aev = (AnnotationElementValue) v;
+            Preconditions.checkArgument(
+                    aev.getAnnotationEntry().getAnnotationType().equals(mInnerAnnotationName));
+            mWrappedHandler.handleAnnotation(aev.getAnnotationEntry(), context);
+        }
+    }
+
+    private ElementValuePair findValue(AnnotationEntry a) {
+        for (ElementValuePair property : a.getElementValuePairs()) {
+            if (property.getNameString().equals(VALUE)) {
+                return property;
+            }
+        }
+        // not found
+        return null;
+    }
+}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java
deleted file mode 100644
index 25f2844..0000000
--- a/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.class2greylist;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import static java.util.Collections.emptySet;
-
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-
-import java.io.IOException;
-import java.util.Map;
-
-public class CovariantReturnTypeMultiHandlerTest extends AnnotationHandlerTestBase {
-
-    private static final String FLAG = "test-flag";
-
-    @Before
-    public void setup() throws IOException {
-        // To keep the test simpler and more concise, we don't use the real
-        // @CovariantReturnType annotation here, but use our own @Annotation
-        // and @Annotation.Multi that have the same semantics. It doesn't have
-        // to match the real annotation, just have the same properties
-        // (returnType and value).
-        mJavac.addSource("annotation.Annotation", Joiner.on('\n').join(
-                "package annotation;",
-                "import static java.lang.annotation.RetentionPolicy.CLASS;",
-                "import java.lang.annotation.Repeatable;",
-                "import java.lang.annotation.Retention;",
-                "@Repeatable(Annotation.Multi.class)",
-                "@Retention(CLASS)",
-                "public @interface Annotation {",
-                "  Class<?> returnType();",
-                "  @Retention(CLASS)",
-                "  @interface Multi {",
-                "    Annotation[] value();",
-                "  }",
-                "}"));
-    }
-
-    @Test
-    public void testReturnTypeMulti() throws IOException {
-        mJavac.addSource("a.b.Class", Joiner.on('\n').join(
-                "package a.b;",
-                "import annotation.Annotation;",
-                "public class Class {",
-                "  @Annotation(returnType=Integer.class)",
-                "  @Annotation(returnType=Long.class)",
-                "  public String method() {return null;}",
-                "}"));
-        mJavac.compile();
-
-        Map<String, AnnotationHandler> handlerMap =
-                ImmutableMap.of("Lannotation/Annotation$Multi;",
-                        new CovariantReturnTypeMultiHandler(
-                                mConsumer,
-                                ImmutableSet.of("La/b/Class;->method()Ljava/lang/String;"),
-                                FLAG,
-                                "Lannotation/Annotation;"));
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
-
-        assertNoErrors();
-        ArgumentCaptor<String> whitelist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(2)).consume(whitelist.capture(), any(),
-                eq(ImmutableSet.of(FLAG)));
-        assertThat(whitelist.getAllValues()).containsExactly(
-                "La/b/Class;->method()Ljava/lang/Integer;",
-                "La/b/Class;->method()Ljava/lang/Long;");
-    }
-
-    @Test
-    public void testReturnTypeMultiNotPublicApi() throws IOException {
-        mJavac.addSource("a.b.Class", Joiner.on('\n').join(
-                "package a.b;",
-                "import annotation.Annotation;",
-                "public class Class {",
-                "  @Annotation(returnType=Integer.class)",
-                "  @Annotation(returnType=Long.class)",
-                "  public String method() {return null;}",
-                "}"));
-        mJavac.compile();
-
-        Map<String, AnnotationHandler> handlerMap =
-                ImmutableMap.of("Lannotation/Annotation$Multi;",
-                        new CovariantReturnTypeMultiHandler(
-                                mConsumer,
-                                emptySet(),
-                                FLAG,
-                                "Lannotation/Annotation;"));
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
-
-        verify(mStatus, atLeastOnce()).error(any(), any());
-    }
-}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/RepeatedAnnotationHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/RepeatedAnnotationHandlerTest.java
new file mode 100644
index 0000000..f2f70ee
--- /dev/null
+++ b/tools/class2greylist/test/src/com/android/class2greylist/RepeatedAnnotationHandlerTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.class2greylist;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.bcel.classfile.AnnotationEntry;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RepeatedAnnotationHandlerTest extends AnnotationHandlerTestBase {
+
+    @Before
+    public void setup() {
+        // To keep the test simpler and more concise, we don't use a real annotation here, but use
+        // our own @Annotation and @Annotation.Multi that have the same relationship.
+        mJavac.addSource("annotation.Annotation", Joiner.on('\n').join(
+                "package annotation;",
+                "import static java.lang.annotation.RetentionPolicy.CLASS;",
+                "import java.lang.annotation.Repeatable;",
+                "import java.lang.annotation.Retention;",
+                "@Repeatable(Annotation.Multi.class)",
+                "@Retention(CLASS)",
+                "public @interface Annotation {",
+                "  Class<?> clazz();",
+                "  @Retention(CLASS)",
+                "  @interface Multi {",
+                "    Annotation[] value();",
+                "  }",
+                "}"));
+    }
+
+    @Test
+    public void testRepeated() throws IOException {
+        mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+                "package a.b;",
+                "import annotation.Annotation;",
+                "public class Class {",
+                "  @Annotation(clazz=Integer.class)",
+                "  @Annotation(clazz=Long.class)",
+                "  public String method() {return null;}",
+                "}"));
+        mJavac.compile();
+
+        TestAnnotationHandler handler = new TestAnnotationHandler();
+        Map<String, AnnotationHandler> handlerMap =
+            ImmutableMap.of("Lannotation/Annotation$Multi;",
+                new RepeatedAnnotationHandler("Lannotation/Annotation;", handler));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
+
+        assertNoErrors();
+        assertThat(handler.getClasses()).containsExactly(
+                "Ljava/lang/Integer;",
+                "Ljava/lang/Long;");
+    }
+
+    private static class TestAnnotationHandler extends AnnotationHandler {
+
+        private final List<String> classes;
+
+        private TestAnnotationHandler() {
+            this.classes = new ArrayList<>();
+        }
+
+        @Override
+        void handleAnnotation(AnnotationEntry annotation,
+            AnnotationContext context) {
+            classes.add(annotation.getElementValuePairs()[0].getValue().stringifyValue());
+        }
+
+        private List<String> getClasses() {
+            return classes;
+        }
+    }
+}