diff --git a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KTypeImpl.kt b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KTypeImpl.kt index 3397cd2644c..855bfb52738 100644 --- a/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KTypeImpl.kt +++ b/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KTypeImpl.kt @@ -19,6 +19,9 @@ package kotlin.reflect.jvm.internal import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.TypeAliasDescriptor import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.descriptors.runtime.structure.createArrayType +import org.jetbrains.kotlin.descriptors.runtime.structure.parameterizedTypeArguments +import org.jetbrains.kotlin.descriptors.runtime.structure.primitiveByWrapper import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.TypeUtils import org.jetbrains.kotlin.types.Variance @@ -27,19 +30,16 @@ import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.lang.reflect.WildcardType import kotlin.LazyThreadSafetyMode.PUBLICATION +import kotlin.jvm.internal.KTypeBase import kotlin.reflect.KClassifier -import kotlin.reflect.KType import kotlin.reflect.KTypeProjection -import org.jetbrains.kotlin.descriptors.runtime.structure.createArrayType -import org.jetbrains.kotlin.descriptors.runtime.structure.parameterizedTypeArguments -import org.jetbrains.kotlin.descriptors.runtime.structure.primitiveByWrapper import kotlin.reflect.jvm.jvmErasure internal class KTypeImpl( val type: KotlinType, computeJavaType: () -> Type -) : KType { - internal val javaType: Type by ReflectProperties.lazySoft(computeJavaType) +) : KTypeBase { + override val javaType: Type by ReflectProperties.lazySoft(computeJavaType) override val classifier: KClassifier? by ReflectProperties.lazySoft { convert(type) } diff --git a/libraries/stdlib/jvm/build.gradle b/libraries/stdlib/jvm/build.gradle index 2783cd37f7b..bf686592346 100644 --- a/libraries/stdlib/jvm/build.gradle +++ b/libraries/stdlib/jvm/build.gradle @@ -132,6 +132,8 @@ compileTestKotlin { "-Xopt-in=kotlin.ExperimentalUnsignedTypes", "-Xopt-in=kotlin.ExperimentalStdlibApi", ] + // This is needed for JavaTypeTest; typeOf for non-reified type parameters doesn't work otherwise, for implementation reasons. + freeCompilerArgs.remove("-Xno-optimized-callable-references") } } diff --git a/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/KTypeBase.kt b/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/KTypeBase.kt new file mode 100644 index 00000000000..d611af88d08 --- /dev/null +++ b/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/KTypeBase.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.jvm.internal + +import java.lang.reflect.Type +import kotlin.reflect.KType + +@SinceKotlin("1.4") +public interface KTypeBase : KType { + val javaType: Type? +} diff --git a/libraries/stdlib/jvm/src/kotlin/reflect/TypesJVM.kt b/libraries/stdlib/jvm/src/kotlin/reflect/TypesJVM.kt new file mode 100644 index 00000000000..8f2fb559411 --- /dev/null +++ b/libraries/stdlib/jvm/src/kotlin/reflect/TypesJVM.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.reflect + +import java.lang.reflect.* +import kotlin.internal.LowPriorityInOverloadResolution +import kotlin.jvm.internal.KTypeBase + +/** + * Returns a Java [Type] instance corresponding to the given Kotlin type. + * + * This function is experimental because not all the features are supported yet, and behavior might change in corner cases. + * In particular, the following is not supported correctly or at all: + * - declaration-site variance + * - variance of types annotated with [JvmSuppressWildcards] + * - obtaining the containing declaration of a type parameter ([TypeVariable.getGenericDeclaration]) + */ +@SinceKotlin("1.4") +@ExperimentalStdlibApi +@LowPriorityInOverloadResolution // To make non-experimental kotlin.reflect.full.javaType always win in overload resolution +public val KType.javaType: Type + get() { + if (this is KTypeBase) { + // Use kotlin-reflect implementation for types which are position-dependent, e.g. "Unit" in a callable's return type. + javaType?.let { return it } + } + + return computeJavaType() + } + +@ExperimentalStdlibApi +private fun KType.computeJavaType(forceWrapper: Boolean = false): Type { + when (val classifier = classifier) { + is KTypeParameter -> return TypeVariableImpl(classifier) + is KClass<*> -> { + val jClass = if (forceWrapper) classifier.javaObjectType else classifier.java + val arguments = arguments + if (arguments.isEmpty()) return jClass + + if (jClass.isArray) { + if (jClass.componentType.isPrimitive) return jClass + + val (variance, elementType) = arguments.singleOrNull() + ?: throw IllegalArgumentException("kotlin.Array must have exactly one type argument: $this") + return when (variance) { + // Array is always erased to Object[], and Array<*> is Object[]. + null, KVariance.IN -> jClass + KVariance.INVARIANT, KVariance.OUT -> { + val javaElementType = elementType!!.computeJavaType() + if (javaElementType is Class<*>) jClass else GenericArrayTypeImpl(javaElementType) + } + } + } + + return createPossiblyInnerType(jClass, arguments) + } + else -> throw UnsupportedOperationException("Unsupported type classifier: $this") + } +} + +@ExperimentalStdlibApi +private fun createPossiblyInnerType(jClass: Class<*>, arguments: List): Type { + val ownerClass = jClass.declaringClass + ?: return ParameterizedTypeImpl(jClass, null, arguments.map(KTypeProjection::javaType)) + if (Modifier.isStatic(jClass.modifiers)) + return ParameterizedTypeImpl(jClass, ownerClass, arguments.map(KTypeProjection::javaType)) + + val n = jClass.typeParameters.size + return ParameterizedTypeImpl( + jClass, + createPossiblyInnerType(ownerClass, arguments.subList(n, arguments.size)), + arguments.subList(0, n).map(KTypeProjection::javaType) + ) +} + +@ExperimentalStdlibApi +private val KTypeProjection.javaType: Type + get() { + val variance = variance ?: return WildcardTypeImpl.STAR + val type = type!! + // TODO: JvmSuppressWildcards + return when (variance) { + KVariance.INVARIANT -> { + // TODO: declaration-site variance + type.computeJavaType(forceWrapper = true) + } + KVariance.IN -> WildcardTypeImpl(null, type.computeJavaType(forceWrapper = true)) + KVariance.OUT -> WildcardTypeImpl(type.computeJavaType(forceWrapper = true), null) + } + } + +@ExperimentalStdlibApi +private interface TypeImpl : Type { + // This is a copy of [Type.getTypeName] which is present on JDK 8+. + fun getTypeName(): String +} + +@ExperimentalStdlibApi +private class TypeVariableImpl(private val typeParameter: KTypeParameter) : TypeVariable, TypeImpl { + override fun getName(): String = typeParameter.name + + override fun getGenericDeclaration(): GenericDeclaration = + TODO("getGenericDeclaration() is not yet supported for type variables created from KType: $typeParameter") + + override fun getBounds(): Array = typeParameter.upperBounds.map { it.computeJavaType(forceWrapper = true) }.toTypedArray() + + override fun getTypeName(): String = name + + override fun equals(other: Any?): Boolean = + other is TypeVariable<*> && name == other.name && genericDeclaration == other.genericDeclaration + + override fun hashCode(): Int = + name.hashCode() xor genericDeclaration.hashCode() + + override fun toString(): String = getTypeName() +} + +@ExperimentalStdlibApi +private class GenericArrayTypeImpl(private val elementType: Type) : GenericArrayType, TypeImpl { + override fun getGenericComponentType(): Type = elementType + + override fun getTypeName(): String = "${typeToString(elementType)}[]" + + override fun equals(other: Any?): Boolean = other is GenericArrayType && genericComponentType == other.genericComponentType + + override fun hashCode(): Int = genericComponentType.hashCode() + + override fun toString(): String = getTypeName() +} + +@ExperimentalStdlibApi +private class WildcardTypeImpl(private val upperBound: Type?, private val lowerBound: Type?) : WildcardType, TypeImpl { + override fun getUpperBounds(): Array = + arrayOf(upperBound ?: Any::class.java) + + override fun getLowerBounds(): Array = + if (lowerBound == null) emptyArray() else arrayOf(lowerBound) + + override fun getTypeName(): String = when { + lowerBound != null -> "? super ${typeToString(lowerBound)}" + upperBound != null && upperBound != Any::class.java -> "? extends ${typeToString(upperBound)}" + else -> "?" + } + + override fun equals(other: Any?): Boolean = + other is WildcardType && upperBounds.contentEquals(other.upperBounds) && lowerBounds.contentEquals(other.lowerBounds) + + override fun hashCode(): Int = + upperBounds.contentHashCode() xor lowerBounds.contentHashCode() + + override fun toString(): String = getTypeName() + + companion object { + val STAR = WildcardTypeImpl(null, null) + } +} + +@ExperimentalStdlibApi +private class ParameterizedTypeImpl( + private val rawType: Class<*>, + private val ownerType: Type?, + typeArguments: List +) : ParameterizedType, TypeImpl { + private val typeArguments = typeArguments.toTypedArray() + + override fun getRawType(): Type = rawType + + override fun getOwnerType(): Type? = ownerType + + override fun getActualTypeArguments(): Array = typeArguments + + override fun getTypeName(): String = buildString { + if (ownerType != null) { + append(typeToString(ownerType)) + append("$") + append(rawType.simpleName) + } else { + append(typeToString(rawType)) + } + + if (typeArguments.isNotEmpty()) { + typeArguments.joinTo(this, prefix = "<", postfix = ">", transform = ::typeToString) + } + } + + override fun equals(other: Any?): Boolean = + other is ParameterizedType && rawType == other.rawType && ownerType == other.ownerType && + actualTypeArguments.contentEquals(other.actualTypeArguments) + + override fun hashCode(): Int = + rawType.hashCode() xor ownerType.hashCode() xor actualTypeArguments.contentHashCode() + + override fun toString(): String = getTypeName() +} + +// TODO: use getTypeName instead of this, when this code is migrated to JDK 8. +private fun typeToString(type: Type): String = + if (type is Class<*>) { + if (type.isArray) { + val unwrap = generateSequence(type, Class<*>::getComponentType) + unwrap.last().name + "[]".repeat(unwrap.count()) + } else type.name + } else type.toString() diff --git a/libraries/stdlib/jvm/test/reflection/JavaTypeTest.kt b/libraries/stdlib/jvm/test/reflection/JavaTypeTest.kt new file mode 100644 index 00000000000..46b163293ba --- /dev/null +++ b/libraries/stdlib/jvm/test/reflection/JavaTypeTest.kt @@ -0,0 +1,377 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package test.reflection + +import org.junit.Test +import java.lang.reflect.* +import kotlin.internal.OnlyInputTypes +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +@Suppress( + "unused", "UNUSED_VARIABLE", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE", "RedundantInnerClassModifier", "NAME_SHADOWING", + "FINAL_UPPER_BOUND" +) +class JavaTypeTest { + @Test + fun primitives() { + assertEquals(Boolean::class.java, javaTypeOf()) + assertEquals(Char::class.java, javaTypeOf()) + assertEquals(Byte::class.java, javaTypeOf()) + assertEquals(Short::class.java, javaTypeOf()) + assertEquals(Int::class.java, javaTypeOf()) + assertEquals(Float::class.java, javaTypeOf()) + assertEquals(Long::class.java, javaTypeOf()) + assertEquals(Double::class.java, javaTypeOf()) + } + + @Test + fun primitiveArrays() { + assertEquals(BooleanArray::class.java, javaTypeOf()) + assertEquals(CharArray::class.java, javaTypeOf()) + assertEquals(ByteArray::class.java, javaTypeOf()) + assertEquals(ShortArray::class.java, javaTypeOf()) + assertEquals(IntArray::class.java, javaTypeOf()) + assertEquals(FloatArray::class.java, javaTypeOf()) + assertEquals(LongArray::class.java, javaTypeOf()) + assertEquals(DoubleArray::class.java, javaTypeOf()) + } + + @Test + fun builtinClasses() { + assertEquals(String::class.java, javaTypeOf()) + assertEquals(Number::class.java, javaTypeOf()) + assertEquals(Unit::class.java, javaTypeOf()) + assertEquals(Annotation::class.java, javaTypeOf()) + assertEquals(Cloneable::class.java, javaTypeOf()) + assertEquals(String.Companion::class.java, javaTypeOf()) + assertEquals(Deprecated::class.java, javaTypeOf()) + + assertEquals(Void::class.java, javaTypeOf()) + } + + @Test + fun enumOfStar() { + parameterized(javaTypeOf>()) { type -> + star(type.actualTypeArguments.single()) + assertEquals(null, type.ownerType) + assertEquals(Enum::class.java, type.rawType) + assertEquals("java.lang.Enum", type.typeName) + } + + assertEqualsAndHashCode(javaTypeOf>(), javaTypeOf>()) + } + + @Test + fun recursiveEnumOfStar() { + parameterized(javaTypeOf>>()) { type -> + wildcardExtends(type.actualTypeArguments.single()) { starEnum -> + parameterized(starEnum) { starEnumType -> + star(starEnumType.actualTypeArguments.single()) + assertEquals(Enum::class.java, starEnumType.rawType) + } + } + assertEquals(Enum::class.java, type.rawType) + assertEquals("java.lang.Enum>", type.typeName) + } + + assertEqualsAndHashCode(javaTypeOf>>(), javaTypeOf>>()) + } + + @Test + fun listOfString() { + parameterized(javaTypeOf>()) { type -> + assertEquals(String::class.java, type.actualTypeArguments.single()) + assertEquals(null, type.ownerType) + assertEquals(List::class.java, type.rawType) + assertEquals("java.util.List", type.typeName) + } + parameterized(javaTypeOf>()) { type -> + assertEquals(String::class.java, type.actualTypeArguments.single()) + assertEquals(null, type.ownerType) + assertEquals(MutableList::class.java, type.rawType) + assertEquals("java.util.List", type.typeName) + } + + assertEqualsAndHashCode(javaTypeOf>(), javaTypeOf>()) + } + + @Test + fun arrays() { + assertEquals(Array::class.java, javaTypeOf>()) + assertEquals(Array>::class.java, javaTypeOf>>()) + assertEquals(Array::class.java, javaTypeOf>()) + + assertEquals(Array::class.java, javaTypeOf>()) + assertEquals(Array::class.java, javaTypeOf>>()) + + assertEquals(Array::class.java, javaTypeOf>()) + assertEquals(Array>::class.java, javaTypeOf>>()) + + assertEquals(Array::class.java, javaTypeOf>()) + + genericArray(javaTypeOf>>()) { type -> + parameterized(type.genericComponentType) { argument -> + assertEquals(String::class.java, argument.actualTypeArguments.single()) + assertEquals(Set::class.java, argument.rawType) + } + assertEquals("java.util.Set[]", type.typeName) + } + + assertEqualsAndHashCode(javaTypeOf>>(), javaTypeOf>>()) + assertNotEquals(javaTypeOf>>(), javaTypeOf>>()) + assertNotEquals(javaTypeOf>>(), javaTypeOf>>()) + assertNotEquals(javaTypeOf>>(), javaTypeOf>>()) + } + + @Test + fun functionTypeVariables() where U2 : Number, U2 : Comparable<*> { + val m = this::class.java.declaredMethods.single { it.name == "functionTypeVariables" } + val tvs = m.typeParameters + parameterized(javaTypeOf>()) { type -> + typeVariable(type.actualTypeArguments.single()) { tv -> + assertEquals("U1", tv.name) + assertEquals("U1", tv.typeName) + assertEquals(listOf(Any::class.java), tv.bounds.toList()) + + // assertEquals(m, tv.genericDeclaration) + // assertEqualsAndHashCode(tvs[0], tv) + } + } + lateinit var u2: TypeVariable<*> + parameterized(javaTypeOf>()) { type -> + typeVariable(type.actualTypeArguments.single()) { tv -> + assertEquals("U2", tv.name) + val bounds = tv.bounds.toList() + assertEquals(2, bounds.size) + assertEquals(Number::class.java, bounds[0]) + parameterized(bounds[1]) { comparable -> + assertEquals(Comparable::class.java, comparable.rawType) + star(comparable.actualTypeArguments.single()) + } + u2 = tv + + // assertEquals(m, tv.genericDeclaration) + // assertEqualsAndHashCode(tvs[1], tv) + } + } + parameterized(javaTypeOf>()) { type -> + typeVariable(type.actualTypeArguments.single()) { tv -> + assertEquals("U3", tv.name) + // assertEquals(listOf(u2), tv.bounds.toList()) + + // assertEquals(m, tv.genericDeclaration) + // assertEqualsAndHashCode(tvs[2], tv) + } + } + } + + private inner class T1 where V2 : Number, V2 : Comparable<*> { + fun setOfV1(): Type = javaTypeOf>() + fun setOfV2(): Type = javaTypeOf>() + fun setOfV3(): Type = javaTypeOf>() + } + + @Test + fun classTypeVariables() { + val t1 = T1() + val tvs = T1::class.java.typeParameters + parameterized(t1.setOfV1()) { type -> + typeVariable(type.actualTypeArguments.single()) { tv -> + assertEquals("V1", tv.name) + assertEquals(listOf(Any::class.java), tv.bounds.toList()) + assertEquals("V1", tv.typeName) + + // assertEquals(T1::class.java, tv.genericDeclaration) + // assertEqualsAndHashCode(tvs[0], tv) + } + } + lateinit var v2: TypeVariable<*> + parameterized(t1.setOfV2()) { type -> + typeVariable(type.actualTypeArguments.single()) { tv -> + assertEquals("V2", tv.name) + val bounds = tv.bounds.toList() + assertEquals(2, bounds.size) + assertEquals(Number::class.java, bounds[0]) + parameterized(bounds[1]) { comparable -> + assertEquals(Comparable::class.java, comparable.rawType) + star(comparable.actualTypeArguments.single()) + } + v2 = tv + + // assertEquals(T1::class.java, tv.genericDeclaration) + // assertEqualsAndHashCode(tvs[1], tv) + } + } + parameterized(t1.setOfV3()) { type -> + typeVariable(type.actualTypeArguments.single()) { tv -> + assertEquals("V3", tv.name) + // assertEquals(listOf(v2), tv.bounds.toList()) + + // assertEquals(T1::class.java, tv.genericDeclaration) + // assertEqualsAndHashCode(tvs[2], tv) + } + } + } + + private class T2 + + @Test + fun useSiteVariance() { + parameterized(javaTypeOf>()) { type -> + star(type.actualTypeArguments.single()) + } + parameterized(javaTypeOf>()) { type -> + wildcardExtends(type.actualTypeArguments.single()) { bound -> + assertEquals(CharSequence::class.java, bound) + } + } + parameterized(javaTypeOf>()) { type -> + wildcardSuper(type.actualTypeArguments.single()) { bound -> + assertEquals(CharSequence::class.java, bound) + } + } + + assertEqualsAndHashCode(javaTypeOf>(), javaTypeOf>()) + assertEqualsAndHashCode(javaTypeOf>(), javaTypeOf>()) + assertEqualsAndHashCode(javaTypeOf>(), javaTypeOf>()) + + assertNotEquals(javaTypeOf>(), javaTypeOf>()) + assertNotEquals(javaTypeOf>(), javaTypeOf>()) + } + + private class T3 { + class Nested + + inner class Inner { + inner class NonGeneric { + inner class DeepInner + } + } + } + + @Test + fun nestedTypes() { + val nestedGenericType = javaTypeOf>() + parameterized(nestedGenericType) { type -> + assertEquals(listOf(IntRange::class.java), type.actualTypeArguments.toList()) + assertEquals(T3::class.java, type.ownerType) + assertEquals("reflection.JavaTypeTest\$T3\$Nested", type.typeName) + } + assertEqualsAndHashCode(javaTypeOf>(), javaTypeOf>()) + assertNotEquals(javaTypeOf>(), javaTypeOf>()) + + + val innerGenericType = javaTypeOf.Inner>() + parameterized(innerGenericType) { type -> + assertEquals(listOf(IntRange::class.java), type.actualTypeArguments.toList()) + parameterized(type.ownerType) { ownerType -> + assertEquals(listOf(String::class.java, Unit::class.java), ownerType.actualTypeArguments.toList()) + } + assertEquals("reflection.JavaTypeTest\$T3\$Inner", type.typeName) + } + assertEqualsAndHashCode(javaTypeOf.Inner>(), javaTypeOf.Inner>()) + assertNotEquals(javaTypeOf.Inner>(), javaTypeOf.Inner>()) + assertNotEquals(javaTypeOf.Inner>(), javaTypeOf.Inner>()) + + + val deepInnerType = javaTypeOf.Inner.NonGeneric.DeepInner>() + parameterized(deepInnerType) { deepInnerType -> + assertEquals(listOf(Byte::class.javaObjectType, Short::class.javaObjectType), deepInnerType.actualTypeArguments.toList()) + parameterized(deepInnerType.ownerType) { nonGenericType -> + assertEquals(emptyList(), nonGenericType.actualTypeArguments.toList()) + parameterized(nonGenericType.ownerType) { innerType -> + assertEquals(listOf(Char::class.javaObjectType), innerType.actualTypeArguments.toList()) + parameterized(innerType.ownerType) { t3 -> + assertEquals(listOf(Any::class.java, Int::class.javaObjectType), t3.actualTypeArguments.toList()) + assertEquals(JavaTypeTest::class.java, t3.ownerType) + } + } + } + assertEquals( + "reflection.JavaTypeTest\$T3\$Inner" + + "\$NonGeneric\$DeepInner", deepInnerType.typeName + ) + } + } + + private inner class T4 { + fun setOfI(): Type = javaTypeOf>() + } + + @Test + fun primitiveUpperBound() { + parameterized(T4().setOfI()) { setOfI -> + typeVariable(setOfI.actualTypeArguments.single()) { i -> + val bound = i.bounds.single() + assertEquals(Int::class.javaObjectType, bound) + assertNotEquals(Int::class.javaPrimitiveType, bound) + } + } + } + + // ----- + + private inline fun javaTypeOf(): Type = + typeOf().javaType + + private fun <@OnlyInputTypes T> assertEqualsAndHashCode(expected: T, actual: T, message: String? = null) { + assertEquals(expected, actual, message) + assertEquals(actual, expected, message) + assertEquals(expected.hashCode(), actual.hashCode(), message) + } + + private inline fun checkIsType(type: Type, block: (T) -> Unit) { + assertTrue(type is T, "Type is not ${T::class.java.simpleName}: $type (${type::class.java.name})") + block(type) + } + + private val Type.typeName: String + // Calling getTypeName() via reflection because the interface method Type.getTypeName has only appeared in JDK 8. + get() = (this::class.java.getDeclaredMethod("getTypeName").apply { isAccessible = true })(this) as String + + // A + private fun parameterized(type: Type, block: (ParameterizedType) -> Unit): Unit = checkIsType(type, block) + + // ?, ? extends ..., ? super ... + private fun wildcard(type: Type, block: (WildcardType) -> Unit): Unit = checkIsType(type, block) + + // Array> + private fun genericArray(type: Type, block: (GenericArrayType) -> Unit): Unit = checkIsType(type, block) + + // T + private fun typeVariable(type: Type, block: (TypeVariable<*>) -> Unit): Unit = checkIsType(type, block) + + // ? extends ... + private fun wildcardExtends(type: Type, block: (upperBound: Type) -> Unit) { + wildcard(type) { argument -> + assertEquals(emptyList(), argument.lowerBounds.toList()) + val bound = argument.upperBounds.singleOrNull() ?: fail("Type is not an extends-wildcard: $type (${type::class.java.name})") + block(bound) + } + } + + // ? super ... + private fun wildcardSuper(type: Type, block: (lowerBound: Type) -> Unit) { + wildcard(type) { argument -> + assertEquals(listOf(Any::class.java), argument.upperBounds.toList()) + val bound = argument.lowerBounds.singleOrNull() ?: fail("Type is not a super-wildcard: $type (${type::class.java.name})") + block(bound) + } + } + + // * + private fun star(type: Type) { + wildcardExtends(type) { bound -> + assertEquals(Any::class.java, bound) + } + assertEquals("?", type.typeName) + } +}