Model annotations in XProcessing

This CL adds support for AnnotationBox and querying annotations from elements.

We had 1 use case where we query all annotations in room to ensure the
Auto-Value integration does not use unsupported annotations.
To avoid modelling Annotation and an XElement, I've added a helper
method to check annotations by package name. It is a bit ugly API wise
but helps reduce API surface.

Bug: 160322705
Bug: 160323720
Test: compiler-xprocessing tests
Change-Id: I3ba53254cade3a85d1c4db2471f2424a5fe08e50
diff --git a/room/compiler-xprocessing/src/main/java/androidx/room/processing/XAnnotationBox.kt b/room/compiler-xprocessing/src/main/java/androidx/room/processing/XAnnotationBox.kt
new file mode 100644
index 0000000..7a7ced9
--- /dev/null
+++ b/room/compiler-xprocessing/src/main/java/androidx/room/processing/XAnnotationBox.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 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 androidx.room.processing
+
+/**
+ * This wraps an annotation element that is both accessible from the processor and runtime.
+ *
+ * It won't scale to a general purpose processing APIs where an equivelant of the AnnotationMirror
+ * API needs to be provided but works well for Room's case.
+ */
+interface XAnnotationBox<T> {
+    /**
+     * The value field of the annotation
+     */
+    val value: T
+
+    /**
+     * Returns the value of the given [methodName] as a type reference.
+     */
+    fun getAsType(methodName: String): XType?
+
+    /**
+     * Returns the value of the given [methodName] as a list of type references.
+     */
+    fun getAsTypeList(methodName: String): List<XType>
+
+    /**
+     * Returns the value of the given [methodName] as another boxed annotation.
+     */
+    fun <T : Annotation> getAsAnnotationBox(methodName: String): XAnnotationBox<T>
+
+    /**
+     * Returns the value of the given [methodName] as an array of boxed annotations.
+     */
+    fun <T : Annotation> getAsAnnotationBoxArray(methodName: String): Array<XAnnotationBox<T>>
+}
\ No newline at end of file
diff --git a/room/compiler-xprocessing/src/main/java/androidx/room/processing/XElement.kt b/room/compiler-xprocessing/src/main/java/androidx/room/processing/XElement.kt
index fe19167..995d4536 100644
--- a/room/compiler-xprocessing/src/main/java/androidx/room/processing/XElement.kt
+++ b/room/compiler-xprocessing/src/main/java/androidx/room/processing/XElement.kt
@@ -17,6 +17,7 @@
 package androidx.room.processing
 
 import kotlin.contracts.contract
