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:
+50
-9
@@ -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
@@ -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 = "")
|
||||
|
||||
Reference in New Issue
Block a user