Support equals, hashCode, toString for annotations instantiated with reflection

#KT-13106 Fixed
This commit is contained in:
Alexander Udalov
2016-09-15 16:45:17 +03:00
parent 525937252d
commit 621c6691d0
9 changed files with 377 additions and 5 deletions
@@ -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"
}
@@ -8,5 +8,6 @@ annotation class Anno(val klasses: Array<KClass<*>> = 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<KClass<*>>).toList() /* TODO: KT-9453 */)
assertEquals("@Anno(klasses=[class java.lang.String, int])", anno.toString())
return "OK"
}
@@ -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<DeprecationLevel>,
val klasses: Array<KClass<*>>,
val foos: Array<Foo>
)
@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"
}
@@ -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<A>().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<B>().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"
}
@@ -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"
}
@@ -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"
}
@@ -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<String>
)
@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"
}
@@ -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")
@@ -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<String>,
private val callMode: CallMode,
origin: Origin,
methods: List<ReflectMethod> = parameterNames.map { name -> jClass.getDeclaredMethod(name) }
private val methods: List<ReflectMethod> = parameterNames.map { name -> jClass.getDeclaredMethod(name) }
) : FunctionCaller<Nothing?>(
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<String, Any>): Any {
private fun createAnnotationInstance(annotationClass: Class<*>, methods: List<ReflectMethod>, values: Map<String, Any>): 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()})")
}
}
}
}