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();
+}