Refactor and fix script annotation processing

This commit is contained in:
Ilya Chernikov
2016-12-08 22:23:38 +01:00
parent 1cc85df78e
commit b1cd98f2d5
12 changed files with 390 additions and 208 deletions
@@ -1,144 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.cli.common
import kotlin.reflect.*
import kotlin.reflect.jvm.jvmErasure
fun tryConstructClassFromStringArgs(scriptClass: Class<*>, scriptArgs: List<String>): Any? {
try {
return scriptClass.getConstructor(Array<String>::class.java).newInstance(scriptArgs.toTypedArray())
}
catch (e: NoSuchMethodException) {
for (ctor in scriptClass.kotlin.constructors) {
val mapping = tryCreateCallableMapping(ctor, scriptArgs, StringArgsConverter())
if (mapping != null) {
try {
return ctor.callBy(mapping)
}
catch (e: Exception) { // TODO: find the exact exception type thrown then callBy fails
}
}
}
}
return null
}
fun tryCreateCallableMapping(callable: KCallable<*>, args: List<Any?>): Map<KParameter, Any?>? =
tryCreateCallableMapping(callable, args, AnyArgsConverter())
private interface ArgsConverter<T> {
data class Result(val v: Any?, val argsConsumed: Int)
fun convert(parameter: KParameter, args: List<T>, startArgIndex: Int): Result?
}
private fun <T: Any?> tryCreateCallableMapping(callable: KCallable<*>, args: List<T>, converter: ArgsConverter<T>): Map<KParameter, Any?>? {
var argIdx = 0
val res = mutableMapOf<KParameter, Any?>()
for (par in callable.parameters) {
val (compatibleArg, argsConsumed) = converter.convert(par, args, argIdx) ?: return null
if (argsConsumed > 0) {
res.put(par, compatibleArg)
argIdx += argsConsumed
}
}
return res
}
private class StringArgsConverter : ArgsConverter<String> {
override fun convert(parameter: KParameter, args: List<String>, startArgIndex: Int): ArgsConverter.Result? {
fun convertPrimitive(type: KType?, arg: String): Any? =
when (type?.classifier) {
String::class -> arg
Int::class -> arg.toInt()
Long::class -> arg.toLong()
Short::class -> arg.toShort()
Byte::class -> arg.toByte()
Char::class -> arg[0]
Float::class -> arg.toFloat()
Double::class -> arg.toDouble()
Boolean::class -> arg.toBoolean()
else -> null
}
fun convertArray(type: KType?, args: List<String>): Any? =
when (type?.classifier) {
String::class -> args.toTypedArray()
Int::class -> args.map(String::toInt).toTypedArray()
Long::class -> args.map(String::toLong).toTypedArray()
Short::class -> args.map(String::toShort).toTypedArray()
Byte::class -> args.map(String::toByte).toTypedArray()
Char::class -> args.map { it[0] }.toTypedArray()
Float::class -> args.map(String::toFloat).toTypedArray()
Double::class -> args.map(String::toDouble).toTypedArray()
Boolean::class -> args.map(String::toBoolean).toTypedArray()
else -> null
}
try {
if (startArgIndex >= args.size && parameter.isOptional)
return ArgsConverter.Result(null, 0)
if (startArgIndex < args.size) {
val primArgCandidate = convertPrimitive(parameter.type, args[startArgIndex])
if (primArgCandidate != null)
return ArgsConverter.Result(primArgCandidate, 1)
}
if ((parameter.type.classifier as? KClass<*>)?.qualifiedName == Array<Any>::class.qualifiedName) {
val arrCompType = parameter.type.arguments.getOrNull(0)?.type
val arrayArgCandidate = convertArray(arrCompType, args.drop(startArgIndex))
if (arrayArgCandidate != null)
return ArgsConverter.Result(arrayArgCandidate, args.size - startArgIndex)
}
}
catch (e: NumberFormatException) {}
return null
}
}
private class AnyArgsConverter : ArgsConverter<Any?> {
override fun convert(parameter: KParameter, args: List<Any?>, startArgIndex: Int): ArgsConverter.Result? {
fun convertSingle(type: KType, arg: Any?): Any? = when {
type.classifier == arg?.javaClass?.kotlin -> arg
type.jvmErasure.java.isAssignableFrom(arg?.javaClass) -> arg
else -> null
}
if (startArgIndex >= args.size && parameter.isOptional)
return ArgsConverter.Result(null, 0)
if (startArgIndex < args.size) {
val primArgCandidate = convertSingle(parameter.type, args[startArgIndex])
if (primArgCandidate != null)
return ArgsConverter.Result(primArgCandidate, 1)
}
// TODO: add vararg support
return null
}
}
@@ -16,7 +16,7 @@
package org.jetbrains.kotlin.cli.common.repl package org.jetbrains.kotlin.cli.common.repl
import org.jetbrains.kotlin.cli.common.tryCreateCallableMapping import org.jetbrains.kotlin.utils.tryCreateCallableMapping
import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import java.io.File import java.io.File
import java.net.URLClassLoader import java.net.URLClassLoader
@@ -34,7 +34,7 @@ import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.ExitCode import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.messages.* import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.common.output.outputUtils.writeAll import org.jetbrains.kotlin.cli.common.output.outputUtils.writeAll
import org.jetbrains.kotlin.cli.common.tryConstructClassFromStringArgs import org.jetbrains.kotlin.utils.tryConstructClassFromStringArgs
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.cli.jvm.config.* import org.jetbrains.kotlin.cli.jvm.config.*
import org.jetbrains.kotlin.codegen.ClassBuilderFactories import org.jetbrains.kotlin.codegen.ClassBuilderFactories
@@ -101,9 +101,8 @@ open class KotlinScriptDefinitionFromAnnotatedTemplate(
// TODO: consider advanced matching using semantic similar to actual resolving // TODO: consider advanced matching using semantic similar to actual resolving
acceptedAnnotations.find { ann -> acceptedAnnotations.find { ann ->
psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName } psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName }
}?.let { KtAnnotationWrapper(psiAnn, classLoader.loadClass(it.qualifiedName).kotlin as KClass<out Annotation>) } }?.let { constructAnnotation(psiAnn, classLoader.loadClass(it.qualifiedName).kotlin as KClass<out Annotation>) }
} }
.map { it.getProxy(classLoader) }
} }
catch (ex: Throwable) { catch (ex: Throwable) {
logClassloadingError(ex) logClassloadingError(ex)
@@ -23,10 +23,9 @@ 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 org.jetbrains.kotlin.utils.tryCreateCallableMappingFromNamedArgs
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.primaryConstructor import kotlin.reflect.primaryConstructor
internal val KtAnnotationEntry.typeName: String get() = (typeReference?.typeElement as? KtUserType)?.referencedName.orAnonymous() internal val KtAnnotationEntry.typeName: String get() = (typeReference?.typeElement as? KtUserType)?.referencedName.orAnonymous()
@@ -34,50 +33,26 @@ internal val KtAnnotationEntry.typeName: String get() = (typeReference?.typeElem
internal fun String?.orAnonymous(kind: String = ""): String = internal fun String?.orAnonymous(kind: String = ""): String =
this ?: "<anonymous" + (if (kind.isNotBlank()) " $kind" else "") + ">" this ?: "<anonymous" + (if (kind.isNotBlank()) " $kind" else "") + ">"
internal class KtAnnotationWrapper(val psi: KtAnnotationEntry, val targetClass: KClass<out Annotation>) { internal fun constructAnnotation(psi: KtAnnotationEntry, targetClass: KClass<out Annotation>): Annotation {
val name: String get() = psi.typeName
val valueArguments: Map<String, Any?> by lazy { val valueArguments = psi.valueArguments.map { arg ->
var namedStarted = false val evaluator = ConstantExpressionEvaluator(DefaultBuiltIns.Instance, LanguageVersionSettingsImpl.DEFAULT)
val res = hashMapOf<String, Any?>() val trace = BindingTraceContext()
// TODO: annotation constructors unsupported yet in kotlin reflection, test and correct when they will be ready val result = evaluator.evaluateToConstantValue(arg.getArgumentExpression()!!, trace, TypeUtils.NO_EXPECTED_TYPE)
val targetAnnParams = targetClass.primaryConstructor?.parameters // TODO: consider inspecting `trace` to find diagnostics reported during the computation (such as division by zero, integer overflow, invalid annotation parameters etc.)
psi.valueArguments.mapIndexed { i, arg -> val argName = arg.getArgumentName()?.asName?.toString()
val evaluator = ConstantExpressionEvaluator(DefaultBuiltIns.Instance, LanguageVersionSettingsImpl.DEFAULT) argName to result?.value
val trace = BindingTraceContext()
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
// TODO: find out how to properly report problems from here (now using bogus arg names as an indicator)
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 ?: "$(Unnamed argument for $name at $i)"
argName == null && namedStarted -> "$(Invalid argument sequence for $name at arg $i)"
targetAnnParams != null && targetAnnParams.none { it.name == argName } -> "$(Unknown argument $argName for $name)"
else -> {
namedStarted = true
argName!!
}
}
res.put(paramName, result?.value)
}
res
} }
val mappedArguments: Map<KParameter, Any?> =
tryCreateCallableMappingFromNamedArgs(targetClass.constructors.first(), valueArguments)
?: return InvalidScriptResolverAnnotation(psi.typeName, valueArguments)
internal class AnnProxyInvocationHandler(val targetAnnClass: KClass<out Annotation>, val annParams: Map<String, Any?>) : InvocationHandler { try {
override fun invoke(proxy: Any?, method: Method?, params: Array<out Any>?): Any? = method?.let { return targetClass.primaryConstructor!!.callBy(mappedArguments)
method?.name?.let { annParams[it] } }
} catch (ex: Exception) {
return InvalidScriptResolverAnnotation(psi.typeName, valueArguments, ex)
} }
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 class InvalidScriptResolverAnnotation(val name: String, val annParams: List<Pair<String?, Any?>>?, val error: Exception? = null) : Annotation
@@ -19,7 +19,7 @@ package org.jetbrains.kotlin.scripts
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.* import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.common.tryConstructClassFromStringArgs import org.jetbrains.kotlin.utils.tryConstructClassFromStringArgs
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler
@@ -35,6 +35,8 @@ import org.jetbrains.kotlin.utils.PathUtil
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.io.OutputStream
import java.io.PrintStream
import java.lang.Exception import java.lang.Exception
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.net.URL import java.net.URL
@@ -91,7 +93,14 @@ class ScriptTemplateTest {
@Test @Test
fun testScriptWithDependsAnn2() { fun testScriptWithDependsAnn2() {
Assert.assertNull(compileScript("fib_ext_ann2.kts", ScriptWithIntParamAndDummyResolver::class, null, includeKotlinRuntime = false)) val savedErr = System.err
try {
System.setErr(PrintStream(NullOutputStream()))
Assert.assertNull(compileScript("fib_ext_ann2.kts", ScriptWithIntParamAndDummyResolver::class, null, includeKotlinRuntime = false))
}
finally {
System.setErr(savedErr)
}
val aClass = compileScript("fib_ext_ann2.kts", ScriptWithIntParam::class, null, includeKotlinRuntime = false) val aClass = compileScript("fib_ext_ann2.kts", ScriptWithIntParam::class, null, includeKotlinRuntime = false)
Assert.assertNotNull(aClass) Assert.assertNotNull(aClass)
@@ -324,13 +333,13 @@ class TestKotlinScriptDependenciesResolver : TestKotlinScriptDummyDependenciesRe
val cp = script.annotations.flatMap { val cp = script.annotations.flatMap {
when (it) { when (it) {
is DependsOn -> if (it.path == "@{runtime}") listOf(kotlinPaths.runtimePath, kotlinPaths.scriptRuntimePath) else listOf(File(it.path)) is DependsOn -> if (it.path == "@{runtime}") listOf(kotlinPaths.runtimePath, kotlinPaths.scriptRuntimePath) else listOf(File(it.path))
is DependsOnTwo -> listOf(it.path1, it.path2).flatMap { is DependsOnTwo -> listOf(it.path1, it.path2).flatMap { it?.let {
when { when {
it.isBlank() -> emptyList() it.isBlank() -> emptyList()
it == "@{runtime}" -> listOf(kotlinPaths.runtimePath, kotlinPaths.scriptRuntimePath) it == "@{runtime}" -> listOf(kotlinPaths.runtimePath, kotlinPaths.scriptRuntimePath)
else -> listOf(File(it)) else -> listOf(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}")
} }
@@ -394,3 +403,9 @@ annotation class DependsOn(val path: String)
@Target(AnnotationTarget.FILE) @Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class DependsOnTwo(val unused: String = "", val path1: String = "", val path2: String = "") annotation class DependsOnTwo(val unused: String = "", val path1: String = "", val path2: String = "")
private class NullOutputStream : OutputStream() {
override fun write(b: Int) { }
override fun write(b: ByteArray) { }
override fun write(b: ByteArray, off: Int, len: Int) { }
}
@@ -19,7 +19,7 @@ package org.jetbrains.kotlin.scripts
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.* import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.common.tryConstructClassFromStringArgs import org.jetbrains.kotlin.utils.tryConstructClassFromStringArgs
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler
@@ -0,0 +1,86 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.util
import org.jetbrains.kotlin.utils.tryCreateCallableMappingFromNamedArgs
import org.jetbrains.kotlin.utils.tryCreateCallableMappingFromStringArgs
import org.junit.Assert
import org.junit.Test
import kotlin.reflect.KParameter
class ArgsToParamsMatchingTest {
@Test
fun testMatchFromStrings() {
Assert.assertNull(tryCreateCallableMappingFromStringArgs(::foo, listOf()))
Assert.assertNull(tryCreateCallableMappingFromStringArgs(::foo, listOf("1", "2")))
assertParamMapsEquals(tryCreateCallableMappingFromStringArgs(::foo, listOf("1", "2", "s", "0.1")),
"i" to 1, "b" to 2.toByte(), "c" to 's', "d" to 0.1)
Assert.assertNull(tryCreateCallableMappingFromStringArgs(::foo, listOf("1", "258", "s", "0.1")))
Assert.assertNull(tryCreateCallableMappingFromStringArgs(::foo, listOf("1", "258", "s", "0")))
Assert.assertNull(tryCreateCallableMappingFromStringArgs(::foo, listOf("1", "258", "sss", "0.1")))
assertParamMapsEquals(tryCreateCallableMappingFromStringArgs(::foo, listOf("1", "2", "s", "0.1", "abc", "true", "1", "2", "3")),
"i" to 1, "b" to 2.toByte(), "c" to 's', "d" to 0.1, "s" to "abc", "t" to true, "v" to arrayOf(1L, 2L, 3L))
}
@Test
fun testMatchNamed() {
Assert.assertNull(tryCreateCallableMappingFromNamedArgs(::foo, listOf()))
Assert.assertNull(tryCreateCallableMappingFromNamedArgs(::foo, listOf(null to 1, null to 2)))
assertParamMapsEquals(tryCreateCallableMappingFromNamedArgs(::foo, listOf(null to 1, null to 2.toByte(), null to 's', null to 0.1)),
"i" to 1, "b" to 2.toByte(), "c" to 's', "d" to 0.1)
assertParamMapsEquals(tryCreateCallableMappingFromNamedArgs(::foo, listOf(null to 1, null to 2.toByte(), "c" to 's', "d" to 0.1)),
"i" to 1, "b" to 2.toByte(), "c" to 's', "d" to 0.1)
assertParamMapsEquals(tryCreateCallableMappingFromNamedArgs(::foo, listOf(null to 1, null to 2.toByte(), "d" to 0.1, "c" to 's')),
"i" to 1, "b" to 2.toByte(), "c" to 's', "d" to 0.1)
assertParamMapsEquals(tryCreateCallableMappingFromNamedArgs(::foo, listOf(null to 1, null to 2.toByte(), null to 's', null to 0.1, "v" to arrayOf(1L, 2L, 3L))),
"i" to 1, "b" to 2.toByte(), "c" to 's', "d" to 0.1, "v" to arrayOf(1L, 2L, 3L))
Assert.assertNull(tryCreateCallableMappingFromNamedArgs(::foo, listOf(null to 1, null to 2.toByte(), null to 's', "x" to 0.1))) // wrong name
Assert.assertNull(tryCreateCallableMappingFromNamedArgs(::foo, listOf(null to 1, null to 2.toByte(), "c" to 's', null to 0.1))) // unnamed after named
}
}
private fun assertParamMapsEquals(actuals: Map<KParameter, Any?>?, vararg expected: Pair<String, Any?>) {
Assert.assertNotNull(actuals)
val stringifiedActuals = actuals!!.mapKeys { it.key.name }
val mappedExpected = expected.toMap()
if (expected != stringifiedActuals) {
Assert.assertEquals(stringifiedActuals.keys, mappedExpected.keys)
mappedExpected.forEach { exp ->
val actVal = stringifiedActuals[exp.key]
if (exp.value != actVal) {
val msg = "Unexpected value for key '${exp.key}'; expected: ${exp.value}, actual: $actVal"
if ((exp.value?.javaClass?.isArray ?: false) && (actVal?.javaClass?.isArray ?: false )) {
Assert.assertArrayEquals(msg, arrayOf(exp.value), arrayOf(actVal)) // tricking Array.deepEquals to compare single element arrays (instead of tedious casting to typed array)
}
else {
Assert.assertEquals(msg, exp.value, actVal)
}
}
}
}
}
private fun foo(i: Int, b: Byte, c: Char, d: Double = 0.0, s: String = "", t: Boolean = true, vararg v: Long) {}
@@ -0,0 +1,254 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.utils
import org.jetbrains.kotlin.utils.addToStdlib.check
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.jvm.jvmErasure
fun tryConstructClassFromStringArgs(clazz: Class<*>, args: List<String>): Any? {
try {
return clazz.getConstructor(Array<String>::class.java).newInstance(args.toTypedArray())
}
catch (e: NoSuchMethodException) {
for (ctor in clazz.kotlin.constructors) {
val mapping = tryCreateCallableMappingFromStringArgs(ctor, args)
if (mapping != null) {
try {
return ctor.callBy(mapping)
}
catch (e: Exception) { // TODO: find the exact exception type thrown then callBy fails
}
}
}
}
return null
}
fun tryCreateCallableMapping(callable: KCallable<*>, args: List<Any?>): Map<KParameter, Any?>? =
tryCreateCallableMapping(callable, args.map { NamedArgument(null, it) }.iterator(), AnyArgsConverter())
fun tryCreateCallableMappingFromStringArgs(callable: KCallable<*>, args: List<String>): Map<KParameter, Any?>? =
tryCreateCallableMapping(callable, args.map { NamedArgument(null, it) }.iterator(), StringArgsConverter())
fun tryCreateCallableMappingFromNamedArgs(callable: KCallable<*>, args: List<Pair<String?, Any?>>): Map<KParameter, Any?>? =
tryCreateCallableMapping(callable, args.map { NamedArgument(it.first, it.second) }.iterator(), AnyArgsConverter())
// ------------------------------------------------
private data class NamedArgument<out T>(val name: String?, val value: T?)
private interface ArgsConverter<T> {
sealed class Result {
object Failure : Result()
class Success(val v: Any?) : Result()
}
fun tryConvertSingle(parameter: KParameter, arg: NamedArgument<T>): Result
fun tryConvertVararg(parameter: KParameter, firstArg: NamedArgument<T>, restArgsIt: Iterator<NamedArgument<T>>): Result
fun tryConvertTail(parameter: KParameter, firstArg: NamedArgument<T>, restArgsIt: Iterator<NamedArgument<T>>): Result
}
private enum class ArgsTraversalState { UNNAMED, NAMED, TAIL }
private fun <T> tryCreateCallableMapping(callable: KCallable<*>, args: Iterator<NamedArgument<T>>, converter: ArgsConverter<T>): Map<KParameter, Any?>? {
val res = mutableMapOf<KParameter, Any?>()
var state = ArgsTraversalState.UNNAMED
val unboundParams = callable.parameters.toMutableList()
val argIt = args.iterator()
while (argIt.hasNext()) {
if (unboundParams.isEmpty()) return null // failed to match: no param left for the arg
val arg = argIt.next()
when (state) {
ArgsTraversalState.UNNAMED -> if (arg.name != null) state = ArgsTraversalState.NAMED
ArgsTraversalState.NAMED -> if (arg.name == null) state = ArgsTraversalState.TAIL
ArgsTraversalState.TAIL -> if (arg.name != null) throw IllegalArgumentException("Illegal mix of named and unnamed arguments")
}
// TODO: check the logic of named/unnamed/tail(vararg or lambda) arguments matching
when (state) {
ArgsTraversalState.UNNAMED -> {
val par = unboundParams.removeAt(0)
// try single argument first
val cvtRes = converter.tryConvertSingle(par, arg)
if (cvtRes is ArgsConverter.Result.Success) {
if (cvtRes.v == null && !par.type.isMarkedNullable) { // TODO: this is not precise check - see comments to the property; consider better approach
// or if we do not allow to overload on nullability, drop this check
return null // failed to match: null for a non-nullable value
}
res.put(par, cvtRes.v)
}
// TODO: check if second part of the condition is necessary
else if ((par.type.classifier as? KClass<*>)?.let { it.java.isArray || it.qualifiedName == Array<Any>::class.qualifiedName } ?: false) {
// try vararg
val cvtVRes = converter.tryConvertVararg(par, arg, argIt)
if (cvtVRes is ArgsConverter.Result.Success) {
res.put(par, cvtVRes.v)
}
else return null // failed to match: no suitable param for unnamed arg
}
else return null // failed to match: no suitable param for unnamed arg
}
ArgsTraversalState.NAMED -> {
assert(arg.name != null)
val parIdx = unboundParams.indexOfFirst { it.name != null && it.name == arg.name }.check { it >= 0 }
?: return null // failed to match: no matching named parameter found
val par = unboundParams.removeAt(parIdx)
val cvtRes = converter.tryConvertSingle(par, arg)
if (cvtRes is ArgsConverter.Result.Success) {
res.put(par, cvtRes.v)
}
else return null // failed to match: cannot convert arg to param's type
}
ArgsTraversalState.TAIL -> {
assert(arg.name == null)
val par = unboundParams.removeAt(unboundParams.lastIndex)
val cvtVRes = converter.tryConvertTail(par, arg, argIt)
if (cvtVRes is ArgsConverter.Result.Success) {
if (argIt.hasNext()) return null // failed to match: not all tail args are consumed
res.put(par, cvtVRes.v)
}
else return null // failed to match: no suitable param for tail arg(s)
}
}
}
return when {
unboundParams.any { !it.isOptional && !it.isVararg } -> null // fail to match: non-optional params remained
else -> res
}
}
private class StringArgsConverter : ArgsConverter<String> {
override fun tryConvertSingle(parameter: KParameter, arg: NamedArgument<String>): ArgsConverter.Result {
fun convertPrimitive(type: KType?, arg: String): Any? =
when (type?.classifier) {
String::class -> arg
Int::class -> arg.toInt()
Long::class -> arg.toLong()
Short::class -> arg.toShort()
Byte::class -> arg.toByte()
Char::class -> if (arg.length != 1) null else arg[0]
Float::class -> arg.toFloat()
Double::class -> arg.toDouble()
Boolean::class -> arg.toBoolean()
else -> null
}
try {
if (arg.value != null) {
val primArgCandidate = convertPrimitive(parameter.type, arg.value)
if (primArgCandidate != null)
return ArgsConverter.Result.Success(primArgCandidate)
}
else return ArgsConverter.Result.Success(null)
}
catch (e: NumberFormatException) {}
return ArgsConverter.Result.Failure
}
override fun tryConvertVararg(parameter: KParameter, firstArg: NamedArgument<String>, restArgsIt: Iterator<NamedArgument<String>>): ArgsConverter.Result {
fun convertAnyArray(type: KType?, args: Sequence<String?>): Any? =
when (type?.classifier) {
String::class -> args.toList().toTypedArray()
else -> {
(type?.classifier as? KClass<*>)?.constructors?.forEach { ctor ->
try {
return@convertAnyArray args.map { ctor.call(it) }.toList().toTypedArray()
}
catch (e: Exception) {}
}
null
}
}
fun convertPrimitivesArray(type: KType?, args: Sequence<String?>): Any? =
when (type?.classifier) {
IntArray::class -> args.map { it?.toInt() }.toList().toTypedArray()
LongArray::class -> args.map { it?.toLong() }.toList().toTypedArray()
ShortArray::class -> args.map { it?.toShort() }.toList().toTypedArray()
ByteArray::class -> args.map { it?.toByte() }.toList().toTypedArray()
CharArray::class -> args.map { it?.get(0) }.toList().toTypedArray()
FloatArray::class -> args.map { it?.toFloat() }.toList().toTypedArray()
DoubleArray::class -> args.map { it?.toDouble() }.toList().toTypedArray()
BooleanArray::class -> args.map { it?.toBoolean() }.toList().toTypedArray()
else -> null
}
try {
// TODO: check if second part of the condition is necessary
if ((parameter.type.classifier as? KClass<*>)?.let { it.java.isArray || it.qualifiedName == Array<Any>::class.qualifiedName } ?: false) {
val argsSequence = sequenceOf(firstArg.value) + restArgsIt.asSequence().map { it.value }
val primArrayArgCandidate = convertPrimitivesArray(parameter.type, argsSequence)
if (primArrayArgCandidate != null)
return ArgsConverter.Result.Success(primArrayArgCandidate)
val arrCompType = parameter.type.arguments.getOrNull(0)?.type
val arrayArgCandidate = convertAnyArray(arrCompType, argsSequence)
if (arrayArgCandidate != null)
return ArgsConverter.Result.Success(arrayArgCandidate)
}
}
catch (e: NumberFormatException) {}
return ArgsConverter.Result.Failure
}
override fun tryConvertTail(parameter: KParameter, firstArg: NamedArgument<String>, restArgsIt: Iterator<NamedArgument<String>>): ArgsConverter.Result =
tryConvertVararg(parameter, firstArg, restArgsIt)
}
private class AnyArgsConverter : ArgsConverter<Any> {
override fun tryConvertSingle(parameter: KParameter, arg: NamedArgument<Any>): ArgsConverter.Result {
fun convertPrimitivesArray(type: KType?, arg: Any?): Any? =
when (type?.classifier) {
IntArray::class -> (arg as? Array<Int>)?.toIntArray()
LongArray::class -> (arg as? Array<Long>)?.toLongArray()
ShortArray::class -> (arg as? Array<Short>)?.toShortArray()
ByteArray::class -> (arg as? Array<Byte>)?.toByteArray()
CharArray::class -> (arg as? Array<Char>)?.toCharArray()
FloatArray::class -> (arg as? Array<Float>)?.toFloatArray()
DoubleArray::class -> (arg as? Array<Double>)?.toDoubleArray()
BooleanArray::class -> (arg as? Array<Boolean>)?.toBooleanArray()
else -> null
}
fun convertSingle(type: KType, arg: Any?): Any? = when {
type.classifier == arg?.javaClass?.kotlin -> arg
type.jvmErasure.java.isAssignableFrom(arg?.javaClass) -> arg
else -> null
}
return (convertSingle(parameter.type, arg.value) ?: convertPrimitivesArray(parameter.type, arg.value))
?.let { ArgsConverter.Result.Success(it) }
?: ArgsConverter.Result.Failure
}
override fun tryConvertVararg(parameter: KParameter, firstArg: NamedArgument<Any>, restArgsIt: Iterator<NamedArgument<Any>>): ArgsConverter.Result =
ArgsConverter.Result.Failure
override fun tryConvertTail(parameter: KParameter, firstArg: NamedArgument<Any>, restArgsIt: Iterator<NamedArgument<Any>>): ArgsConverter.Result =
tryConvertSingle(parameter, firstArg)
}
@@ -35,7 +35,7 @@ import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.repository.ComponentDependency; import org.codehaus.plexus.component.repository.ComponentDependency;
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys; import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys;
import org.jetbrains.kotlin.cli.common.ReflectionUtilKt; import org.jetbrains.kotlin.utils.ReflectionUtilKt;
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler; import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler;
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles; import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles;
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment;
@@ -28,7 +28,7 @@ interface Resolver {
class DirectResolver : Resolver { class DirectResolver : Resolver {
override fun tryResolve(dependsOn: DependsOn): Iterable<File>? = override fun tryResolve(dependsOn: DependsOn): Iterable<File>? =
dependsOn.value?.let(::File)?.check { it.exists() && (it.isFile || it.isDirectory) }?.let { listOf(it) } dependsOn.value.check(String::isNotBlank)?.let(::File)?.check { it.exists() && (it.isFile || it.isDirectory) }?.let { listOf(it) }
} }
class FlatLibDirectoryResolver(val path: File) : Resolver { class FlatLibDirectoryResolver(val path: File) : Resolver {
@@ -39,10 +39,10 @@ class FlatLibDirectoryResolver(val path: File) : Resolver {
override fun tryResolve(dependsOn: DependsOn): Iterable<File>? = override fun tryResolve(dependsOn: DependsOn): Iterable<File>? =
// TODO: add coordinates and wildcard matching // TODO: add coordinates and wildcard matching
dependsOn.value?.let{ File(path, it) }?.check { it.exists() && (it.isFile || it.isDirectory) }?.let { listOf(it) } dependsOn.value.check(String::isNotBlank)?.let{ File(path, it) }?.check { it.exists() && (it.isFile || it.isDirectory) }?.let { listOf(it) }
companion object { companion object {
fun tryCreate(annotation: Repository): FlatLibDirectoryResolver? = fun tryCreate(annotation: Repository): FlatLibDirectoryResolver? =
annotation.value?.check(String::isNotBlank)?.let(::File)?.check { it.exists() && it.isDirectory }?.let(::FlatLibDirectoryResolver) annotation.value.check(String::isNotBlank)?.let(::File)?.check { it.exists() && it.isDirectory }?.let(::FlatLibDirectoryResolver)
} }
} }
@@ -43,7 +43,7 @@ class MavenResolver(val reportError: ((String) -> Unit)? = null): Resolver {
private fun currentRepos() = if (repos.isEmpty()) arrayListOf(mavenCentral) else repos private fun currentRepos() = if (repos.isEmpty()) arrayListOf(mavenCentral) else repos
private fun String?.isValidParam() = if (this != null && isNotBlank()) true else false private fun String.isValidParam() = isNotBlank()
override fun tryResolve(dependsOn: DependsOn): Iterable<File>? { override fun tryResolve(dependsOn: DependsOn): Iterable<File>? {
@@ -55,13 +55,10 @@ class MavenResolver(val reportError: ((String) -> Unit)? = null): Resolver {
val artifactId: DefaultArtifact = when { val artifactId: DefaultArtifact = when {
dependsOn.groupId.isValidParam() || dependsOn.artifactId.isValidParam() -> { dependsOn.groupId.isValidParam() || dependsOn.artifactId.isValidParam() -> {
val s1 = dependsOn.groupId
val s2 = dependsOn.artifactId
DefaultArtifact(dependsOn.groupId.orNullIfBlank(), dependsOn.artifactId.orNullIfBlank(), null, dependsOn.version.orNullIfBlank()) DefaultArtifact(dependsOn.groupId.orNullIfBlank(), dependsOn.artifactId.orNullIfBlank(), null, dependsOn.version.orNullIfBlank())
} }
dependsOn.value.isValidParam() && dependsOn.value.count { it == ':' } == 2 -> { dependsOn.value.isValidParam() && dependsOn.value.count { it == ':' } == 2 -> {
val s = dependsOn.value DefaultArtifact(dependsOn.value)
DefaultArtifact(s)
} }
else -> { else -> {
error("Unknown set of arguments to maven resolver: ${dependsOn.value}") error("Unknown set of arguments to maven resolver: ${dependsOn.value}")
@@ -84,7 +81,7 @@ class MavenResolver(val reportError: ((String) -> Unit)? = null): Resolver {
} }
fun tryAddRepo(annotation: Repository): Boolean { fun tryAddRepo(annotation: Repository): Boolean {
val urlStr = if (annotation.url.isValidParam()) annotation.url else annotation.value ?: return false val urlStr = annotation.url.check { it.isValidParam() } ?: annotation.value.check { it.isValidParam() } ?: return false
try { try {
URL(urlStr) URL(urlStr)
} catch (_: MalformedURLException) { } catch (_: MalformedURLException) {
@@ -92,7 +89,7 @@ class MavenResolver(val reportError: ((String) -> Unit)? = null): Resolver {
} }
repos.add( repos.add(
RemoteRepository( RemoteRepository(
if (annotation.id.isValidParam()) annotation.id else "cantral", if (annotation.id.isValidParam()) annotation.id else "central",
"default", "default",
urlStr urlStr
)) ))