diff --git a/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt b/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt index db892b1a798..49b64ccd2c1 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt @@ -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> by lazy { - psi.valueArguments.map { +internal fun String?.orAnonymous(kind: String = ""): String = + this ?: "" + +internal class KtAnnotationWrapper(val psi: KtAnnotationEntry, val targetClass: KClass) { + val name: String get() = psi.typeName + + val valueArguments: Map by lazy { + var namedStarted = false + val res = hashMapOf() + // 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>(val targetAnnClass: K, val annParams: Map) : InvocationHandler { + override fun invoke(proxy: Any?, method: Method?, params: Array?): 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 ?: "" + 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, val error: Exception? = null) : Annotation diff --git a/compiler/frontend/src/org/jetbrains/kotlin/script/scriptTemplate.kt b/compiler/frontend/src/org/jetbrains/kotlin/script/scriptTemplate.kt index 5b44d46e9e4..1c173e7d4d1 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/script/scriptTemplate.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/script/scriptTemplate.kt @@ -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, private val definitionData by lazy { val defAnn = template.annotations.firstIsInstanceOrNull() val obsoleteResolverAnn = template.annotations.firstIsInstanceOrNull() - 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()?.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, override fun getScriptName(script: KtScript): Name = ScriptNameUtil.fileNameWithExtensionStripped(script, KotlinParserDefinition.STD_SCRIPT_EXT) override fun 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) } } - 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, } } -class InvalidScriptResolverAnnotation(val name: String, val params: Iterable, val error: Exception? = null) : Annotation - -class AnnProxyInvocationHandler>(val targetAnnClass: K, val annParams: List>) : InvocationHandler { - override fun invoke(proxy: Any?, method: Method?, params: Array?): 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 { diff --git a/compiler/testData/script/fib_ext_ann2.kts b/compiler/testData/script/fib_ext_ann2.kts new file mode 100644 index 00000000000..d0ff25e343c --- /dev/null +++ b/compiler/testData/script/fib_ext_ann2.kts @@ -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) + diff --git a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest2.kt b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest2.kt index 7c7a308f397..1f8081dee9a 100644 --- a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest2.kt +++ b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest2.kt @@ -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, @@ -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?, 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 = classpathFromClassloader() + cp - override val imports: Iterable = listOf("org.jetbrains.kotlin.scripts.DependsOn") + override val imports: Iterable = 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 = "")