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.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
@@ -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 = "")
|
||||||
|
|||||||
Reference in New Issue
Block a user