From b1cd98f2d5d9512cbae8d2b957a6436daab819e5 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Thu, 8 Dec 2016 22:23:38 +0100 Subject: [PATCH] Refactor and fix script annotation processing --- .../kotlin/cli/common/reflectionUtil.kt | 144 ---------- .../repl/GenericReplCompiledEvaluator.kt | 2 +- .../compiler/KotlinToJVMBytecodeCompiler.kt | 2 +- ...inScriptDefinitionFromAnnotatedTemplate.kt | 3 +- .../script/scriptAnnotationsPreprocessing.kt | 63 ++--- .../kotlin/scripts/ScriptTemplateTest.kt | 23 +- .../jetbrains/kotlin/scripts/ScriptTest.kt | 2 +- .../kotlin/util/ArgsToParamsMatchingTest.kt | 86 ++++++ .../jetbrains/kotlin/utils/reflectionUtil.kt | 254 ++++++++++++++++++ .../kotlin/maven/ExecuteKotlinScriptMojo.java | 2 +- .../kotlin/script/util/resolvers/basic.kt | 6 +- .../kotlin/script/util/resolvers/maven.kt | 11 +- 12 files changed, 390 insertions(+), 208 deletions(-) delete mode 100644 compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/reflectionUtil.kt create mode 100644 compiler/tests/org/jetbrains/kotlin/util/ArgsToParamsMatchingTest.kt create mode 100644 compiler/util/src/org/jetbrains/kotlin/utils/reflectionUtil.kt diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/reflectionUtil.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/reflectionUtil.kt deleted file mode 100644 index 13dfd0b7d38..00000000000 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/reflectionUtil.kt +++ /dev/null @@ -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): Any? { - - try { - return scriptClass.getConstructor(Array::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): Map? = - tryCreateCallableMapping(callable, args, AnyArgsConverter()) - - -private interface ArgsConverter { - - data class Result(val v: Any?, val argsConsumed: Int) - - fun convert(parameter: KParameter, args: List, startArgIndex: Int): Result? -} - -private fun tryCreateCallableMapping(callable: KCallable<*>, args: List, converter: ArgsConverter): Map? { - - var argIdx = 0 - val res = mutableMapOf() - 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 { - - override fun convert(parameter: KParameter, args: List, 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): 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::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 { - override fun convert(parameter: KParameter, args: List, 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 - } -} - diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt index 1e675d05a84..c2e2277f9f7 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt @@ -16,7 +16,7 @@ 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 java.io.File import java.net.URLClassLoader diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt index 2fed0247c2f..acec51c584d 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt @@ -34,7 +34,7 @@ import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.ExitCode import org.jetbrains.kotlin.cli.common.messages.* 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.config.* import org.jetbrains.kotlin.codegen.ClassBuilderFactories diff --git a/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionFromAnnotatedTemplate.kt b/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionFromAnnotatedTemplate.kt index 9895c1c06bf..b9e1280400f 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionFromAnnotatedTemplate.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionFromAnnotatedTemplate.kt @@ -101,9 +101,8 @@ open class KotlinScriptDefinitionFromAnnotatedTemplate( // TODO: consider advanced matching using semantic similar to actual resolving acceptedAnnotations.find { ann -> psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName } - }?.let { KtAnnotationWrapper(psiAnn, classLoader.loadClass(it.qualifiedName).kotlin as KClass) } + }?.let { constructAnnotation(psiAnn, classLoader.loadClass(it.qualifiedName).kotlin as KClass) } } - .map { it.getProxy(classLoader) } } catch (ex: Throwable) { logClassloadingError(ex) diff --git a/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt b/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt index cb1d66eb657..f4213b36c77 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/script/scriptAnnotationsPreprocessing.kt @@ -23,10 +23,9 @@ 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 org.jetbrains.kotlin.utils.tryCreateCallableMappingFromNamedArgs import kotlin.reflect.KClass +import kotlin.reflect.KParameter import kotlin.reflect.primaryConstructor 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 = this ?: "" -internal class KtAnnotationWrapper(val psi: KtAnnotationEntry, val targetClass: KClass) { - val name: String get() = psi.typeName +internal fun constructAnnotation(psi: KtAnnotationEntry, targetClass: KClass): Annotation { - 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, LanguageVersionSettingsImpl.DEFAULT) - 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 valueArguments = psi.valueArguments.map { arg -> + val evaluator = ConstantExpressionEvaluator(DefaultBuiltIns.Instance, LanguageVersionSettingsImpl.DEFAULT) + 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() + argName to result?.value } + val mappedArguments: Map = + tryCreateCallableMappingFromNamedArgs(targetClass.constructors.first(), valueArguments) + ?: return InvalidScriptResolverAnnotation(psi.typeName, valueArguments) - internal class AnnProxyInvocationHandler(val targetAnnClass: KClass, val annParams: Map) : InvocationHandler { - override fun invoke(proxy: Any?, method: Method?, params: Array?): Any? = method?.let { - method?.name?.let { annParams[it] } - } + try { + return targetClass.primaryConstructor!!.callBy(mappedArguments) + } + 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, val error: Exception? = null) : Annotation +class InvalidScriptResolverAnnotation(val name: String, val annParams: List>?, val error: Exception? = null) : Annotation diff --git a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt index 759f96ba868..68249bb22ca 100644 --- a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt @@ -19,7 +19,7 @@ package org.jetbrains.kotlin.scripts import com.intellij.openapi.util.Disposer import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys 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.KotlinCoreEnvironment 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.Test import java.io.File +import java.io.OutputStream +import java.io.PrintStream import java.lang.Exception import java.lang.reflect.InvocationTargetException import java.net.URL @@ -91,7 +93,14 @@ class ScriptTemplateTest { @Test 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) Assert.assertNotNull(aClass) @@ -324,13 +333,13 @@ class TestKotlinScriptDependenciesResolver : TestKotlinScriptDummyDependenciesRe val cp = script.annotations.flatMap { when (it) { 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 { it.isBlank() -> emptyList() it == "@{runtime}" -> listOf(kotlinPaths.runtimePath, kotlinPaths.scriptRuntimePath) else -> listOf(File(it)) } - } + }} is InvalidScriptResolverAnnotation -> throw Exception("Invalid annotation ${it.name}", it.error) else -> throw Exception("Unknown annotation ${it.javaClass}") } @@ -394,3 +403,9 @@ annotation class DependsOn(val path: String) @Target(AnnotationTarget.FILE) @Retention(AnnotationRetention.RUNTIME) 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) { } +} diff --git a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest.kt b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest.kt index 606e2b30326..d2f92e2dd04 100644 --- a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTest.kt @@ -19,7 +19,7 @@ package org.jetbrains.kotlin.scripts import com.intellij.openapi.util.Disposer import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys 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.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler diff --git a/compiler/tests/org/jetbrains/kotlin/util/ArgsToParamsMatchingTest.kt b/compiler/tests/org/jetbrains/kotlin/util/ArgsToParamsMatchingTest.kt new file mode 100644 index 00000000000..1d82efdb687 --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/util/ArgsToParamsMatchingTest.kt @@ -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?, vararg expected: Pair) { + 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) {} diff --git a/compiler/util/src/org/jetbrains/kotlin/utils/reflectionUtil.kt b/compiler/util/src/org/jetbrains/kotlin/utils/reflectionUtil.kt new file mode 100644 index 00000000000..6c424896eb4 --- /dev/null +++ b/compiler/util/src/org/jetbrains/kotlin/utils/reflectionUtil.kt @@ -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): Any? { + + try { + return clazz.getConstructor(Array::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): Map? = + tryCreateCallableMapping(callable, args.map { NamedArgument(null, it) }.iterator(), AnyArgsConverter()) + +fun tryCreateCallableMappingFromStringArgs(callable: KCallable<*>, args: List): Map? = + tryCreateCallableMapping(callable, args.map { NamedArgument(null, it) }.iterator(), StringArgsConverter()) + +fun tryCreateCallableMappingFromNamedArgs(callable: KCallable<*>, args: List>): Map? = + tryCreateCallableMapping(callable, args.map { NamedArgument(it.first, it.second) }.iterator(), AnyArgsConverter()) + +// ------------------------------------------------ + +private data class NamedArgument(val name: String?, val value: T?) + +private interface ArgsConverter { + + sealed class Result { + object Failure : Result() + class Success(val v: Any?) : Result() + } + fun tryConvertSingle(parameter: KParameter, arg: NamedArgument): Result + fun tryConvertVararg(parameter: KParameter, firstArg: NamedArgument, restArgsIt: Iterator>): Result + fun tryConvertTail(parameter: KParameter, firstArg: NamedArgument, restArgsIt: Iterator>): Result +} + +private enum class ArgsTraversalState { UNNAMED, NAMED, TAIL } + +private fun tryCreateCallableMapping(callable: KCallable<*>, args: Iterator>, converter: ArgsConverter): Map? { + val res = mutableMapOf() + 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::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 { + + override fun tryConvertSingle(parameter: KParameter, arg: NamedArgument): 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, restArgsIt: Iterator>): ArgsConverter.Result { + + fun convertAnyArray(type: KType?, args: Sequence): 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): 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::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, restArgsIt: Iterator>): ArgsConverter.Result = + tryConvertVararg(parameter, firstArg, restArgsIt) +} + +private class AnyArgsConverter : ArgsConverter { + override fun tryConvertSingle(parameter: KParameter, arg: NamedArgument): ArgsConverter.Result { + + fun convertPrimitivesArray(type: KType?, arg: Any?): Any? = + when (type?.classifier) { + IntArray::class -> (arg as? Array)?.toIntArray() + LongArray::class -> (arg as? Array)?.toLongArray() + ShortArray::class -> (arg as? Array)?.toShortArray() + ByteArray::class -> (arg as? Array)?.toByteArray() + CharArray::class -> (arg as? Array)?.toCharArray() + FloatArray::class -> (arg as? Array)?.toFloatArray() + DoubleArray::class -> (arg as? Array)?.toDoubleArray() + BooleanArray::class -> (arg as? Array)?.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, restArgsIt: Iterator>): ArgsConverter.Result = + ArgsConverter.Result.Failure + + override fun tryConvertTail(parameter: KParameter, firstArg: NamedArgument, restArgsIt: Iterator>): ArgsConverter.Result = + tryConvertSingle(parameter, firstArg) +} + diff --git a/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java b/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java index f6be1a68850..ee5391ae4fa 100644 --- a/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java +++ b/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java @@ -35,7 +35,7 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.component.repository.ComponentDependency; 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.compiler.EnvironmentConfigFiles; import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; diff --git a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/basic.kt b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/basic.kt index e94f5f726d0..f8b32ba2104 100644 --- a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/basic.kt +++ b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/basic.kt @@ -28,7 +28,7 @@ interface Resolver { class DirectResolver : Resolver { override fun tryResolve(dependsOn: DependsOn): Iterable? = - 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 { @@ -39,10 +39,10 @@ class FlatLibDirectoryResolver(val path: File) : Resolver { override fun tryResolve(dependsOn: DependsOn): Iterable? = // 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 { 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) } } diff --git a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/maven.kt b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/maven.kt index b734ea0b66b..25b541f7842 100644 --- a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/maven.kt +++ b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/util/resolvers/maven.kt @@ -43,7 +43,7 @@ class MavenResolver(val reportError: ((String) -> Unit)? = null): Resolver { 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? { @@ -55,13 +55,10 @@ class MavenResolver(val reportError: ((String) -> Unit)? = null): Resolver { val artifactId: DefaultArtifact = when { 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()) } dependsOn.value.isValidParam() && dependsOn.value.count { it == ':' } == 2 -> { - val s = dependsOn.value - DefaultArtifact(s) + DefaultArtifact(dependsOn.value) } else -> { 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 { - 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 { URL(urlStr) } catch (_: MalformedURLException) { @@ -92,7 +89,7 @@ class MavenResolver(val reportError: ((String) -> Unit)? = null): Resolver { } repos.add( RemoteRepository( - if (annotation.id.isValidParam()) annotation.id else "cantral", + if (annotation.id.isValidParam()) annotation.id else "central", "default", urlStr ))