+import kotlin.reflect.KClass
 
 interface XElement {
     val name: String
@@ -41,6 +42,15 @@
 
     fun kindName(): String
 
+    fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>?
+
+    // a very sad method but helps avoid abstraction annotation
+    fun hasAnnotationInPackage(pkg: String): Boolean
+
+    fun hasAnnotation(annotation: KClass<out Annotation>): Boolean
+
+    fun hasAnyOf(vararg annotations: KClass<out Annotation>) = annotations.any(this::hasAnnotation)
+
     fun asTypeElement() = this as XTypeElement
 
     fun asDeclaredType(): XDeclaredType {
diff --git a/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacAnnotationBox.kt b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacAnnotationBox.kt
new file mode 100644
index 0000000..06e1dda
--- /dev/null
+++ b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacAnnotationBox.kt
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2020 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 androidx.room.processing.javac
+
+import androidx.room.processing.XAnnotationBox
+import androidx.room.processing.XType
+import com.google.auto.common.AnnotationMirrors
+import java.lang.reflect.Proxy
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.util.SimpleAnnotationValueVisitor6
+
+internal interface JavacClassGetter {
+    fun getAsType(methodName: String): XType?
+    fun getAsTypeList(methodName: String): List<XType>
+    fun <T : Annotation> getAsAnnotationBox(methodName: String): XAnnotationBox<T>
+    fun <T : Annotation> getAsAnnotationBoxArray(methodName: String): Array<XAnnotationBox<T>>
+}
+
+/**
+ * Class that helps to read values from annotations. Simple types as string, int, lists can
+ * be read from [value]. If you need to read classes or another annotations from annotation use
+ * [getAsType], [getAsAnnotationBox] and [getAsAnnotationBoxArray] correspondingly.
+ */
+internal class JavacAnnotationBox<T : Annotation>(obj: Any) : XAnnotationBox<T> {
+    private val classGetter = obj as JavacClassGetter
+
+    @Suppress("UNCHECKED_CAST")
+    override val value: T = obj as T
+    override fun getAsType(methodName: String): XType? = classGetter.getAsType(methodName)
+
+    override fun getAsTypeList(methodName: String): List<XType> =
+        classGetter.getAsTypeList(methodName)
+
+    override fun <T : Annotation> getAsAnnotationBox(methodName: String): XAnnotationBox<T> {
+        return classGetter.getAsAnnotationBox(methodName)
+    }
+
+    override fun <T : Annotation> getAsAnnotationBoxArray(
+        methodName: String
+    ): Array<XAnnotationBox<T>> {
+        return classGetter.getAsAnnotationBoxArray(methodName)
+    }
+}
+
+internal fun <T : Annotation> AnnotationMirror.box(
+    env: JavacProcessingEnv,
+    cl: Class<T>
+): JavacAnnotationBox<T> {
+    if (!cl.isAnnotation) {
+        throw IllegalArgumentException("$cl is not annotation")
+    }
+    val map = cl.declaredMethods.associate { method ->
+        val value = AnnotationMirrors.getAnnotationValue(this, method.name)
+        val returnType = method.returnType
+        val defaultValue = method.defaultValue
+        val result: Any? = when {
+            returnType == Boolean::class.java -> value.getAsBoolean(defaultValue as Boolean)
+            returnType == String::class.java -> value.getAsString(defaultValue as String?)
+            returnType == Array<String>::class.java -> value.getAsStringList().toTypedArray()
+            returnType == emptyArray<Class<*>>()::class.java -> value.toListOfClassTypes(env)
+            returnType == IntArray::class.java -> value.getAsIntList().toIntArray()
+            returnType == Class::class.java -> {
+                try {
+                    value.toClassType(env)
+                } catch (notPresent: TypeNotPresentException) {
+                    null
+                }
+            }
+            returnType == Int::class.java -> value.getAsInt(defaultValue as Int?)
+            returnType.isAnnotation -> {
+                @Suppress("UNCHECKED_CAST")
+                AnnotationClassVisitor(env, returnType as Class<out Annotation>).visit(value)
+            }
+            returnType.isArray && returnType.componentType.isAnnotation -> {
+                @Suppress("UNCHECKED_CAST")
+                ListVisitor(env, returnType.componentType as Class<out Annotation>).visit(value)
+            }
+            returnType.isEnum -> {
+                @Suppress("UNCHECKED_CAST")
+                value.getAsEnum(returnType as Class<out Enum<*>>)
+            }
+            else -> throw UnsupportedOperationException("$returnType isn't supported")
+        }
+        method.name to result
+    }
+    return JavacAnnotationBox(
+        Proxy.newProxyInstance(
+            JavacClassGetter::class.java.classLoader,
+            arrayOf(cl, JavacClassGetter::class.java)
+        ) { _, method, args ->
+            when (method.name) {
+                JavacClassGetter::getAsType.name -> map[args[0]]
+                JavacClassGetter::getAsTypeList.name -> map[args[0]]
+                "getAsAnnotationBox" -> map[args[0]]
+                "getAsAnnotationBoxArray" -> map[args[0]]
+                else -> map[method.name]
+            }
+        })
+}
+
+@Suppress("DEPRECATION")
+private val ANNOTATION_VALUE_TO_INT_VISITOR = object : SimpleAnnotationValueVisitor6<Int?, Void>() {
+    override fun visitInt(i: Int, p: Void?): Int? {
+        return i
+    }
+}
+
+@Suppress("DEPRECATION")
+private val ANNOTATION_VALUE_TO_BOOLEAN_VISITOR = object :
+    SimpleAnnotationValueVisitor6<Boolean?, Void>() {
+    override fun visitBoolean(b: Boolean, p: Void?): Boolean? {
+        return b
+    }
+}
+
+@Suppress("DEPRECATION")
+private val ANNOTATION_VALUE_TO_STRING_VISITOR = object :
+    SimpleAnnotationValueVisitor6<String?, Void>() {
+    override fun visitString(s: String?, p: Void?): String? {
+        return s
+    }
+}
+
+@Suppress("DEPRECATION")
+private val ANNOTATION_VALUE_STRING_ARR_VISITOR = object :
+    SimpleAnnotationValueVisitor6<List<String>, Void>() {
+    override fun visitArray(vals: MutableList<out AnnotationValue>?, p: Void?): List<String> {
+        return vals?.mapNotNull {
+            ANNOTATION_VALUE_TO_STRING_VISITOR.visit(it)
+        } ?: emptyList()
+    }
+}
+
+@Suppress("DEPRECATION")
+private val ANNOTATION_VALUE_INT_ARR_VISITOR = object :
+    SimpleAnnotationValueVisitor6<List<Int>, Void>() {
+    override fun visitArray(vals: MutableList<out AnnotationValue>?, p: Void?): List<Int> {
+        return vals?.mapNotNull {
+            ANNOTATION_VALUE_TO_INT_VISITOR.visit(it)
+        } ?: emptyList()
+    }
+}
+
+private fun AnnotationValue.getAsInt(def: Int? = null): Int? {
+    return ANNOTATION_VALUE_TO_INT_VISITOR.visit(this) ?: def
+}
+
+private fun AnnotationValue.getAsIntList(): List<Int> {
+    return ANNOTATION_VALUE_INT_ARR_VISITOR.visit(this)
+}
+
+private fun AnnotationValue.getAsString(def: String? = null): String? {
+    return ANNOTATION_VALUE_TO_STRING_VISITOR.visit(this) ?: def
+}
+
+private fun AnnotationValue.getAsBoolean(def: Boolean): Boolean {
+    return ANNOTATION_VALUE_TO_BOOLEAN_VISITOR.visit(this) ?: def
+}
+
+private fun AnnotationValue.getAsStringList(): List<String> {
+    return ANNOTATION_VALUE_STRING_ARR_VISITOR.visit(this)
+}
+
+// code below taken from dagger2
+// compiler/src/main/java/dagger/internal/codegen/ConfigurationAnnotations.java
+@Suppress("DEPRECATION")
+private val TO_LIST_OF_TYPES = object :
+    SimpleAnnotationValueVisitor6<List<TypeMirror>, Void?>() {
+    override fun visitArray(values: MutableList<out AnnotationValue>?, p: Void?): List<TypeMirror> {
+        return values?.mapNotNull {
+            val tmp = TO_TYPE.visit(it)
+            tmp
+        } ?: emptyList()
+    }
+
+    override fun defaultAction(o: Any?, p: Void?): List<TypeMirror>? {
+        return emptyList()
+    }
+}
+
+@Suppress("DEPRECATION")
+private val TO_TYPE = object : SimpleAnnotationValueVisitor6<TypeMirror, Void>() {
+
+    override fun visitType(t: TypeMirror, p: Void?): TypeMirror {
+        return t
+    }
+
+    override fun defaultAction(o: Any?, p: Void?): TypeMirror {
+        throw TypeNotPresentException(o!!.toString(), null)
+    }
+}
+
+private fun AnnotationValue.toListOfClassTypes(env: JavacProcessingEnv): List<XType> {
+    return TO_LIST_OF_TYPES.visit(this).map {
+        env.wrap<JavacType>(it)
+    }
+}
+
+private fun AnnotationValue.toClassType(env: JavacProcessingEnv): XType? {
+    return TO_TYPE.visit(this)?.let {
+        env.wrap(it)
+    }
+}
+
+@Suppress("DEPRECATION")
+private class ListVisitor<T : Annotation>(
+    private val env: JavacProcessingEnv,
+    private val annotationClass: Class<T>
+) :
+    SimpleAnnotationValueVisitor6<Array<JavacAnnotationBox<T>>, Void?>() {
+    override fun visitArray(
+        values: MutableList<out AnnotationValue>?,
+        void: Void?
+    ): Array<JavacAnnotationBox<T>> {
+        val visitor = AnnotationClassVisitor(env, annotationClass)
+        return values?.mapNotNull { visitor.visit(it) }?.toTypedArray() ?: emptyArray()
+    }
+}
+
+@Suppress("DEPRECATION")
+private class AnnotationClassVisitor<T : Annotation>(
+    private val env: JavacProcessingEnv,
+    private val annotationClass: Class<T>
+) :
+    SimpleAnnotationValueVisitor6<JavacAnnotationBox<T>?, Void?>() {
+    override fun visitAnnotation(a: AnnotationMirror?, v: Void?) = a?.box(env, annotationClass)
+}
+
+@Suppress("UNCHECKED_CAST", "DEPRECATION")
+private fun <T : Enum<*>> AnnotationValue.getAsEnum(enumClass: Class<T>): T {
+    return object : SimpleAnnotationValueVisitor6<T, Void>() {
+        override fun visitEnumConstant(value: VariableElement?, p: Void?): T {
+            return enumClass.getDeclaredMethod("valueOf", String::class.java)
+                .invoke(null, value!!.simpleName.toString()) as T
+        }
+    }.visit(this)
+}
\ No newline at end of file
diff --git a/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacElement.kt b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacElement.kt
index a3e641d..17ca59a 100644
--- a/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacElement.kt
+++ b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacElement.kt
@@ -16,12 +16,14 @@
 
 package androidx.room.processing.javac
 
+import androidx.room.processing.XAnnotationBox
 import androidx.room.processing.XElement
 import androidx.room.processing.XEquality
 import com.google.auto.common.MoreElements
 import java.util.Locale
 import javax.lang.model.element.Element
 import javax.lang.model.element.Modifier
+import kotlin.reflect.KClass
 
 @Suppress("UnstableApiUsage")
 internal abstract class JavacElement(
@@ -74,6 +76,17 @@
         return element.modifiers.contains(Modifier.FINAL)
     }
 
+    override fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>? {
+        return MoreElements
+            .getAnnotationMirror(element, annotation.java)
+            .orNull()
+            ?.box(env, annotation.java)
+    }
+
+    override fun hasAnnotation(annotation: KClass<out Annotation>): Boolean {
+        return MoreElements.isAnnotationPresent(element, annotation.java)
+    }
+
     override fun toString(): String {
         return element.toString()
     }
@@ -89,4 +102,10 @@
     override fun kindName(): String {
         return element.kind.name.toLowerCase(Locale.US)
     }
+
+    override fun hasAnnotationInPackage(pkg: String): Boolean {
+        return element.annotationMirrors.any {
+            MoreElements.getPackage(it.annotationType.asElement()).toString() == pkg
+        }
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-xprocessing/src/test/java/androidx/room/processing/XAnnotationBoxTest.kt b/room/compiler-xprocessing/src/test/java/androidx/room/processing/XAnnotationBoxTest.kt
new file mode 100644
index 0000000..62d76f7
--- /dev/null
+++ b/room/compiler-xprocessing/src/test/java/androidx/room/processing/XAnnotationBoxTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 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 androidx.room.processing
+
+import androidx.room.processing.testcode.MainAnnotation
+import androidx.room.processing.testcode.OtherAnnotation
+import androidx.room.processing.util.Source
+import androidx.room.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class XAnnotationBoxTest {
+    @Test
+    fun readSimpleAnotationValue() {
+        val source = Source.java(
+            "foo.bar.Baz", """
+            package foo.bar;
+            @SuppressWarnings({"warning1", "warning 2"})
+            public class Baz {
+            }
+        """.trimIndent()
+        )
+        runProcessorTest(
+            sources = listOf(source)
+        ) {
+            val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
+            val annotationBox = element.toAnnotationBox(SuppressWarnings::class)
+            assertThat(annotationBox).isNotNull()
+            assertThat(
+                annotationBox!!.value.value
+            ).isEqualTo(
+                arrayOf("warning1", "warning 2")
+            )
+        }
+    }
+
+    @Test
+    fun typeReference() {
+        val mySource = Source.java(
+            "foo.bar.Baz", """
+            package foo.bar;
+            import androidx.room.processing.testcode.MainAnnotation;
+            import androidx.room.processing.testcode.OtherAnnotation;
+            @MainAnnotation(
+                typeList = {String.class, Integer.class},
+                singleType = Long.class,
+                intMethod = 3,
+                otherAnnotationArray = {
+                    @OtherAnnotation(
+                        value = "other list 1"
+                    ),
+                    @OtherAnnotation("other list 2"),
+                },
+                singleOtherAnnotation = @OtherAnnotation("other single")
+            )
+            public class Baz {
+            }
+        """.trimIndent()
+        )
+        val targetName = "foo.bar.Baz"
+        runProcessorTest(
+            listOf(mySource)
+        ) {
+            val element = it.processingEnv.requireTypeElement(targetName)
+            element.toAnnotationBox(MainAnnotation::class)!!.let { annotation ->
+                assertThat(
+                    annotation.getAsTypeList("typeList")
+                ).containsExactly(
+                    it.processingEnv.requireType(java.lang.String::class.java.canonicalName),
+                    it.processingEnv.requireType(java.lang.Integer::class.java.canonicalName)
+                )
+                assertThat(
+                    annotation.getAsType("singleType")
+                ).isEqualTo(
+                    it.processingEnv.requireType(java.lang.Long::class.java.canonicalName)
+                )
+
+                assertThat(annotation.value.intMethod).isEqualTo(3)
+                annotation.getAsAnnotationBox<OtherAnnotation>("singleOtherAnnotation")
+                    .let { other ->
+                        assertThat(other.value.value).isEqualTo("other single")
+                    }
+                annotation.getAsAnnotationBoxArray<OtherAnnotation>("otherAnnotationArray")
+                    .let { boxArray ->
+                        assertThat(boxArray).hasLength(2)
+                        assertThat(boxArray[0].value.value).isEqualTo("other list 1")
+                        assertThat(boxArray[1].value.value).isEqualTo("other list 2")
+                    }
+            }
+        }
+    }
+}
diff --git a/room/compiler-xprocessing/src/test/java/androidx/room/processing/XElementTest.kt b/room/compiler-xprocessing/src/test/java/androidx/room/processing/XElementTest.kt
index c81112a..ed318fb 100644
--- a/room/compiler-xprocessing/src/test/java/androidx/room/processing/XElementTest.kt
+++ b/room/compiler-xprocessing/src/test/java/androidx/room/processing/XElementTest.kt
@@ -60,6 +60,45 @@
     }
 
     @Test
+    fun annotationAvailability() {
+        val source = Source.java(
+            "foo.bar.Baz", """
+            package foo.bar;
+            import org.junit.*;
+            import org.junit.runner.*;
+            import org.junit.runners.*;
+            import androidx.room.processing.testcode.OtherAnnotation;
+
+            @RunWith(JUnit4.class)
+            class Baz {
+            }
+        """.trimIndent()
+        )
+        runProcessorTest(
+            listOf(source)
+        ) {
+            val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
+            assertThat(element.hasAnnotation(RunWith::class)).isTrue()
+            assertThat(element.hasAnnotation(Test::class)).isFalse()
+            assertThat(
+                element.hasAnnotationInPackage(
+                    "org.junit.runner"
+                )
+            ).isTrue()
+            assertThat(
+                element.hasAnnotationInPackage(
+                    "org.junit"
+                )
+            ).isFalse()
+            assertThat(
+                element.hasAnnotationInPackage(
+                    "foo.bar"
+                )
+            ).isFalse()
+        }
+    }
+
+    @Test
     fun nonType() {
         val source = Source.java(
             "foo.bar.Baz", """
diff --git a/room/compiler-xprocessing/src/test/java/androidx/room/processing/testcode/MainAnnotation.java b/room/compiler-xprocessing/src/test/java/androidx/room/processing/testcode/MainAnnotation.java
new file mode 100644
index 0000000..f587f46
--- /dev/null
+++ b/room/compiler-xprocessing/src/test/java/androidx/room/processing/testcode/MainAnnotation.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 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 androidx.room.processing.testcode;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * used in compilation tests
+ */
+@SuppressWarnings("unused")
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MainAnnotation {
+    Class<?>[] typeList();
+
+    Class<?> singleType();
+
+    int intMethod();
+
+    boolean boolMethodWithDefault() default true;
+
+    OtherAnnotation[] otherAnnotationArray() default {};
+
+    OtherAnnotation singleOtherAnnotation();
+}