Support new repeatable annotations in kotlin-reflect

- Unwrap Kotlin-repeatable annotations (with implicit container)
- Introduce `KAnnotatedElement.findAnnotations` to find instances of
  repeated annotations

 #KT-12794
This commit is contained in:
Alexander Udalov
2021-07-24 03:33:42 +02:00
parent 67128c022a
commit 0a6d010d1c
23 changed files with 448 additions and 3 deletions
@@ -7,7 +7,9 @@
package kotlin.reflect.full
import kotlin.reflect.*
import java.lang.reflect.Method
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
/**
* Returns an annotation of the given type on this element.
@@ -25,3 +27,75 @@ inline fun <reified T : Annotation> KAnnotatedElement.findAnnotation(): T? =
@WasExperimental(ExperimentalStdlibApi::class)
inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
findAnnotation<T>() != null
/**
* Returns all annotations of the given type on this element, including individually applied annotations
* as well as repeated annotations.
*
* In case the annotation is repeated, instances are extracted from the container annotation class similarly to how it happens
* in Java reflection ([java.lang.reflect.AnnotatedElement.getAnnotationsByType]). This is supported both for Kotlin-repeatable
* ([kotlin.annotation.Repeatable]) and Java-repeatable ([java.lang.annotation.Repeatable]) annotation classes.
*/
@SinceKotlin("1.5")
@ExperimentalStdlibApi
inline fun <reified T : Annotation> KAnnotatedElement.findAnnotations(): List<T> =
findAnnotations(T::class)
/**
* Returns all annotations of the given type on this element, including individually applied annotations
* as well as repeated annotations.
*
* In case the annotation is repeated, instances are extracted from the container annotation class similarly to how it happens
* in Java reflection ([java.lang.reflect.AnnotatedElement.getAnnotationsByType]). This is supported both for Kotlin-repeatable
* ([kotlin.annotation.Repeatable]) and Java-repeatable ([java.lang.annotation.Repeatable]) annotation classes.
*/
@Suppress("UNCHECKED_CAST")
@SinceKotlin("1.5")
@ExperimentalStdlibApi
fun <T : Annotation> KAnnotatedElement.findAnnotations(klass: KClass<T>): List<T> {
val filtered = annotations.filterIsInstance(klass.java)
if (filtered.isNotEmpty()) return filtered
val containerClass = Java8RepeatableContainerLoader.loadRepeatableContainer(klass.java)
if (containerClass != null) {
val container = annotations.firstOrNull { it.annotationClass.java == containerClass }
if (container != null) {
// A repeatable annotation container must have a method "value" returning the array of repeated annotations.
val valueMethod = container::class.java.getMethod("value")
return (valueMethod(container) as Array<T>).asList()
}
}
return emptyList()
}
@Suppress("UNCHECKED_CAST")
private object Java8RepeatableContainerLoader {
class Cache(val repeatableClass: Class<out Annotation>?, val valueMethod: Method?)
var cache: Cache? = null
private fun buildCache(): Cache {
val repeatableClass = try {
Class.forName("java.lang.annotation.Repeatable") as Class<out Annotation>
} catch (e: ClassNotFoundException) {
return Cache(null, null)
}
return Cache(repeatableClass, repeatableClass.getMethod("value"))
}
fun loadRepeatableContainer(klass: Class<out Annotation>): Class<out Annotation>? {
var cache = cache
if (cache == null) {
cache = buildCache()
this.cache = cache
}
val repeatableClass = cache.repeatableClass ?: return null
val repeatable = klass.getAnnotation(repeatableClass) ?: return null
val valueMethod = cache.valueMethod ?: return null
return valueMethod(repeatable) as Class<out Annotation>
}
}
@@ -30,6 +30,7 @@ import org.jetbrains.kotlin.descriptors.runtime.components.tryLoadClass
import org.jetbrains.kotlin.descriptors.runtime.structure.ReflectJavaAnnotation
import org.jetbrains.kotlin.descriptors.runtime.structure.ReflectJavaClass
import org.jetbrains.kotlin.descriptors.runtime.structure.safeClassLoader
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinarySourceElement
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.BinaryVersion
@@ -49,6 +50,7 @@ import org.jetbrains.kotlin.serialization.deserialization.MemberDeserializer
import java.lang.reflect.Type
import kotlin.jvm.internal.FunctionReference
import kotlin.jvm.internal.PropertyReference
import kotlin.jvm.internal.RepeatableContainer
import kotlin.reflect.KType
import kotlin.reflect.KVisibility
import kotlin.reflect.full.IllegalCallableAccessException
@@ -122,7 +124,22 @@ internal fun Annotated.computeAnnotations(): List<Annotation> =
is RuntimeSourceElementFactory.RuntimeSourceElement -> (source.javaElement as? ReflectJavaAnnotation)?.annotation
else -> it.toAnnotationInstance()
}
}
}.unwrapRepeatableAnnotations()
private fun List<Annotation>.unwrapRepeatableAnnotations(): List<Annotation> =
if (any { it.annotationClass.java.simpleName == JvmAbi.REPEATABLE_ANNOTATION_CONTAINER_NAME })
flatMap {
val klass = it.annotationClass.java
if (klass.simpleName == JvmAbi.REPEATABLE_ANNOTATION_CONTAINER_NAME &&
klass.getAnnotation(RepeatableContainer::class.java) != null
)
@Suppress("UNCHECKED_CAST")
(klass.getDeclaredMethod("value").invoke(it) as Array<out Annotation>).asList()
else
listOf(it)
}
else
this
private fun AnnotationDescriptor.toAnnotationInstance(): Annotation? {
@Suppress("UNCHECKED_CAST")