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.BindingTraceContext
import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator
import org.jetbrains.kotlin.types.TypeUtils 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) { internal val KtAnnotationEntry.typeName: String get() = (typeReference?.typeElement as? KtUserType)?.referencedName.orAnonymous()
val name: String
get() = (psi.typeReference?.typeElement as? KtUserType)?.referencedName.orAnonymous()
val valueArguments: List<Pair<String?, Any?>> by lazy { internal fun String?.orAnonymous(kind: String = ""): String =
psi.valueArguments.map { 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 evaluator = ConstantExpressionEvaluator(DefaultBuiltIns.Instance)
val trace = BindingTraceContext() val trace = BindingTraceContext()
val result = evaluator.evaluateToConstantValue(it.getArgumentExpression()!!, trace, TypeUtils.NO_EXPECTED_TYPE) val result = evaluator.evaluateToConstantValue(arg.getArgumentExpression()!!, trace, TypeUtils.NO_EXPECTED_TYPE)
it.getArgumentName()?.asName.toString() to result?.value
// TODO: consider inspecting `trace` to find diagnostics reported during the computation (such as division by zero, integer overflow, invalid annotation parameters etc.) // 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 = fun getProxy(classLoader: ClassLoader): Annotation =
this ?: "<anonymous" + (if (kind.isNotBlank()) " $kind" else "") + ">" 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.types.KotlinType
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File 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.* import kotlin.reflect.*
const val DEFAULT_SCRIPT_FILE_PATTERN = "*.\\.kts" const val DEFAULT_SCRIPT_FILE_PATTERN = "*.\\.kts"
@@ -125,17 +121,15 @@ data class KotlinScriptDefinitionFromTemplate(val template: KClass<out Any>,
private val definitionData by lazy { private val definitionData by lazy {
val defAnn = template.annotations.firstIsInstanceOrNull<ScriptTemplateDefinition>() val defAnn = template.annotations.firstIsInstanceOrNull<ScriptTemplateDefinition>()
val obsoleteResolverAnn = template.annotations.firstIsInstanceOrNull<ScriptDependencyResolver>() 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 ?: val filePattern = defAnn?.scriptFilePattern ?:
template.annotations.firstIsInstanceOrNull<ScriptFilePattern>()?.pattern ?: template.annotations.firstIsInstanceOrNull<ScriptFilePattern>()?.pattern ?:
DEFAULT_SCRIPT_FILE_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!! 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 getScriptName(script: KtScript): Name = ScriptNameUtil.fileNameWithExtensionStripped(script, KotlinParserDefinition.STD_SCRIPT_EXT)
override fun <TF> getDependenciesFor(file: TF, project: Project, previousDependencies: KotlinScriptExternalDependencies?): KotlinScriptExternalDependencies? { override fun <TF> getDependenciesFor(file: TF, project: Project, previousDependencies: KotlinScriptExternalDependencies?): KotlinScriptExternalDependencies? {
val fileAnnotations = getAnnotationEntries(file, project) val classLoader = (template as Any).javaClass.classLoader
.map { KtAnnotationWrapper(it) } val annotationWrappers = getAnnotationEntries(file, project)
.mapNotNull { wrappedAnn -> .mapNotNull { psiAnn ->
// TODO: consider advanced matching using semantic similar to actual resolving // TODO: consider advanced matching using semantic similar to actual resolving
definitionData.acceptedAnnotations.find { definitionData.acceptedAnnotations.find { ann ->
wrappedAnn.name == it.simpleName || wrappedAnn.name == it.qualifiedName psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName }
}?.let { it to wrappedAnn } }?.let { KtAnnotationWrapper(psiAnn, classLoader.loadClass(it.qualifiedName).kotlin as KClass<out Annotation>) }
} }
val annotations = fileAnnotations.map { annClassToWrapper -> val fileDeps = definitionData.resolver?.resolve(BasicScriptContents(file, annotationWrappers.map { it.getProxy(classLoader) }), environment, previousDependencies)
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)
return fileDeps 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 = internal fun sameSignature(left: KFunction<*>, right: KFunction<*>): Boolean =
left.parameters.size == right.parameters.size && left.parameters.size == right.parameters.size &&
left.parameters.zip(right.parameters).all { 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) 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( private fun compileScript(
scriptPath: String, scriptPath: String,
scriptBase: KClass<out Any>, scriptBase: KClass<out Any>,
@@ -123,7 +130,7 @@ class TestKotlinScriptDependenciesResolver : ScriptDependenciesResolverEx {
private val kotlinPaths by lazy { PathUtil.getKotlinPathsForCompiler() } private val kotlinPaths by lazy { PathUtil.getKotlinPathsForCompiler() }
@AcceptedAnnotations(DependsOn::class) @AcceptedAnnotations(DependsOn::class, DependsOnTwo::class)
override fun resolve(script: ScriptContents, override fun resolve(script: ScriptContents,
environment: Map<String, Any?>?, environment: Map<String, Any?>?,
previousDependencies: KotlinScriptExternalDependencies? previousDependencies: KotlinScriptExternalDependencies?
@@ -132,13 +139,20 @@ class TestKotlinScriptDependenciesResolver : ScriptDependenciesResolverEx {
val cp = script.annotations.flatMap { val cp = script.annotations.flatMap {
when (it) { when (it) {
is DependsOn -> listOf(if (it.path == "@{runtime}") kotlinPaths.runtimePath else File(it.path)) 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) is InvalidScriptResolverAnnotation -> throw Exception("Invalid annotation ${it.name}", it.error)
else -> throw Exception("Unknown annotation ${it.javaClass}") else -> throw Exception("Unknown annotation ${it.javaClass}")
} }
} }
return object : KotlinScriptExternalDependencies { return object : KotlinScriptExternalDependencies {
override val classpath: Iterable<File> = classpathFromClassloader() + cp 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) @Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class DependsOn(val path: String) annotation class DependsOn(val path: String)
@Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.RUNTIME)
annotation class DependsOnTwo(val unused: String = "", val path1: String = "", val path2: String = "")