[GH] [Room][XProcessing] Get all annotations on element
## Proposed Changes
Adds `getAllAnnotations` to XAnnotated to support retrieving the complete list of annotations on an element without having to query them specifically by class. This allows us to read information on annotations without prior knowledge of what they are. To represent this information I created a new XAnnotation interface because XAnnotationBox requires the class to already be compiled - however, I do have an extension function to convert XAnnotation to XAnnotationBox when the class is available.
This includes support for repeatable annotations, and non primitive types for classes, enums, and annotations (as well as the list varieties).
I ran into a few KSP problems and linked KSP github issues where appropriate.
## Testing
Test: Added XAnnotationTest, which adopts the same tests from XAnnotationBoxTest
## Issues Fixed
Fixes: https://issuetracker.google.com/issues/188853151
This is an imported pull request from https://github.com/androidx/androidx/pull/183.
Resolves #183
Github-Pr-Head-Sha: d36e6cd062a9f6e63da8e67fb9f3bdab36804983
GitOrigin-RevId: 1f789d966ed79e2113b1adb5726e0d9015b72bcd
Change-Id: I846aa050824152bff2f24e90f42134b81d25cd74
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotation.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotation.kt
new file mode 100644
index 0000000..ab7350a
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotation.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021 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.compiler.processing
+
+import java.lang.annotation.Repeatable
+
+@PublishedApi
+internal interface InternalXAnnotation : XAnnotation {
+ fun <T : Annotation> asAnnotationBox(annotationClass: Class<T>): XAnnotationBox<T>
+}
+
+/**
+ * If this represents a repeatable annotation container this will return the repeated annotations
+ * nested inside it.
+ */
+internal fun XAnnotation.unwrapRepeatedAnnotationsFromContainer(): List<XAnnotation>? {
+ return try {
+ // The contract of a repeatable annotation requires that the container annotation have a single
+ // "default" method that returns an array typed with the repeatable annotation type.
+ val nestedAnnotations = getAsAnnotationList("value")
+
+ // Ideally we would read the value of the Repeatable annotation to get the container class
+ // type and check that it matches "this" type. However, there seems to be a KSP bug where
+ // the value of Repeatable is not present so the best we can do is check that all the nested
+ // members are annotated with repeatable.
+ // https://github.com/google/ksp/issues/358
+ val isRepeatable = nestedAnnotations.all {
+ // The java and kotlin versions of Repeatable are not interchangeable.
+ // https://github.com/google/ksp/issues/459 asks whether the built in type mapper
+ // should convert them, but it may not be possible because there are differences
+ // to how they work (eg different parameters).
+ it.type.typeElement?.hasAnnotation(Repeatable::class) == true ||
+ it.type.typeElement?.hasAnnotation(kotlin.annotation.Repeatable::class) == true
+ }
+
+ if (isRepeatable) nestedAnnotations else null
+ } catch (e: Throwable) {
+ // If the "value" type either doesn't exist or isn't an array of annotations then the
+ // above code will throw.
+ null
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
index de23361..f42dc03 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
@@ -37,6 +37,20 @@
): List<XAnnotationBox<T>>
/**
+ * Returns all annotations on this element represented as [XAnnotation].
+ *
+ * As opposed to other functions like [getAnnotations] this does not require you to have a
+ * reference to each annotation class, and thus it can represent annotations in the module
+ * sources being compiled. However, note that the returned [XAnnotation] cannot provide
+ * an instance of the annotation (like [XAnnotationBox.value] can) and instead all values
+ * must be accessed dynamically.
+ *
+ * The returned [XAnnotation]s can be converted to [XAnnotationBox] via
+ * [XAnnotation.asAnnotationBox] if the annotation class is on the class path.
+ */
+ fun getAllAnnotations(): List<XAnnotation>
+
+ /**
* Returns `true` if this element is annotated with the given [annotation].
*
* For repeated annotations declared in Java code, please use the repeated annotation type,
@@ -50,8 +64,8 @@
/**
* Returns `true` if this element has an annotation that is declared in the given package.
+ * Alternatively, all annotations can be accessed with [getAllAnnotations].
*/
- // a very sad method but helps avoid abstraction annotation
fun hasAnnotationWithPackage(pkg: String): Boolean
/**
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotation.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotation.kt
new file mode 100644
index 0000000..fcb81d0
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotation.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 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.compiler.processing
+
+/**
+ * This wraps annotations that may be declared in sources, and thus not representable with a
+ * compiled type. This is an equivalent to the Java AnnotationMirror API.
+ *
+ * Values in the annotation can be accessed via [annotationValues], the [XAnnotation.get] extension
+ * function, or any of the "getAs*" helper functions.
+ *
+ * In comparison, [XAnnotationBox] is used in situations where the annotation class is already
+ * compiled and can be referenced. This can be converted with [asAnnotationBox] if the annotation
+ * class is already compiled.
+ */
+interface XAnnotation {
+ /**
+ * The simple name of the annotation class.
+ */
+ val name: String
+
+ /**
+ * The fully qualified name of the annotation class.
+ * Accessing this forces the type to be resolved.
+ */
+ val qualifiedName: String
+
+ /**
+ * The [XType] representing the annotation class.
+ *
+ * Accessing this requires resolving the type, and is thus more expensive that just accessing
+ * [name].
+ */
+ val type: XType
+
+ /**
+ * All values declared in the annotation class.
+ */
+ val annotationValues: List<XAnnotationValue>
+
+ /**
+ * Returns the value of the given [methodName] as a type reference.
+ */
+ fun getAsType(methodName: String): XType = get(methodName)
+
+ /**
+ * Returns the value of the given [methodName] as a list of type references.
+ */
+ fun getAsTypeList(methodName: String): List<XType> = get(methodName)
+
+ /**
+ * Returns the value of the given [methodName] as another [XAnnotation].
+ */
+ fun getAsAnnotation(methodName: String): XAnnotation = get(methodName)
+
+ /**
+ * Returns the value of the given [methodName] as a list of [XAnnotation].
+ */
+ fun getAsAnnotationList(methodName: String): List<XAnnotation> = get(methodName)
+
+ /**
+ * Returns the value of the given [methodName] as a [XEnumEntry].
+ */
+ fun getAsEnum(methodName: String): XEnumEntry = get(methodName)
+
+ /**
+ * Returns the value of the given [methodName] as a list of [XEnumEntry].
+ */
+ fun getAsEnumList(methodName: String): List<XEnumEntry> = get(methodName)
+}
+
+/**
+ * Returns the value of the given [methodName], throwing an exception if the method is not
+ * found or if the given type [T] does not match the actual type.
+ *
+ * Note that non primitive types are wrapped by interfaces in order to allow them to be
+ * represented by the process:
+ * - "Class" types are represented with [XType]
+ * - Annotations are represented with [XAnnotation]
+ * - Enums are represented with [XEnumEntry]
+ *
+ * For convenience, wrapper functions are provided for these types, eg [XAnnotation.getAsType]
+ */
+inline fun <reified T> XAnnotation.get(methodName: String): T {
+ val argument = annotationValues.firstOrNull { it.name == methodName }
+ ?: error("No property named $methodName was found in annotation $name")
+
+ return argument.value as? T ?: error(
+ "Value of $methodName of type ${argument.value?.javaClass} " +
+ "cannot be cast to ${T::class.java}"
+ )
+}
+
+/**
+ * Get a representation of this [XAnnotation] as a [XAnnotationBox]. This is helpful for converting
+ * to [XAnnotationBox] after getting annotations with [XAnnotated.getAllAnnotations].
+ *
+ * Only possible if the annotation class is available (ie it is in the classpath and not in
+ * the compiled sources).
+ */
+inline fun <reified T : Annotation> XAnnotation.asAnnotationBox(): XAnnotationBox<T> {
+ return (this as InternalXAnnotation).asAnnotationBox(T::class.java)
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotationBox.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotationBox.kt
index 1e9282d..09844b0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotationBox.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotationBox.kt
@@ -19,7 +19,7 @@
/**
* 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
+ * It won't scale to a general purpose processing APIs where an equivalent of the AnnotationMirror
* API needs to be provided but works well for Room's case.
*/
interface XAnnotationBox<T> {
@@ -47,4 +47,4 @@
* 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/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotationValue.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotationValue.kt
new file mode 100644
index 0000000..c3c6d34
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotationValue.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 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.compiler.processing
+
+/**
+ * This wraps information about an argument in an annotation.
+ */
+interface XAnnotationValue {
+ /**
+ * The property name.
+ */
+ val name: String
+
+ /**
+ * The value set on the annotation property, or the default value if it was not explicitly set.
+ *
+ * Possible types are:
+ * - Primitives (Boolean, Byte, Int, Long, Float, Double)
+ * - String
+ * - XEnumEntry
+ * - XAnnotation
+ * - XType
+ * - List of any of the above
+ */
+ val value: Any?
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumEntry.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumEntry.kt
new file mode 100644
index 0000000..0006ad5
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumEntry.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 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.compiler.processing
+
+import kotlin.contracts.contract
+
+/**
+ * Represents a named entry within an enum class.
+ */
+interface XEnumEntry : XElement {
+ /**
+ * The name of this enum object.
+ */
+ val name: String
+
+ /**
+ * The parent enum type declaration that holds all entries for this enum type..
+ */
+ val enumTypeElement: XEnumTypeElement
+}
+
+fun XTypeElement.isEnumEntry(): Boolean {
+ contract {
+ returns(true) implies (this@isEnumEntry is XEnumEntry)
+ }
+ return this is XEnumEntry
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt
index 5d35646..5061d0a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt
@@ -22,7 +22,7 @@
* Type elements that represent Enum declarations.
*/
interface XEnumTypeElement : XTypeElement {
- val enumConstantNames: Set<String>
+ val entries: Set<XEnumEntry>
}
fun XTypeElement.isEnum(): Boolean {
@@ -30,4 +30,4 @@
returns(true) implies (this@isEnum is XEnumTypeElement)
}
return this is XEnumTypeElement
-}
\ No newline at end of file
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotation.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotation.kt
new file mode 100644
index 0000000..dc2775e
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotation.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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.compiler.processing.javac
+
+import androidx.room.compiler.processing.InternalXAnnotation
+import androidx.room.compiler.processing.XAnnotationBox
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XAnnotationValue
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.AnnotationMirror
+
+internal class JavacAnnotation(
+ val env: JavacProcessingEnv,
+ val mirror: AnnotationMirror
+) : InternalXAnnotation {
+
+ override val name: String
+ get() = mirror.annotationType.asElement().simpleName.toString()
+
+ override val qualifiedName: String
+ get() = MoreTypes.asTypeElement(mirror.annotationType).qualifiedName.toString()
+
+ override val type: XType by lazy {
+ JavacDeclaredType(env, mirror.annotationType, XNullability.NONNULL)
+ }
+
+ override val annotationValues: List<XAnnotationValue>
+ get() {
+ return AnnotationMirrors.getAnnotationValuesWithDefaults(mirror)
+ .map { (executableElement, annotationValue) ->
+ JavacAnnotationValue(env, executableElement, annotationValue)
+ }
+ }
+
+ override fun <T : Annotation> asAnnotationBox(annotationClass: Class<T>): XAnnotationBox<T> {
+ return mirror.box(env, annotationClass)
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationValue.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationValue.kt
new file mode 100644
index 0000000..77e1e6e
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationValue.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 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.compiler.processing.javac
+
+import androidx.room.compiler.processing.XEnumTypeElement
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XAnnotationValue
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.TypeMirror
+
+internal class JavacAnnotationValue(
+ val env: JavacProcessingEnv,
+ val element: ExecutableElement,
+ val annotationValue: AnnotationValue
+) : XAnnotationValue {
+ override val name: String
+ get() = element.simpleName.toString()
+
+ override val value: Any? by lazy { annotationValue.unwrap() }
+
+ private fun AnnotationValue.unwrap(): Any? {
+ val value = this.value
+ fun Any?.unwrapIfNeeded(): Any? {
+ return if (this is AnnotationValue) this.unwrap() else this
+ }
+ return when {
+ // The List implementation further wraps each value as a AnnotationValue.
+ // We don't expose arrays because we don't have a reified type to instantiate the array
+ // with, and using "Any" prevents the array from being cast to the correct
+ // type later on.
+ value is List<*> -> value.map { it.unwrapIfNeeded() }
+ value is TypeMirror -> env.wrap(value, kotlinType = null, XNullability.NONNULL)
+ value is AnnotationMirror -> JavacAnnotation(env, value)
+ // Enums are wrapped in a variable element with kind ENUM_CONSTANT
+ value is VariableElement -> {
+ when {
+ value.kind == ElementKind.ENUM_CONSTANT -> {
+ val enumTypeElement = MoreTypes.asTypeElement(value.asType())
+ JavacEnumEntry(
+ env,
+ value,
+ JavacTypeElement.create(env, enumTypeElement) as XEnumTypeElement
+ )
+ }
+ else -> error("Unexpected annotation value $value for argument $name")
+ }
+ }
+ else -> value
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index 0a70f22..494a356 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -17,9 +17,11 @@
package androidx.room.compiler.processing.javac
import androidx.room.compiler.processing.InternalXAnnotated
+import androidx.room.compiler.processing.XAnnotation
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XEquality
+import androidx.room.compiler.processing.unwrapRepeatedAnnotationsFromContainer
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreElements.isAnnotationPresent
import java.util.Locale
@@ -57,6 +59,13 @@
} ?: emptyList()
}
+ override fun getAllAnnotations(): List<XAnnotation> {
+ return element.annotationMirrors.map { mirror -> JavacAnnotation(env, mirror) }
+ .flatMap { annotation ->
+ annotation.unwrapRepeatedAnnotationsFromContainer() ?: listOf(annotation)
+ }
+ }
+
override fun hasAnnotation(
annotation: KClass<out Annotation>,
containerAnnotation: KClass<out Annotation>?
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
new file mode 100644
index 0000000..2cc8a63
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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.compiler.processing.javac
+
+import androidx.room.compiler.processing.XEnumEntry
+import androidx.room.compiler.processing.XEnumTypeElement
+import javax.lang.model.element.Element
+
+internal class JavacEnumEntry(
+ env: JavacProcessingEnv,
+ entryElement: Element,
+ override val enumTypeElement: XEnumTypeElement,
+) : JavacElement(env, entryElement), XEnumEntry {
+
+ override val name: String
+ get() = element.simpleName.toString()
+
+ override val equalityItems: Array<out Any?> by lazy {
+ arrayOf(name, enumTypeElement)
+ }
+
+ override val fallbackLocationText: String
+ get() = "$name enum entry in ${enumTypeElement.fallbackLocationText}"
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index fa5ad96..5c8d663 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -16,6 +16,7 @@
package androidx.room.compiler.processing.javac
+import androidx.room.compiler.processing.XEnumEntry
import androidx.room.compiler.processing.XEnumTypeElement
import androidx.room.compiler.processing.XFieldElement
import androidx.room.compiler.processing.XHasModifiers
@@ -179,11 +180,11 @@
check(element.kind == ElementKind.ENUM)
}
- override val enumConstantNames: Set<String> by lazy {
+ override val entries: Set<XEnumEntry> by lazy {
element.enclosedElements.filter {
it.kind == ElementKind.ENUM_CONSTANT
}.mapTo(mutableSetOf()) {
- it.simpleName.toString()
+ JavacEnumEntry(env, it, this)
}
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
index 708e243..2a150d9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
@@ -18,21 +18,30 @@
import androidx.room.compiler.processing.InternalXAnnotated
import androidx.room.compiler.processing.XAnnotationBox
+import androidx.room.compiler.processing.XAnnotation
+import androidx.room.compiler.processing.unwrapRepeatedAnnotationsFromContainer
+import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import kotlin.reflect.KClass
+@OptIn(KspExperimental::class)
internal sealed class KspAnnotated(
val env: KspProcessingEnv
) : InternalXAnnotated {
abstract fun annotations(): Sequence<KSAnnotation>
private fun <T : Annotation> findAnnotations(annotation: KClass<T>): Sequence<KSAnnotation> {
- return annotations().filter {
- val qName = it.annotationType.resolve().declaration.qualifiedName?.asString()
- qName == annotation.qualifiedName
- }
+ return annotations().filter { isSameAnnotationClass(it, annotation) }
+ }
+
+ override fun getAllAnnotations(): List<XAnnotation> {
+ return annotations().map { ksAnnotated ->
+ KspAnnotation(env, ksAnnotated)
+ }.flatMap { annotation ->
+ annotation.unwrapRepeatedAnnotationsFromContainer() ?: listOf(annotation)
+ }.toList()
}
override fun <T : Annotation> getAnnotations(
@@ -72,12 +81,20 @@
containerAnnotation: KClass<out Annotation>?
): Boolean {
return annotations().any {
- val qName = it.annotationType.resolve().declaration.qualifiedName?.asString()
- qName == annotation.qualifiedName ||
- (containerAnnotation != null && qName == containerAnnotation.qualifiedName)
+ isSameAnnotationClass(it, annotation) ||
+ (containerAnnotation != null && isSameAnnotationClass(it, containerAnnotation))
}
}
+ private fun isSameAnnotationClass(
+ ksAnnotation: KSAnnotation,
+ annotationClass: KClass<out Annotation>
+ ): Boolean {
+ val declaration = ksAnnotation.annotationType.resolve().declaration
+ val qualifiedName = declaration.qualifiedName?.asString() ?: return false
+ return qualifiedName == annotationClass.qualifiedName
+ }
+
private class KSAnnotatedDelegate(
env: KspProcessingEnv,
private val delegate: KSAnnotated,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotation.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotation.kt
new file mode 100644
index 0000000..122b1b7
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotation.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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.compiler.processing.ksp
+
+import androidx.room.compiler.processing.InternalXAnnotation
+import androidx.room.compiler.processing.XAnnotationBox
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XAnnotationValue
+import androidx.room.compiler.processing.isArray
+import com.google.devtools.ksp.getConstructors
+import com.google.devtools.ksp.symbol.KSAnnotation
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSType
+
+internal class KspAnnotation(
+ val env: KspProcessingEnv,
+ val ksAnnotated: KSAnnotation
+) : InternalXAnnotation {
+
+ val ksType: KSType by lazy { ksAnnotated.annotationType.resolve() }
+
+ override val name: String
+ get() = ksAnnotated.shortName.asString()
+
+ override val qualifiedName: String
+ get() = ksType.declaration.qualifiedName?.asString() ?: ""
+
+ override val type: XType by lazy {
+ env.wrap(ksType, allowPrimitives = true)
+ }
+
+ override val annotationValues: List<XAnnotationValue> by lazy {
+ ksAnnotated.arguments.map { arg ->
+ KspAnnotationValue(
+ env, arg,
+ isListType = {
+ (ksType.declaration as KSClassDeclaration).getConstructors()
+ .singleOrNull()
+ ?.parameters
+ ?.firstOrNull { it.name == arg.name }
+ ?.let { env.wrap(it.type).isArray() } == true
+ }
+ )
+ }
+ }
+
+ override fun <T : Annotation> asAnnotationBox(annotationClass: Class<T>): XAnnotationBox<T> {
+ return KspAnnotationBox(
+ env = env,
+ annotationClass = annotationClass,
+ annotation = ksAnnotated
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
new file mode 100644
index 0000000..71c0519
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 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.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XAnnotationValue
+import com.google.devtools.ksp.symbol.ClassKind
+import com.google.devtools.ksp.symbol.KSAnnotation
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSValueArgument
+
+internal class KspAnnotationValue(
+ val env: KspProcessingEnv,
+ val valueArgument: KSValueArgument,
+ val isListType: () -> Boolean,
+) : XAnnotationValue {
+
+ override val name: String
+ get() = valueArgument.name?.asString()
+ ?: error("Value argument $this does not have a name.")
+
+ override val value: Any? by lazy { valueArgument.unwrap() }
+
+ private fun KSValueArgument.unwrap(): Any? {
+ fun unwrap(value: Any?): Any? {
+ return when (value) {
+ is KSType -> {
+ val declaration = value.declaration
+ // Wrap enum entries in enum specific type elements
+ if (declaration is KSClassDeclaration &&
+ declaration.classKind == ClassKind.ENUM_ENTRY
+ ) {
+ KspEnumEntry.create(env, declaration)
+ } else {
+ // And otherwise represent class types as generic XType
+ env.wrap(value, allowPrimitives = true)
+ }
+ }
+ is KSAnnotation -> KspAnnotation(env, value)
+ // The List implementation further wraps each value as a AnnotationValue.
+ // We don't use arrays because we don't have a reified type to instantiate the array
+ // with, and using "Any" prevents the array from being cast to the correct
+ // type later on.
+ is List<*> -> value.map { unwrap(it) }
+ else -> value
+ }
+ }
+ return unwrap(value).let { result ->
+ // TODO: 5/24/21 KSP does not wrap a single item in a list, even though the
+ // return type should be Class<?>[] (only in sources).
+ // https://github.com/google/ksp/issues/172
+ // https://github.com/google/ksp/issues/214
+ if (result !is List<*> && isListType()) {
+ listOf(result)
+ } else {
+ result
+ }
+ }
+ }
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspEnumEntry.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspEnumEntry.kt
new file mode 100644
index 0000000..c668a02
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspEnumEntry.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XEnumEntry
+import androidx.room.compiler.processing.XEnumTypeElement
+import com.google.devtools.ksp.symbol.ClassKind
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+
+internal class KspEnumEntry(
+ env: KspProcessingEnv,
+ element: KSClassDeclaration,
+ override val enumTypeElement: XEnumTypeElement,
+) : KspTypeElement(env, element), XEnumEntry {
+
+ override val name: String
+ get() = declaration.simpleName.asString()
+
+ companion object {
+ fun create(
+ env: KspProcessingEnv,
+ declaration: KSClassDeclaration,
+ ): KspEnumEntry {
+ require(declaration.classKind == ClassKind.ENUM_ENTRY) {
+ "Expected declaration to be an enum entry but was ${declaration.classKind}"
+ }
+
+ return KspEnumEntry(
+ env,
+ declaration,
+ KspTypeElement.create(
+ env,
+ declaration
+ .requireEnclosingMemberContainer(env)
+ .declaration as KSClassDeclaration
+ ) as XEnumTypeElement
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 1d21c53..ff9fb3f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -18,6 +18,7 @@
import androidx.room.compiler.processing.XAnnotated
import androidx.room.compiler.processing.XConstructorElement
+import androidx.room.compiler.processing.XEnumEntry
import androidx.room.compiler.processing.XEnumTypeElement
import androidx.room.compiler.processing.XFieldElement
import androidx.room.compiler.processing.XHasModifiers
@@ -356,12 +357,13 @@
env: KspProcessingEnv,
declaration: KSClassDeclaration
) : KspTypeElement(env, declaration), XEnumTypeElement {
- override val enumConstantNames: Set<String> by lazy {
- declaration.declarations.filter {
- it is KSClassDeclaration && it.classKind == ClassKind.ENUM_ENTRY
- }.mapTo(mutableSetOf()) {
- it.simpleName.asString()
- }
+ override val entries: Set<XEnumEntry> by lazy {
+ declaration.declarations
+ .filterIsInstance<KSClassDeclaration>()
+ .filter { it.classKind == ClassKind.ENUM_ENTRY }
+ .mapTo(mutableSetOf()) {
+ KspEnumEntry(env, it, this)
+ }
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
index 06a1983..44e3068 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
@@ -81,7 +81,7 @@
mapping["java.util.List"] = "kotlin.collections.MutableList"
mapping["java.util.ListIterator"] = "kotlin.collections.ListIterator"
mapping["java.util.Map"] = "kotlin.collections.MutableMap"
- mapping["java.util.Map.Entry"] = "Map.kotlin.collections.MutableEntry"
+ mapping["java.util.Map.Entry"] = "kotlin.collections.MutableEntry"
}
fun swapWithKotlinType(javaType: String): String = mapping[javaType] ?: javaType
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
new file mode 100644
index 0000000..44f0e8b
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
@@ -0,0 +1,708 @@
+/*
+ * Copyright 2021 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.compiler.processing
+
+import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults
+import androidx.room.compiler.processing.testcode.JavaAnnotationWithPrimitiveArray
+import androidx.room.compiler.processing.testcode.JavaAnnotationWithTypeReferences
+import androidx.room.compiler.processing.testcode.JavaEnum
+import androidx.room.compiler.processing.testcode.MainAnnotation
+import androidx.room.compiler.processing.testcode.OtherAnnotation
+import androidx.room.compiler.processing.testcode.RepeatableJavaAnnotation
+import androidx.room.compiler.processing.testcode.TestSuppressWarnings
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.compileFiles
+import androidx.room.compiler.processing.util.getField
+import androidx.room.compiler.processing.util.getMethod
+import androidx.room.compiler.processing.util.getParameter
+import androidx.room.compiler.processing.util.getSystemClasspathFiles
+import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
+import androidx.room.compiler.processing.util.typeName
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.squareup.javapoet.ClassName
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class XAnnotationTest(
+ private val preCompiled: Boolean
+) {
+ private fun runTest(
+ sources: List<Source>,
+ handler: (XTestInvocation) -> Unit
+ ) {
+ if (preCompiled) {
+ val compiled = compileFiles(sources)
+ val hasKotlinSources = sources.any {
+ it is Source.KotlinSource
+ }
+ val kotlinSources = if (hasKotlinSources) {
+ listOf(
+ Source.kotlin("placeholder.kt", "class PlaceholderKotlin")
+ )
+ } else {
+ emptyList()
+ }
+ val newSources = kotlinSources + Source.java(
+ "PlaceholderJava",
+ "public class " +
+ "PlaceholderJava {}"
+ )
+ runProcessorTest(
+ sources = newSources,
+ handler = handler,
+ classpath = listOf(compiled) + getSystemClasspathFiles()
+ )
+ } else {
+ runProcessorTest(
+ sources = sources,
+ handler = handler
+ )
+ }
+ }
+
+ @Test
+ fun readsAnnotationsDeclaredInSources() {
+ val source = Source.kotlin(
+ "MyClass.kt",
+ """
+ annotation class MyAnnotation1(val bar: Int)
+ @MyAnnotation1(bar = 1)
+ class MyClass
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(source),
+ ) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("MyClass")
+
+ val allAnnotations = element.getAllAnnotations().filterNot {
+ // Drop metadata annotation in kapt
+ it.name == "Metadata"
+ }
+ assertThat(allAnnotations).hasSize(1)
+
+ val annotation1 = allAnnotations[0]
+ assertThat(annotation1.name)
+ .isEqualTo("MyAnnotation1")
+ assertThat(annotation1.qualifiedName)
+ .isEqualTo("MyAnnotation1")
+ assertThat(annotation1.type.typeElement)
+ .isEqualTo(invocation.processingEnv.requireTypeElement("MyAnnotation1"))
+ assertThat(annotation1.get<Int>("bar"))
+ .isEqualTo(1)
+ assertThat(annotation1.annotationValues).hasSize(1)
+ assertThat(annotation1.annotationValues.first().name).isEqualTo("bar")
+ assertThat(annotation1.annotationValues.first().value).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun annotationsInClassPathCanBeBoxed() {
+ val source = Source.kotlin(
+ "MyClass.kt",
+ """
+ import androidx.room.compiler.processing.testcode.TestSuppressWarnings
+ @TestSuppressWarnings("a", "b")
+ class MyClass
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(source)
+ ) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("MyClass")
+ val annotation = element.requireAnnotation<TestSuppressWarnings>()
+ assertThat(annotation.name)
+ .isEqualTo(TestSuppressWarnings::class.simpleName)
+ assertThat(annotation.qualifiedName)
+ .isEqualTo(TestSuppressWarnings::class.qualifiedName)
+ assertThat(annotation.type.typeElement)
+ .isEqualTo(invocation.processingEnv.requireTypeElement(TestSuppressWarnings::class))
+ assertThat(
+ annotation.asAnnotationBox<TestSuppressWarnings>().value.value
+ ).isEqualTo(arrayOf("a", "b"))
+ }
+ }
+
+ @Test
+ fun readSimpleAnnotationValue() {
+ val source = Source.java(
+ "foo.bar.Baz",
+ """
+ package foo.bar;
+ import androidx.room.compiler.processing.testcode.TestSuppressWarnings;
+ @TestSuppressWarnings({"warning1", "warning 2"})
+ public class Baz {
+ }
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(source)
+ ) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
+ val annotation = element.requireAnnotation<TestSuppressWarnings>()
+
+ val argument = annotation.annotationValues.single()
+ assertThat(argument.name).isEqualTo("value")
+ assertThat(
+ argument.value
+ ).isEqualTo(
+ listOf("warning1", "warning 2")
+ )
+ }
+ }
+
+ @Test
+ fun typeReference_javac() {
+ val mySource = Source.java(
+ "foo.bar.Baz",
+ """
+ package foo.bar;
+ import androidx.room.compiler.processing.testcode.MainAnnotation;
+ import androidx.room.compiler.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()
+ )
+ runProcessorTestWithoutKsp(
+ listOf(mySource)
+ ) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
+ val annotation = element.requireAnnotation<MainAnnotation>()
+
+ assertThat(
+ annotation.get<List<XType>>("typeList")
+ ).containsExactly(
+ invocation.processingEnv.requireType(java.lang.String::class),
+ invocation.processingEnv.requireType(Integer::class)
+ )
+ assertThat(
+ annotation.get<XType>("singleType")
+ ).isEqualTo(
+ invocation.processingEnv.requireType(java.lang.Long::class)
+ )
+
+ assertThat(annotation.get<Int>("intMethod")).isEqualTo(3)
+ annotation.get<XAnnotation>("singleOtherAnnotation")
+ .let { other ->
+ assertThat(other.name).isEqualTo(OtherAnnotation::class.simpleName)
+ assertThat(other.qualifiedName).isEqualTo(OtherAnnotation::class.qualifiedName)
+ assertThat(other.get<String>("value"))
+ .isEqualTo("other single")
+ }
+ annotation.get<List<XAnnotation>>("otherAnnotationArray")
+ .let { boxArray ->
+ assertThat(boxArray).hasSize(2)
+ assertThat(boxArray[0].get<String>("value")).isEqualTo("other list 1")
+ assertThat(boxArray[1].get<String>("value")).isEqualTo("other list 2")
+ }
+ }
+ }
+
+ @Test
+ fun readSimpleAnnotationValue_kotlin() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.compiler.processing.testcode.TestSuppressWarnings
+ @TestSuppressWarnings("warning1", "warning 2")
+ class Subject {
+ }
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(source)
+ ) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("Subject")
+ val annotation = element.requireAnnotation<TestSuppressWarnings>()
+
+ assertThat(annotation).isNotNull()
+ assertThat(
+ annotation.get<List<String>>("value")
+ ).isEqualTo(
+ listOf("warning1", "warning 2")
+ )
+ }
+ }
+
+ @Test
+ fun typeReference_kotlin() {
+ val mySource = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.compiler.processing.testcode.MainAnnotation
+ import androidx.room.compiler.processing.testcode.OtherAnnotation
+
+ @MainAnnotation(
+ typeList = [String::class, Int::class],
+ singleType = Long::class,
+ intMethod = 3,
+ otherAnnotationArray = [
+ OtherAnnotation(
+ value = "other list 1"
+ ),
+ OtherAnnotation(
+ value = "other list 2"
+ )
+ ],
+ singleOtherAnnotation = OtherAnnotation("other single")
+ )
+ public class Subject {
+ }
+ """.trimIndent()
+ )
+ runTest(
+ listOf(mySource)
+ ) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("Subject")
+ val annotation = element.requireAnnotation<MainAnnotation>()
+
+ assertThat(
+ annotation.get<List<XType>>("typeList").map {
+ it.typeName
+ }
+ ).containsExactly(
+ String::class.typeName(),
+ Int::class.typeName()
+ )
+ assertThat(
+ annotation.get<XType>("singleType")
+ ).isEqualTo(
+ invocation.processingEnv.requireType(Long::class.typeName())
+ )
+
+ assertThat(annotation.get<Int>("intMethod")).isEqualTo(3)
+ annotation.get<XAnnotation>("singleOtherAnnotation")
+ .let { other ->
+ assertThat(other.name).isEqualTo(OtherAnnotation::class.simpleName)
+ assertThat(other.qualifiedName).isEqualTo(OtherAnnotation::class.qualifiedName)
+ assertThat(other.get<String>("value"))
+ .isEqualTo("other single")
+ }
+ annotation.get<List<XAnnotation>>("otherAnnotationArray")
+ .let { boxArray ->
+ assertThat(boxArray).hasSize(2)
+ assertThat(boxArray[0].get<String>("value")).isEqualTo("other list 1")
+ assertThat(boxArray[1].get<String>("value")).isEqualTo("other list 2")
+ }
+ }
+ }
+
+ @Test
+ fun typeReferenceArray_singleItemInJava() {
+ val src = Source.java(
+ "Subject",
+ """
+ import androidx.room.compiler.processing.testcode.JavaAnnotationWithTypeReferences;
+ @JavaAnnotationWithTypeReferences(String.class)
+ class Subject {
+ }
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src)
+ ) { invocation ->
+ if (!invocation.isKsp) return@runTest
+ val subject = invocation.processingEnv.requireTypeElement("Subject")
+ val annotation = subject.requireAnnotation<JavaAnnotationWithTypeReferences>()
+ val annotationValue = annotation.get<List<XType>>("value").single()
+ assertThat(annotationValue.typeName).isEqualTo(
+ ClassName.get(String::class.java)
+ )
+ }
+ }
+
+ @Test
+ fun propertyAnnotations() {
+ val src = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.compiler.processing.testcode.OtherAnnotation
+ import androidx.room.compiler.processing.testcode.TestSuppressWarnings
+ class Subject {
+ @TestSuppressWarnings("onProp1")
+ var prop1:Int = TODO()
+
+ @get:TestSuppressWarnings("onGetter2")
+ @set:TestSuppressWarnings("onSetter2")
+ @field:TestSuppressWarnings("onField2")
+ @setparam:TestSuppressWarnings("onSetterParam2")
+ var prop2:Int = TODO()
+
+ @get:TestSuppressWarnings("onGetter3")
+ @set:TestSuppressWarnings("onSetter3")
+ @setparam:TestSuppressWarnings("onSetterParam3")
+ var prop3:Int
+ @OtherAnnotation("_onGetter3")
+ get() = 3
+
+ @OtherAnnotation("_onSetter3")
+ set(@OtherAnnotation("_onSetterParam3") value) = Unit
+ }
+ """.trimIndent()
+ )
+ runTest(sources = listOf(src)) { invocation ->
+ val subject = invocation.processingEnv.requireTypeElement("Subject")
+
+ subject.getField("prop1").assertHasSuppressWithValue("onProp1")
+ subject.getMethod("getProp1").assertDoesNotHaveAnnotation()
+ subject.getMethod("setProp1").assertDoesNotHaveAnnotation()
+ subject.getMethod("setProp1").parameters.first().assertDoesNotHaveAnnotation()
+
+ subject.getField("prop2").assertHasSuppressWithValue("onField2")
+ subject.getMethod("getProp2").assertHasSuppressWithValue("onGetter2")
+ subject.getMethod("setProp2").assertHasSuppressWithValue("onSetter2")
+ subject.getMethod("setProp2").parameters.first().assertHasSuppressWithValue(
+ "onSetterParam2"
+ )
+
+ subject.getMethod("getProp3").assertHasSuppressWithValue("onGetter3")
+ subject.getMethod("setProp3").assertHasSuppressWithValue("onSetter3")
+ subject.getMethod("setProp3").parameters.first().assertHasSuppressWithValue(
+ "onSetterParam3"
+ )
+
+ assertThat(
+ subject.getMethod("getProp3").getOtherAnnotationValue()
+ ).isEqualTo("_onGetter3")
+ assertThat(
+ subject.getMethod("setProp3").getOtherAnnotationValue()
+ ).isEqualTo("_onSetter3")
+ val otherAnnotationValue =
+ subject.getMethod("setProp3").parameters.first().getOtherAnnotationValue()
+ assertThat(
+ otherAnnotationValue
+ ).isEqualTo("_onSetterParam3")
+ }
+ }
+
+ @Test
+ fun methodAnnotations() {
+ val src = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.compiler.processing.testcode.OtherAnnotation
+ import androidx.room.compiler.processing.testcode.TestSuppressWarnings
+ class Subject {
+ fun noAnnotations(x:Int): Unit = TODO()
+ @TestSuppressWarnings("onMethod")
+ fun methodAnnotation(
+ @TestSuppressWarnings("onParam") annotated:Int,
+ notAnnotated:Int
+ ): Unit = TODO()
+ }
+ """.trimIndent()
+ )
+ runTest(sources = listOf(src)) { invocation ->
+ val subject = invocation.processingEnv.requireTypeElement("Subject")
+ subject.getMethod("noAnnotations").let { method ->
+ method.assertDoesNotHaveAnnotation()
+ method.getParameter("x").assertDoesNotHaveAnnotation()
+ }
+ subject.getMethod("methodAnnotation").let { method ->
+ method.assertHasSuppressWithValue("onMethod")
+ method.getParameter("annotated").assertHasSuppressWithValue("onParam")
+ method.getParameter("notAnnotated").assertDoesNotHaveAnnotation()
+ }
+ }
+ }
+
+ @Test
+ fun constructorParameterAnnotations() {
+ val src = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.compiler.processing.testcode.TestSuppressWarnings
+ @TestSuppressWarnings("onClass")
+ data class Subject(
+ @field:TestSuppressWarnings("onField")
+ @param:TestSuppressWarnings("onConstructorParam")
+ @get:TestSuppressWarnings("onGetter")
+ @set:TestSuppressWarnings("onSetter")
+ var x:Int
+ )
+ """.trimIndent()
+ )
+ runTest(sources = listOf(src)) { invocation ->
+ val subject = invocation.processingEnv.requireTypeElement("Subject")
+ subject.assertHasSuppressWithValue("onClass")
+ assertThat(subject.getConstructors()).hasSize(1)
+ val constructor = subject.getConstructors().single()
+ constructor.getParameter("x").assertHasSuppressWithValue("onConstructorParam")
+ subject.getMethod("getX").assertHasSuppressWithValue("onGetter")
+ subject.getMethod("setX").assertHasSuppressWithValue("onSetter")
+ subject.getField("x").assertHasSuppressWithValue("onField")
+ }
+ }
+
+ @Test
+ fun defaultValues() {
+ val kotlinSrc = Source.kotlin(
+ "KotlinClass.kt",
+ """
+ import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults
+ @JavaAnnotationWithDefaults
+ class KotlinClass
+ """.trimIndent()
+ )
+ val javaSrc = Source.java(
+ "JavaClass.java",
+ """
+ import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults;
+ @JavaAnnotationWithDefaults
+ class JavaClass {}
+ """.trimIndent()
+ )
+ runTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
+ listOf("KotlinClass", "JavaClass")
+ .map {
+ invocation.processingEnv.requireTypeElement(it)
+ }.forEach { typeElement ->
+ val annotation = typeElement.requireAnnotation<JavaAnnotationWithDefaults>()
+
+ assertThat(annotation.get<Int>("intVal"))
+ .isEqualTo(3)
+ assertThat(annotation.get<List<Int>>("intArrayVal"))
+ .isEqualTo(listOf(1, 3, 5))
+ assertThat(annotation.get<List<String>>("stringArrayVal"))
+ .isEqualTo(listOf("x", "y"))
+ assertThat(annotation.get<String>("stringVal"))
+ .isEqualTo("foo")
+ assertThat(
+ annotation.getAsType("typeVal").rawType.typeName
+ ).isEqualTo(
+ ClassName.get(HashMap::class.java)
+ )
+ assertThat(
+ annotation.getAsTypeList("typeArrayVal").map {
+ it.rawType.typeName
+ }
+ ).isEqualTo(
+ listOf(ClassName.get(LinkedHashMap::class.java))
+ )
+
+ val enumValueEntry = annotation.getAsEnum("enumVal")
+ assertThat(enumValueEntry.name).isEqualTo("DEFAULT")
+ val javaEnumType = invocation.processingEnv.requireTypeElement(JavaEnum::class)
+ assertThat(enumValueEntry.enumTypeElement)
+ .isEqualTo(javaEnumType)
+
+ val enumList = annotation.getAsEnumList("enumArrayVal")
+ assertThat(enumList[0].name).isEqualTo("VAL1")
+ assertThat(enumList[1].name).isEqualTo("VAL2")
+ assertThat(enumList[0].enumTypeElement).isEqualTo(javaEnumType)
+ assertThat(enumList[1].enumTypeElement).isEqualTo(javaEnumType)
+
+ // TODO: KSP mistakenly sees null for the value in a default annotation in
+ // sources. https://github.com/google/ksp/issues/53
+ if (!invocation.isKsp && !preCompiled) {
+
+ annotation.getAsAnnotation("otherAnnotationVal")
+ .let { other ->
+ assertThat(other.name).isEqualTo("OtherAnnotation")
+ assertThat(other.get<String>("value"))
+ .isEqualTo("def")
+ }
+
+ annotation.getAsAnnotationList("otherAnnotationArrayVal")
+ .forEach { other ->
+ assertThat(other.name).isEqualTo("OtherAnnotation")
+ assertThat(other.get<String>("value"))
+ .isEqualTo("v1")
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun javaPrimitiveArray() {
+ // TODO: expand this test for other primitive types: 179081610
+ val javaSrc = Source.java(
+ "JavaSubject.java",
+ """
+ import androidx.room.compiler.processing.testcode.*;
+ class JavaSubject {
+ @JavaAnnotationWithPrimitiveArray(intArray = {1, 2, 3})
+ Object annotated1;
+ }
+ """.trimIndent()
+ )
+ val kotlinSrc = Source.kotlin(
+ "KotlinSubject.kt",
+ """
+ import androidx.room.compiler.processing.testcode.*;
+ class KotlinSubject {
+ @JavaAnnotationWithPrimitiveArray(intArray = [1, 2, 3])
+ val annotated1:Any = TODO()
+ }
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(javaSrc, kotlinSrc)
+ ) { invocation ->
+ listOf("JavaSubject", "KotlinSubject").map {
+ invocation.processingEnv.requireTypeElement(it)
+ }.forEach { subject ->
+ val annotation = subject.getField("annotated1")
+ .requireAnnotation<JavaAnnotationWithPrimitiveArray>()
+ assertThat(
+ annotation.get<List<Int>>("intArray")
+ ).isEqualTo(
+ listOf(1, 2, 3)
+ )
+ }
+ }
+ }
+
+ @Test
+ fun javaRepeatableAnnotation() {
+ val javaSrc = Source.java(
+ "JavaSubject",
+ """
+ import ${RepeatableJavaAnnotation::class.qualifiedName};
+ @RepeatableJavaAnnotation("x")
+ @RepeatableJavaAnnotation("y")
+ @RepeatableJavaAnnotation("z")
+ public class JavaSubject {}
+ """.trimIndent()
+ )
+ val kotlinSrc = Source.kotlin(
+ "KotlinSubject.kt",
+ """
+ import ${RepeatableJavaAnnotation::class.qualifiedName}
+ // TODO update when https://youtrack.jetbrains.com/issue/KT-12794 is fixed.
+ // right now, kotlin does not support repeatable annotations.
+ @RepeatableJavaAnnotation.List(
+ RepeatableJavaAnnotation("x"),
+ RepeatableJavaAnnotation("y"),
+ RepeatableJavaAnnotation("z")
+ )
+ public class KotlinSubject
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(javaSrc, kotlinSrc)
+ ) { invocation ->
+ listOf("JavaSubject", "KotlinSubject")
+ .map(invocation.processingEnv::requireTypeElement)
+ .forEach { subject ->
+ val annotations = subject.getAllAnnotations().filter {
+ it.name == "RepeatableJavaAnnotation"
+ }
+ val values = annotations.map { it.get<String>("value") }
+ assertWithMessage(subject.qualifiedName)
+ .that(values)
+ .containsExactly("x", "y", "z")
+ }
+ }
+ }
+
+ @Test
+ fun javaRepeatableAnnotation_notRepeated() {
+ val javaSrc = Source.java(
+ "JavaSubject",
+ """
+ import ${RepeatableJavaAnnotation::class.qualifiedName};
+ @RepeatableJavaAnnotation("x")
+ public class JavaSubject {}
+ """.trimIndent()
+ )
+ val kotlinSrc = Source.kotlin(
+ "KotlinSubject.kt",
+ """
+ import ${RepeatableJavaAnnotation::class.qualifiedName}
+ @RepeatableJavaAnnotation("x")
+ public class KotlinSubject
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(javaSrc, kotlinSrc)
+ ) { invocation ->
+ listOf("JavaSubject", "KotlinSubject")
+ .map(invocation.processingEnv::requireTypeElement)
+ .forEach { subject ->
+ val annotations = subject.getAllAnnotations().filter {
+ it.name == "RepeatableJavaAnnotation"
+ }
+ val values = annotations.map { it.get<String>("value") }
+ assertWithMessage(subject.qualifiedName)
+ .that(values)
+ .containsExactly("x")
+ }
+ }
+ }
+
+ // helper function to read what we need
+ private fun XAnnotated.getSuppressValues(): List<String>? {
+ return this.findAnnotation<TestSuppressWarnings>()
+ ?.get<List<String>>("value")
+ }
+
+ private inline fun <reified T : Annotation> XAnnotated.requireAnnotation(): XAnnotation {
+ return findAnnotation<T>() ?: error("Annotation ${T::class} not found on $this")
+ }
+
+ private inline fun <reified T : Annotation> XAnnotated.findAnnotation(): XAnnotation? {
+ return getAllAnnotations().singleOrNull { it.name == T::class.simpleName }
+ }
+
+ private fun XAnnotated.assertHasSuppressWithValue(vararg expected: String) {
+ assertWithMessage("has suppress annotation $this")
+ .that(this.findAnnotation<TestSuppressWarnings>())
+ .isNotNull()
+ assertWithMessage("$this")
+ .that(getSuppressValues())
+ .isEqualTo(expected.toList())
+ }
+
+ private fun XAnnotated.assertDoesNotHaveAnnotation() {
+ assertWithMessage("$this")
+ .that(this.findAnnotation<TestSuppressWarnings>())
+ .isNull()
+ assertWithMessage("$this")
+ .that(this.getSuppressValues())
+ .isNull()
+ }
+
+ private fun XAnnotated.getOtherAnnotationValue(): String? {
+ return this.findAnnotation<OtherAnnotation>()
+ ?.get<String>("value")
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "preCompiled_{0}")
+ fun params() = arrayOf(false, true)
+ }
+}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index e6fc8e1..ff621a7 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -962,7 +962,7 @@
containsNoneOf("VAL1", "VAL2")
}
assertWithMessage("$qName can return enum constants")
- .that((typeElement as XEnumTypeElement).enumConstantNames)
+ .that((typeElement as XEnumTypeElement).entries.map { it.name })
.containsExactly("VAL1", "VAL2")
assertWithMessage("$qName does not report enum constants in fields")
.that(typeElement.getAllFieldNames())
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
index 879019b..74cdaf9 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -90,7 +90,7 @@
beginControlFlow("if ($N == null)", param)
addStatement("return null")
nextControlFlow("switch ($N)", param)
- enumTypeElement.enumConstantNames.forEach { enumConstantName ->
+ enumTypeElement.entries.map { it.name }.forEach { enumConstantName ->
addStatement("case $L: return $S", enumConstantName, enumConstantName)
}
addStatement(
@@ -127,7 +127,7 @@
beginControlFlow("if ($N == null)", param)
addStatement("return null")
nextControlFlow("switch ($N)", param)
- enumTypeElement.enumConstantNames.forEach { enumConstantName ->
+ enumTypeElement.entries.map { it.name }.forEach { enumConstantName ->
addStatement(
"case $S: return $T.$L",
enumConstantName, out.typeName, enumConstantName