diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/callByJava.kt b/compiler/testData/codegen/box/reflection/createAnnotation/callByJava.kt new file mode 100644 index 00000000000..d1e5e8b4786 --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/callByJava.kt @@ -0,0 +1,78 @@ +// WITH_REFLECT +// FILE: J.java + +public interface J { + @interface NoParams {} + + @interface OneDefault { + String s() default "OK"; + } + + @interface OneNonDefault { + String s(); + } + + @interface TwoParamsOneDefault { + String s(); + int x() default 42; + } + + @interface TwoNonDefaults { + String string(); + Class clazz(); + } + + @interface ManyDefaultParams { + int i() default 0; + String s() default ""; + double d() default 3.14; + } +} + +// FILE: K.kt + +import J.* +import kotlin.reflect.KClass +import kotlin.reflect.primaryConstructor +import kotlin.test.assertEquals +import kotlin.test.assertFails + +inline fun create(args: Map): T { + val ctor = T::class.constructors.single() + return ctor.callBy(args.mapKeys { entry -> ctor.parameters.single { it.name == entry.key } }) +} + +inline fun create(): T = create(emptyMap()) + +fun box(): String { + create() + + val t1 = create() + assertEquals("OK", t1.s) + assertFails { create(mapOf("s" to 42)) } + + val t2 = create(mapOf("s" to "OK")) + assertEquals("OK", t2.s) + assertFails { create() } + + val t3 = create(mapOf("s" to "OK")) + assertEquals("OK", t3.s) + assertEquals(42, t3.x) + val t4 = create(mapOf("s" to "OK", "x" to 239)) + assertEquals(239, t4.x) + assertFails { create(mapOf("s" to "Fail", "x" to "Fail")) } + + assertFails("KClass (not Class) instances should be passed as arguments") { + create(mapOf("clazz" to String::class.java, "string" to "Fail")) + } + + val t5 = create(mapOf("clazz" to String::class, "string" to "OK")) + assertEquals("OK", t5.string) + + val t6 = create() + assertEquals(0, t6.i) + assertEquals("", t6.s) + assertEquals(3.14, t6.d) + + return "OK" +} diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/callJava.kt b/compiler/testData/codegen/box/reflection/createAnnotation/callJava.kt new file mode 100644 index 00000000000..60f662ed07e --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/callJava.kt @@ -0,0 +1,90 @@ +// WITH_REFLECT +// FILE: J.java + +public interface J { + @interface NoParams {} + + @interface OneDefault { + String foo() default "foo"; + } + + @interface OneDefaultValue { + String value() default "value"; + } + + @interface OneNonDefault { + String foo(); + } + + @interface OneNonDefaultValue { + String value(); + } + + @interface TwoParamsOneDefault { + String string(); + Class clazz() default Object.class; + } + + @interface TwoParamsOneValueOneDefault { + String value(); + Class clazz() default Object.class; + } + + @interface TwoNonDefaults { + String string(); + Class clazz(); + } + + @interface ManyDefaults { + int i() default 0; + String s() default ""; + double d() default 3.14; + } +} + +// FILE: K.kt + +import J.* +import kotlin.reflect.KClass +import kotlin.reflect.primaryConstructor +import kotlin.test.assertEquals +import kotlin.test.assertFails + +inline fun create(vararg args: Any?): T = + T::class.constructors.single().call(*args) + +fun box(): String { + create() + + assertFails { create() } + assertFails { create("") } + assertFails { create("", "") } + + assertFails { create() } + create("") + assertFails { create("", "") } + + assertFails { create() } + assertFails { create("") } + + assertFails { create() } + create("") + + assertFails { create() } + assertFails { create("") } + assertFails { create("", Any::class) } + assertFails { create(Any::class, "") } + + assertFails { create() } + assertFails { create("") } + assertFails { create("", Any::class) } + assertFails { create(Any::class, "") } + + assertFails { create("", Any::class) } + assertFails { create(Any::class, "") } + + assertFails { create() } + assertFails { create(42, "Fail", 2.72) } + + return "OK" +} diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/createJdkAnnotationInstance.kt b/compiler/testData/codegen/box/reflection/createAnnotation/createJdkAnnotationInstance.kt new file mode 100644 index 00000000000..dcb310532cb --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/createJdkAnnotationInstance.kt @@ -0,0 +1,15 @@ +// WITH_REFLECT + +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import kotlin.test.assertEquals + +fun box(): String { + val ctor = Retention::class.constructors.single() + val r = ctor.callBy(mapOf( + ctor.parameters.single { it.name == "value" } to RetentionPolicy.RUNTIME + )) + assertEquals(RetentionPolicy.RUNTIME, r.value as RetentionPolicy) + assertEquals(Retention::class.java.classLoader, r.javaClass.classLoader) + return "OK" +} diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index fda259d825a..4ef341ab7f3 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -12156,17 +12156,35 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { doTest(fileName); } + @TestMetadata("callByJava.kt") + public void testCallByJava() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/callByJava.kt"); + doTest(fileName); + } + @TestMetadata("callByKotlin.kt") public void testCallByKotlin() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/callByKotlin.kt"); doTest(fileName); } + @TestMetadata("callJava.kt") + public void testCallJava() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/callJava.kt"); + doTest(fileName); + } + @TestMetadata("callKotlin.kt") public void testCallKotlin() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/callKotlin.kt"); doTest(fileName); } + + @TestMetadata("createJdkAnnotationInstance.kt") + public void testCreateJdkAnnotationInstance() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/createJdkAnnotationInstance.kt"); + doTest(fileName); + } } @TestMetadata("compiler/testData/codegen/box/reflection/enclosing") diff --git a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/AnnotationConstructorCaller.kt b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/AnnotationConstructorCaller.kt index 1190c36071b..506df4b51f2 100644 --- a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/AnnotationConstructorCaller.kt +++ b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/AnnotationConstructorCaller.kt @@ -26,17 +26,31 @@ internal class AnnotationConstructorCaller( private val jClass: Class<*>, private val parameterNames: List, private val callMode: CallMode, + origin: Origin, methods: List = parameterNames.map { name -> jClass.getDeclaredMethod(name) } ) : FunctionCaller( null, jClass, null, methods.map { it.genericReturnType }.toTypedArray() ) { enum class CallMode { CALL_BY_NAME, POSITIONAL_CALL } + enum class Origin { JAVA, KOTLIN } + // Transform primitive int to java.lang.Integer because actual arguments passed here will be boxed and Class#isInstance should succeed private val erasedParameterTypes: List> = methods.map { method -> method.returnType.let { it.wrapperByPrimitive ?: it } } private val defaultValues: List = methods.map { method -> method.defaultValue } + init { + // TODO: consider lifting this restriction once KT-8957 is implemented + if (callMode == CallMode.POSITIONAL_CALL && origin == Origin.JAVA && (parameterNames - "value").isNotEmpty()) { + throw UnsupportedOperationException( + "Positional call of a Java annotation constructor is allowed only if there are no parameters " + + "or one parameter named \"value\". This restriction exists because Java annotations (in contrast to Kotlin)" + + "do not impose any order on their arguments. Use KCallable#callBy instead." + ) + } + } + override fun call(args: Array<*>): Any? { checkArguments(args) diff --git a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KFunctionImpl.kt b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KFunctionImpl.kt index 41e9542880f..80e295370d9 100644 --- a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KFunctionImpl.kt +++ b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KFunctionImpl.kt @@ -28,6 +28,8 @@ import kotlin.reflect.KFunction import kotlin.reflect.KotlinReflectionInternalError import kotlin.reflect.jvm.internal.AnnotationConstructorCaller.CallMode.CALL_BY_NAME import kotlin.reflect.jvm.internal.AnnotationConstructorCaller.CallMode.POSITIONAL_CALL +import kotlin.reflect.jvm.internal.AnnotationConstructorCaller.Origin.JAVA +import kotlin.reflect.jvm.internal.AnnotationConstructorCaller.Origin.KOTLIN import kotlin.reflect.jvm.internal.JvmFunctionSignature.* internal class KFunctionImpl private constructor( @@ -55,12 +57,16 @@ internal class KFunctionImpl private constructor( val member: Member? = when (jvmSignature) { is KotlinConstructor -> { if (isAnnotationConstructor) - return@caller AnnotationConstructorCaller(container.jClass, parameters.map { it.name!! }, POSITIONAL_CALL) + return@caller AnnotationConstructorCaller(container.jClass, parameters.map { it.name!! }, POSITIONAL_CALL, KOTLIN) container.findConstructorBySignature(jvmSignature.constructorDesc, isDeclared()) } is KotlinFunction -> container.findMethodBySignature(jvmSignature.methodName, jvmSignature.methodDesc, isDeclared()) is JavaMethod -> jvmSignature.method is JavaConstructor -> jvmSignature.constructor + is FakeJavaAnnotationConstructor -> { + val methods = jvmSignature.methods + return@caller AnnotationConstructorCaller(container.jClass, methods.map { it.name }, POSITIONAL_CALL, JAVA, methods) + } is BuiltInFunction -> jvmSignature.getMember(container) } @@ -86,9 +92,13 @@ internal class KFunctionImpl private constructor( } is KotlinConstructor -> { if (isAnnotationConstructor) - return@defaultCaller AnnotationConstructorCaller(container.jClass, parameters.map { it.name!! }, CALL_BY_NAME) + return@defaultCaller AnnotationConstructorCaller(container.jClass, parameters.map { it.name!! }, CALL_BY_NAME, KOTLIN) container.findDefaultConstructor(jvmSignature.constructorDesc, isDeclared()) } + is FakeJavaAnnotationConstructor -> { + val methods = jvmSignature.methods + return@defaultCaller AnnotationConstructorCaller(container.jClass, methods.map { it.name }, CALL_BY_NAME, JAVA, methods) + } else -> { // Java methods, Java constructors and built-ins don't have $default methods null diff --git a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/RuntimeTypeMapper.kt b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/RuntimeTypeMapper.kt index 2ce8df8785c..c17403fc765 100644 --- a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/RuntimeTypeMapper.kt +++ b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/RuntimeTypeMapper.kt @@ -71,9 +71,15 @@ internal sealed class JvmFunctionSignature { class JavaConstructor(val constructor: Constructor<*>) : JvmFunctionSignature() { override fun asString(): String = - "" + - constructor.parameterTypes.joinToString(separator = "", prefix = "(", postfix = ")") { it.desc } + - "V" + constructor.parameterTypes.joinToString(separator = "", prefix = "(", postfix = ")V") { it.desc } + } + + class FakeJavaAnnotationConstructor(val jClass: Class<*>) : JvmFunctionSignature() { + // Java annotations do not impose any order of methods inside them, so we consider them lexicographic here for stability + val methods = jClass.declaredMethods.sortedBy { it.name } + + override fun asString(): String = + methods.joinToString(separator = "", prefix = "(", postfix = ")V") { it.returnType.desc } } open class BuiltInFunction(private val signature: String) : JvmFunctionSignature() { @@ -189,10 +195,14 @@ internal object RuntimeTypeMapper { return JvmFunctionSignature.JavaMethod(method) } is JavaClassConstructorDescriptor -> { - val constructor = ((function.source as? JavaSourceElement)?.javaElement as? ReflectJavaConstructor)?.member ?: - throw KotlinReflectionInternalError("Incorrect resolution sequence for Java constructor $function") - - return JvmFunctionSignature.JavaConstructor(constructor) + val element = (function.source as? JavaSourceElement)?.javaElement + when { + element is ReflectJavaConstructor -> + return JvmFunctionSignature.JavaConstructor(element.member) + element is ReflectJavaClass && element.isAnnotationType -> + return JvmFunctionSignature.FakeJavaAnnotationConstructor(element.element) + else -> throw KotlinReflectionInternalError("Incorrect resolution sequence for Java constructor $function ($element)") + } } else -> throw KotlinReflectionInternalError("Unknown origin of $function (${function.javaClass})") }