diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/annotationType.kt b/compiler/testData/codegen/box/reflection/createAnnotation/annotationType.kt new file mode 100644 index 00000000000..f0a37391294 --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/annotationType.kt @@ -0,0 +1,11 @@ +// WITH_REFLECT + +import kotlin.test.assertEquals + +annotation class Foo + +fun box(): String { + val foo = Foo::class.constructors.single().call() + assertEquals(Foo::class, foo.annotationClass) + return "OK" +} diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/arrayOfKClasses.kt b/compiler/testData/codegen/box/reflection/createAnnotation/arrayOfKClasses.kt index 69fca655d33..3916c31653b 100644 --- a/compiler/testData/codegen/box/reflection/createAnnotation/arrayOfKClasses.kt +++ b/compiler/testData/codegen/box/reflection/createAnnotation/arrayOfKClasses.kt @@ -8,5 +8,6 @@ annotation class Anno(val klasses: Array> = arrayOf(String::class, Int fun box(): String { val anno = Anno::class.constructors.single().callBy(emptyMap()) assertEquals(listOf(String::class, Int::class), (anno.klasses as Array>).toList() /* TODO: KT-9453 */) + assertEquals("@Anno(klasses=[class java.lang.String, int])", anno.toString()) return "OK" } diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/enumKClassAnnotation.kt b/compiler/testData/codegen/box/reflection/createAnnotation/enumKClassAnnotation.kt new file mode 100644 index 00000000000..ae91ba37409 --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/enumKClassAnnotation.kt @@ -0,0 +1,47 @@ +// WITH_REFLECT + +import kotlin.reflect.KClass +import kotlin.test.assertEquals + +annotation class Foo(val value: String) + +annotation class Anno( + val level: DeprecationLevel, + val klass: KClass<*>, + val foo: Foo, + val levels: Array, + val klasses: Array>, + val foos: Array +) + +@Anno( + DeprecationLevel.WARNING, + Number::class, + Foo("OK"), + arrayOf(DeprecationLevel.WARNING), + arrayOf(Number::class), + arrayOf(Foo("OK")) +) +fun foo() {} + +fun box(): String { + // Construct an annotation with exactly the same parameters, check that the proxy created by Kotlin and by Java reflection are the same and have the same hash code + val a1 = Anno::class.constructors.single().call( + DeprecationLevel.WARNING, + Number::class, + Foo::class.constructors.single().call("OK"), + arrayOf(DeprecationLevel.WARNING), + arrayOf(Number::class), + arrayOf(Foo::class.constructors.single().call("OK")) + ) + val a2 = ::foo.annotations.single() as Anno + + assertEquals(a1, a2) + assertEquals(a2, a1) + assertEquals(a1.hashCode(), a2.hashCode()) + + assertEquals("@Anno(level=WARNING, klass=class java.lang.Number, foo=@Foo(value=OK), " + + "levels=[WARNING], klasses=[class java.lang.Number], foos=[@Foo(value=OK)])", a1.toString()) + + return "OK" +} diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/equalsHashCodeToString.kt b/compiler/testData/codegen/box/reflection/createAnnotation/equalsHashCodeToString.kt new file mode 100644 index 00000000000..997c869c6e9 --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/equalsHashCodeToString.kt @@ -0,0 +1,46 @@ +// WITH_REFLECT + +package test + +annotation class A +annotation class B(val s: String) + +@A +@B("2") +fun javaReflectionAnnotationInstances() {} + +fun box(): String { + val createA = A::class.constructors.single() + + val a1 = createA.call() + if (a1.toString() != "@test.A()") return "Fail: toString does not correspond to the documentation of java.lang.annotation.Annotation#toString: $a1" + + val a2 = createA.call() + if (a1 === a2) return "Fail: instances created by the constructor should be different" + if (a1 != a2) return "Fail: any instance of A should be equal to any other instance of A" + if (a1.hashCode() != a2.hashCode()) return "Fail: hash codes of equal instances should be equal" + if (a1.hashCode() != 0) return "Fail: hashCode does not correspond to the documentation of java.lang.annotation.Annotation#hashCode: ${a1.hashCode()}" + + val createB = B::class.constructors.single() + val b1 = createB.call("1") + if (b1.toString() != "@test.B(s=1)") return "Fail: toString does not correspond to the documentation of java.lang.annotation.Annotation#toString: $b1" + if (b1 != b1) return "Fail: instance should be equal to itself" + + val b2 = createB.call("2") + if (b1 == b2) return "Fail: instances with different data should not be equal" + if (b1.hashCode() == b2.hashCode()) return "Fail: hash codes of different instances should very likely be also different" + + val a3 = ::javaReflectionAnnotationInstances.annotations.filterIsInstance().single() + if (a1 === a3) return "Fail: instance created by the constructor and the one obtained from Java reflection should be different" + if (a1 != a3) return "Fail: instance created by the constructor should be equal to the one obtained from Java reflection" + if (a3 != a1) return "Fail: instance obtained from Java reflection should be equal to the one created by the constructor" + if (a1.hashCode() != a3.hashCode()) return "Fail: hash codes of equal instances should be equal" + + val b3 = ::javaReflectionAnnotationInstances.annotations.filterIsInstance().single() + if (b2 === b3) return "Fail: instance created by the constructor and the one obtained from Java reflection should be different" + if (b2 != b3) return "Fail: instance created by the constructor should be equal to the one obtained from Java reflection" + if (b3 != b2) return "Fail: instance obtained from Java reflection should be equal to the one created by the constructor" + if (b2.hashCode() != b3.hashCode()) return "Fail: hash codes of equal instances should be equal" + + return "OK" +} diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/floatingPointParameters.kt b/compiler/testData/codegen/box/reflection/createAnnotation/floatingPointParameters.kt new file mode 100644 index 00000000000..85242256bd9 --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/floatingPointParameters.kt @@ -0,0 +1,63 @@ +// WITH_REFLECT + +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +annotation class D(val d: Double) +annotation class F(val f: Float) + +/* +// TODO: uncomment once KT-13887 is implemented +@D(Double.NaN) +fun dnan() {} + +@F(Float.NaN) +fun fnan() {} +*/ + +@D(-0.0) +fun dMinusZero() {} +@D(+0.0) +fun dPlusZero() {} + +@F(-0.0f) +fun fMinusZero() {} +@F(+0.0f) +fun fPlusZero() {} + +fun check(x: Any, y: Any) { + assertEquals(x, y) + assertEquals(y, x) + assertEquals(x.hashCode(), y.hashCode()) + assertEquals(x.toString(), y.toString()) +} + +fun checkNot(x: Any, y: Any) { + assertNotEquals(x, y) + assertNotEquals(y, x) + assertNotEquals(x.hashCode(), y.hashCode()) + assertNotEquals(x.toString(), y.toString()) +} + +fun box(): String { +/* + check(::dnan.annotations.single() as D, D::class.constructors.single().call(Double.NaN)) + check(::fnan.annotations.single() as F, F::class.constructors.single().call(Float.NaN)) +*/ + + val dmz = D::class.constructors.single().call(-0.0) + val dpz = D::class.constructors.single().call(+0.0) + val fmz = F::class.constructors.single().call(-0.0f) + val fpz = F::class.constructors.single().call(+0.0f) + check(::dMinusZero.annotations.single() as D, dmz) + check(::dPlusZero.annotations.single() as D, dpz) + check(::fMinusZero.annotations.single() as F, fmz) + check(::fPlusZero.annotations.single() as F, fpz) + + checkNot(dmz, dpz) + checkNot(fmz, fpz) + checkNot(dmz, fmz) + checkNot(dpz, fpz) + + return "OK" +} diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/parameterNamedEquals.kt b/compiler/testData/codegen/box/reflection/createAnnotation/parameterNamedEquals.kt new file mode 100644 index 00000000000..f5f21d2ebbb --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/parameterNamedEquals.kt @@ -0,0 +1,15 @@ +// WITH_REFLECT + +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +annotation class Anno(val equals: Boolean) + +fun box(): String { + val t = Anno::class.constructors.single().call(true) + val f = Anno::class.constructors.single().call(false) + assertEquals(true, t.equals) + assertEquals(false, f.equals) + assertNotEquals(t, f) + return "OK" +} diff --git a/compiler/testData/codegen/box/reflection/createAnnotation/primitivesAndArrays.kt b/compiler/testData/codegen/box/reflection/createAnnotation/primitivesAndArrays.kt new file mode 100644 index 00000000000..0d7def0cce6 --- /dev/null +++ b/compiler/testData/codegen/box/reflection/createAnnotation/primitivesAndArrays.kt @@ -0,0 +1,82 @@ +// WITH_REFLECT + +import kotlin.test.assertEquals + +annotation class Anno( + val b: Byte, + val c: Char, + val d: Double, + val f: Float, + val i: Int, + val j: Long, + val s: Short, + val z: Boolean, + val ba: ByteArray, + val ca: CharArray, + val da: DoubleArray, + val fa: FloatArray, + val ia: IntArray, + val ja: LongArray, + val sa: ShortArray, + val za: BooleanArray, + val str: String, + val stra: Array +) + +@Anno( + 1.toByte(), + 'x', + 3.14, + -2.72f, + 42424242, + 239239239239239L, + 42.toShort(), + true, + byteArrayOf((-1).toByte()), + charArrayOf('y'), + doubleArrayOf(-3.14159), + floatArrayOf(2.7218f), + intArrayOf(424242), + longArrayOf(239239239239L), + shortArrayOf((-43).toShort()), + booleanArrayOf(false, true), + "lol", + arrayOf("rofl") +) +fun foo() {} + +fun box(): String { + // Construct an annotation with exactly the same parameters, check that the proxy created by Kotlin and by Java reflection are the same and have the same hash code + val a1 = Anno::class.constructors.single().call( + 1.toByte(), + 'x', + 3.14, + -2.72f, + 42424242, + 239239239239239L, + 42.toShort(), + true, + byteArrayOf((-1).toByte()), + charArrayOf('y'), + doubleArrayOf(-3.14159), + floatArrayOf(2.7218f), + intArrayOf(424242), + longArrayOf(239239239239L), + shortArrayOf((-43).toShort()), + booleanArrayOf(false, true), + "lol", + arrayOf("rofl") + ) + + val a2 = ::foo.annotations.single() as Anno + + assertEquals(a1, a2) + assertEquals(a2, a1) + assertEquals(a1.hashCode(), a2.hashCode()) + + assertEquals("@Anno(b=1, c=x, d=3.14, f=-2.72, i=42424242, j=239239239239239, s=42, z=true, " + + "ba=[-1], ca=[y], da=[-3.14159], fa=[2.7218], ia=[424242], ja=[239239239239], sa=[-43], za=[false, true], " + + "str=lol, stra=[rofl])", a1.toString()) + + return "OK" +} diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index 4ef341ab7f3..7ca15394dca 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -12150,6 +12150,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/reflection/createAnnotation"), Pattern.compile("^(.+)\\.kt$"), true); } + @TestMetadata("annotationType.kt") + public void testAnnotationType() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/annotationType.kt"); + doTest(fileName); + } + @TestMetadata("arrayOfKClasses.kt") public void testArrayOfKClasses() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/arrayOfKClasses.kt"); @@ -12185,6 +12191,36 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/createJdkAnnotationInstance.kt"); doTest(fileName); } + + @TestMetadata("enumKClassAnnotation.kt") + public void testEnumKClassAnnotation() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/enumKClassAnnotation.kt"); + doTest(fileName); + } + + @TestMetadata("equalsHashCodeToString.kt") + public void testEqualsHashCodeToString() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/equalsHashCodeToString.kt"); + doTest(fileName); + } + + @TestMetadata("floatingPointParameters.kt") + public void testFloatingPointParameters() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/floatingPointParameters.kt"); + doTest(fileName); + } + + @TestMetadata("parameterNamedEquals.kt") + public void testParameterNamedEquals() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/parameterNamedEquals.kt"); + doTest(fileName); + } + + @TestMetadata("primitivesAndArrays.kt") + public void testPrimitivesAndArrays() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/reflection/createAnnotation/primitivesAndArrays.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 506df4b51f2..5e9d042aed4 100644 --- a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/AnnotationConstructorCaller.kt +++ b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/AnnotationConstructorCaller.kt @@ -18,6 +18,7 @@ package kotlin.reflect.jvm.internal import org.jetbrains.kotlin.load.java.structure.reflect.wrapperByPrimitive import java.lang.reflect.Proxy +import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KotlinReflectionInternalError import java.lang.reflect.Method as ReflectMethod @@ -27,7 +28,7 @@ internal class AnnotationConstructorCaller( private val parameterNames: List, private val callMode: CallMode, origin: Origin, - methods: List = parameterNames.map { name -> jClass.getDeclaredMethod(name) } + private val methods: List = parameterNames.map { name -> jClass.getDeclaredMethod(name) } ) : FunctionCaller( null, jClass, null, methods.map { it.genericReturnType }.toTypedArray() ) { @@ -61,7 +62,7 @@ internal class AnnotationConstructorCaller( value ?: throwIllegalArgumentType(index, parameterNames[index], erasedParameterTypes[index]) } - return createAnnotationInstance(jClass, parameterNames.zip(values).toMap()) + return createAnnotationInstance(jClass, methods, parameterNames.zip(values).toMap()) } } @@ -101,9 +102,79 @@ private fun throwIllegalArgumentType(index: Int, name: String, expectedJvmType: throw IllegalArgumentException("Argument #$index $name is not of the required type $typeString") } -private fun createAnnotationInstance(annotationClass: Class<*>, values: Map): Any { +private fun createAnnotationInstance(annotationClass: Class<*>, methods: List, values: Map): Any { + fun equals(other: Any?): Boolean = + (other as? Annotation)?.annotationClass?.java == annotationClass && + methods.all { method -> + val ours = values[method.name] + val theirs = method(other) + when (ours) { + is BooleanArray -> Arrays.equals(ours, theirs as BooleanArray) + is CharArray -> Arrays.equals(ours, theirs as CharArray) + is ByteArray -> Arrays.equals(ours, theirs as ByteArray) + is ShortArray -> Arrays.equals(ours, theirs as ShortArray) + is IntArray -> Arrays.equals(ours, theirs as IntArray) + is FloatArray -> Arrays.equals(ours, theirs as FloatArray) + is LongArray -> Arrays.equals(ours, theirs as LongArray) + is DoubleArray -> Arrays.equals(ours, theirs as DoubleArray) + is Array<*> -> Arrays.equals(ours, theirs as Array<*>) + else -> ours == theirs + } + } + + val hashCode by lazy { + values.entries.sumBy { entry -> + val (key, value) = entry + val valueHash = when (value) { + is BooleanArray -> Arrays.hashCode(value) + is CharArray -> Arrays.hashCode(value) + is ByteArray -> Arrays.hashCode(value) + is ShortArray -> Arrays.hashCode(value) + is IntArray -> Arrays.hashCode(value) + is FloatArray -> Arrays.hashCode(value) + is LongArray -> Arrays.hashCode(value) + is DoubleArray -> Arrays.hashCode(value) + is Array<*> -> Arrays.hashCode(value) + else -> value.hashCode() + } + 127 * key.hashCode() xor valueHash + } + } + + val toString by lazy { + buildString { + append('@') + append(annotationClass.canonicalName) + values.entries.joinTo(this, separator = ", ", prefix = "(", postfix = ")") { entry -> + val (key, value) = entry + val valueString = when (value) { + is BooleanArray -> Arrays.toString(value) + is CharArray -> Arrays.toString(value) + is ByteArray -> Arrays.toString(value) + is ShortArray -> Arrays.toString(value) + is IntArray -> Arrays.toString(value) + is FloatArray -> Arrays.toString(value) + is LongArray -> Arrays.toString(value) + is DoubleArray -> Arrays.toString(value) + is Array<*> -> Arrays.toString(value) + else -> value.toString() + } + "$key=$valueString" + } + } + } + return Proxy.newProxyInstance(annotationClass.classLoader, arrayOf(annotationClass)) { proxy, method, args -> - // TODO: support equals, hashCode, toString, annotationType - values[method.name] ?: throw KotlinReflectionInternalError("Method is not supported: $method (args: ${args.orEmpty().toList()})") + val name = method.name + when (name) { + "annotationType" -> annotationClass + "toString" -> toString + "hashCode" -> hashCode + else -> when { + name == "equals" && args?.size == 1 -> equals(args.single()) + values.containsKey(name) -> values[name] + else -> throw KotlinReflectionInternalError("Method is not supported: $method (args: ${args.orEmpty().toList()})") + } + } } }