Support for multiparam dependency annotations (with reservations)

Due to missing feature (#KT-13106) it works either with single argument
or with all named arguments.
This commit is contained in:
Ilya Chernikov
2016-07-14 15:21:17 +02:00
parent 5900cad8b3
commit 210a37e08c
4 changed files with 99 additions and 58 deletions
@@ -22,21 +22,62 @@ import org.jetbrains.kotlin.psi.KtUserType
import org.jetbrains.kotlin.resolve.BindingTraceContext
import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator
import org.jetbrains.kotlin.types.TypeUtils
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import kotlin.reflect.KClass
import kotlin.reflect.primaryConstructor
internal class KtAnnotationWrapper(val psi: KtAnnotationEntry) {
val name: String
get() = (psi.typeReference?.typeElement as? KtUserType)?.referencedName.orAnonymous()
internal val KtAnnotationEntry.typeName: String get() = (typeReference?.typeElement as? KtUserType)?.referencedName.orAnonymous()
val valueArguments: List<Pair<String?, Any?>> by lazy {
psi.valueArguments.map {
internal fun String?.orAnonymous(kind: String = ""): String =
this ?: "<anonymous" + (if (kind.isNotBlank()) " $kind" else "") + ">"
internal class KtAnnotationWrapper(val psi: KtAnnotationEntry, val targetClass: KClass<out Annotation>) {
val name: String get() = psi.typeName
val valueArguments: Map<String, Any?> by lazy {
var namedStarted = false
val res = hashMapOf<String, Any?>()
// TODO: annotation constructors unsupported yet in kotlin reflection, test and correct when they will be ready
val targetAnnParams = targetClass.primaryConstructor?.parameters
psi.valueArguments.mapIndexed { i, arg ->
val evaluator = ConstantExpressionEvaluator(DefaultBuiltIns.Instance)
val trace = BindingTraceContext()
val result = evaluator.evaluateToConstantValue(it.getArgumentExpression()!!, trace, TypeUtils.NO_EXPECTED_TYPE)
it.getArgumentName()?.asName.toString() to result?.value
val result = evaluator.evaluateToConstantValue(arg.getArgumentExpression()!!, trace, TypeUtils.NO_EXPECTED_TYPE)
// TODO: consider inspecting `trace` to find diagnostics reported during the computation (such as division by zero, integer overflow, invalid annotation parameters etc.)
val argName = arg.getArgumentName()?.asName?.toString()
// TODO: consider reusing arguments mapping logic from compiler code
val paramName = when {
argName == null && !namedStarted && targetAnnParams == null -> "$" // TODO: using invalid name here. Drop when annotation constructors will be accessible (se above)
argName == null && !namedStarted -> targetAnnParams?.get(i)?.name ?: throw IllegalArgumentException("Unnamed argument for $name at $i")
argName == null && namedStarted -> throw IllegalArgumentException("Invalid argument sequence for $name at arg $i")
targetAnnParams != null && targetAnnParams.none { it.name == argName } ->
throw IllegalArgumentException("Unknown argument $argName for $name")
else -> {
namedStarted = true
argName!!
}
}
res.put(paramName, result?.value)
}
res
}
internal class AnnProxyInvocationHandler<out K: KClass<out Any>>(val targetAnnClass: K, val annParams: Map<String, Any?>) : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, params: Array<out Any>?): Any? = method?.let {
// TODO: the functionality with checking annParams size is here only to workaround missing access to constructors in annotations. Drop as soon as possible (see above)
annParams[it.name] ?: if (annParams.size == 1) annParams.values.firstOrNull() else null
}
}
internal fun String?.orAnonymous(kind: String = ""): String =
this ?: "<anonymous" + (if (kind.isNotBlank()) " $kind" else "") + ">"
fun getProxy(classLoader: ClassLoader): Annotation =
try {
Proxy.newProxyInstance(classLoader, arrayOf(targetClass.java), AnnProxyInvocationHandler(targetClass, valueArguments)) as Annotation
}
catch (ex: Exception) {
InvalidScriptResolverAnnotation(name, valueArguments, ex)
}
}
class InvalidScriptResolverAnnotation(val name: String, val annParams: Map<String, Any?>, val error: Exception? = null) : Annotation
@@ -30,10 +30,6 @@ import org.jetbrains.kotlin.psi.KtScript
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.Future
import kotlin.reflect.*
const val DEFAULT_SCRIPT_FILE_PATTERN = "*.\\.kts"
@@ -125,17 +121,15 @@ data class KotlinScriptDefinitionFromTemplate(val template: KClass<out Any>,
private val definitionData by lazy {
val defAnn = template.annotations.firstIsInstanceOrNull<ScriptTemplateDefinition>()
val obsoleteResolverAnn = template.annotations.firstIsInstanceOrNull<ScriptDependencyResolver>()
val (resolverClass, resolverObject) =
when {
resolver != null -> resolver.javaClass.kotlin to resolver
defAnn != null -> defAnn.resolver to defAnn.resolver.primaryConstructor?.call()
obsoleteResolverAnn != null -> ObsoleteResolverProxy::class to ObsoleteResolverProxy(obsoleteResolverAnn)
else -> BasicScriptDependenciesResolver::class to BasicScriptDependenciesResolver()
}
val filePattern = defAnn?.scriptFilePattern ?:
template.annotations.firstIsInstanceOrNull<ScriptFilePattern>()?.pattern ?:
DEFAULT_SCRIPT_FILE_PATTERN
ScriptTemplateDefinitionData(resolverClass, resolverObject, filePattern)
when {
resolver != null -> ScriptTemplateDefinitionData(resolver.javaClass.kotlin, resolver, filePattern)
defAnn != null -> ScriptTemplateDefinitionData(defAnn.resolver, defAnn.resolver.primaryConstructor?.call(), filePattern)
obsoleteResolverAnn != null -> ScriptTemplateDefinitionData(ObsoleteResolverProxy::class, ObsoleteResolverProxy(obsoleteResolverAnn), filePattern)
else -> ScriptTemplateDefinitionData(BasicScriptDependenciesResolver::class, BasicScriptDependenciesResolver(), filePattern)
}
}
override val name = template.simpleName!!
@@ -156,29 +150,15 @@ data class KotlinScriptDefinitionFromTemplate(val template: KClass<out Any>,
override fun getScriptName(script: KtScript): Name = ScriptNameUtil.fileNameWithExtensionStripped(script, KotlinParserDefinition.STD_SCRIPT_EXT)
override fun <TF> getDependenciesFor(file: TF, project: Project, previousDependencies: KotlinScriptExternalDependencies?): KotlinScriptExternalDependencies? {
val fileAnnotations = getAnnotationEntries(file, project)
.map { KtAnnotationWrapper(it) }
.mapNotNull { wrappedAnn ->
val classLoader = (template as Any).javaClass.classLoader
val annotationWrappers = getAnnotationEntries(file, project)
.mapNotNull { psiAnn ->
// TODO: consider advanced matching using semantic similar to actual resolving
definitionData.acceptedAnnotations.find {
wrappedAnn.name == it.simpleName || wrappedAnn.name == it.qualifiedName
}?.let { it to wrappedAnn }
definitionData.acceptedAnnotations.find { ann ->
psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName }
}?.let { KtAnnotationWrapper(psiAnn, classLoader.loadClass(it.qualifiedName).kotlin as KClass<out Annotation>) }
}
val annotations = fileAnnotations.map { annClassToWrapper ->
try {
val handler = AnnProxyInvocationHandler(annClassToWrapper.first, annClassToWrapper.second.valueArguments)
val proxy = Proxy.newProxyInstance((template as Any).javaClass.classLoader, arrayOf(annClassToWrapper.first.java), handler) as Annotation
annClassToWrapper.first to proxy
}
catch (ex: Exception) {
annClassToWrapper.first to InvalidScriptResolverAnnotation(annClassToWrapper.second.name, annClassToWrapper.second.valueArguments, ex)
}
}
val supportedAnnotations = annotations.mapNotNull { annClassToWrapper ->
val annFQN = annClassToWrapper.first.qualifiedName
if (definitionData.acceptedAnnotations.any { it.qualifiedName == annFQN }) annClassToWrapper.second else null
}
val fileDeps = definitionData.resolver?.resolve(BasicScriptContents(file, supportedAnnotations), environment, previousDependencies)
val fileDeps = definitionData.resolver?.resolve(BasicScriptContents(file, annotationWrappers.map { it.getProxy(classLoader) }), environment, previousDependencies)
return fileDeps
}
@@ -209,20 +189,6 @@ data class KotlinScriptDefinitionFromTemplate(val template: KClass<out Any>,
}
}
class InvalidScriptResolverAnnotation(val name: String, val params: Iterable<Any?>, val error: Exception? = null) : Annotation
class AnnProxyInvocationHandler<out K: KClass<out Any>>(val targetAnnClass: K, val annParams: List<Pair<String?, Any?>>) : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, params: Array<out Any>?): Any? {
if (method == null) return null
targetAnnClass.memberProperties.forEachIndexed { i, prop ->
if (prop.name == method.name) {
return if (i >= annParams.size) null else annParams[i].second
}
}
return null
}
}
internal fun sameSignature(left: KFunction<*>, right: KFunction<*>): Boolean =
left.parameters.size == right.parameters.size &&
left.parameters.zip(right.parameters).all {
+16
View File
@@ -0,0 +1,16 @@
// this script expected parameter num : Int
@file:DependsOnTwo(path2 = "@{runtime}")
fun fib(n: Int): Int {
val v = if(n < 2) 1 else fib(n-1) + fib(n-2)
System.out.println("fib($n)=$v")
return v
}
val hdr = "Num".decapitalize()
System.out.println("$hdr: $num")
val result = fib(num)
@@ -69,6 +69,13 @@ class ScriptTest2 {
aClass!!.getConstructor(Integer.TYPE).newInstance(4)
}
@Test
fun testScriptWithDependsAnn2() {
val aClass = compileScript("fib_ext_ann2.kts", ScriptWithIntParam::class, null)
Assert.assertNotNull(aClass)
aClass!!.getConstructor(Integer.TYPE).newInstance(4)
}
private fun compileScript(
scriptPath: String,
scriptBase: KClass<out Any>,
@@ -123,7 +130,7 @@ class TestKotlinScriptDependenciesResolver : ScriptDependenciesResolverEx {
private val kotlinPaths by lazy { PathUtil.getKotlinPathsForCompiler() }
@AcceptedAnnotations(DependsOn::class)
@AcceptedAnnotations(DependsOn::class, DependsOnTwo::class)
override fun resolve(script: ScriptContents,
environment: Map<String, Any?>?,
previousDependencies: KotlinScriptExternalDependencies?
@@ -132,13 +139,20 @@ class TestKotlinScriptDependenciesResolver : ScriptDependenciesResolverEx {
val cp = script.annotations.flatMap {
when (it) {
is DependsOn -> listOf(if (it.path == "@{runtime}") kotlinPaths.runtimePath else File(it.path))
is DependsOnTwo -> listOf(it.path1, it.path2).mapNotNull {
when {
it.isBlank() -> null
it == "@{runtime}" -> kotlinPaths.runtimePath
else -> File(it)
}
}
is InvalidScriptResolverAnnotation -> throw Exception("Invalid annotation ${it.name}", it.error)
else -> throw Exception("Unknown annotation ${it.javaClass}")
}
}
return object : KotlinScriptExternalDependencies {
override val classpath: Iterable<File> = classpathFromClassloader() + cp
override val imports: Iterable<String> = listOf("org.jetbrains.kotlin.scripts.DependsOn")
override val imports: Iterable<String> = listOf("org.jetbrains.kotlin.scripts.DependsOn", "org.jetbrains.kotlin.scripts.DependsOnTwo")
}
}
@@ -167,3 +181,7 @@ abstract class ScriptWithBaseClass(num: Int, passthrough: Int) : TestDSLClassWit
@Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.RUNTIME)
annotation class DependsOn(val path: String)
@Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.RUNTIME)
annotation class DependsOnTwo(val unused: String = "", val path1: String = "", val path2: String = "")