Skip to content

Commit 9873b97

Browse files
committed
Use isSubtypeOf for generic wrapper type checks
- Add ParameterizedType.isSubtypeOf and use it in TypeClassMatcher in place of GenericType.isTypeAssignableFromRawClass - Remove the isListType helpers in favor of the same check - Rename mostSpecificType to containingType and document unwrapGenericType
1 parent dffbc34 commit 9873b97

3 files changed

Lines changed: 35 additions & 37 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/GenericType.kt‎

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,58 +12,61 @@ import java.lang.reflect.WildcardType
1212
/**
1313
* @author Andrew Potter
1414
*/
15-
internal class GenericType(private val mostSpecificType: JavaType, private val options: SchemaParserOptions) {
15+
internal class GenericType(private val containingType: JavaType, private val options: SchemaParserOptions) {
1616

17-
fun isTypeAssignableFromRawClass(type: ParameterizedType, clazz: Class<*>) =
18-
clazz.isAssignableFrom(getRawClass(type.rawType))
17+
fun getRawClass() = getRawClass(containingType)
1918

20-
fun getRawClass() = getRawClass(mostSpecificType)
19+
fun getRawClass(type: JavaType): Class<*> = TypeUtils.getRawType(type, containingType)
2120

22-
fun getRawClass(type: JavaType): Class<*> = TypeUtils.getRawType(type, mostSpecificType)
23-
24-
fun isAssignableFrom(type: JavaType) = TypeUtils.isAssignable(type, mostSpecificType)
21+
fun isAssignableFrom(type: JavaType) = TypeUtils.isAssignable(type, containingType)
2522

2623
/**
27-
* Unwrap certain Java types to find the "real" class.
24+
* Turns a type as declared in the source (a method's return or parameter type, or a field's type) into the type
25+
* the GraphQL type is matched against:
26+
* - type variables are resolved relative to the containing type,
27+
* e.g. `T` of `AbstractItem<T>.getId()` becomes `Long` for `class Item : AbstractItem<Long>()`
28+
* - generic wrappers are replaced by their wrapped type, e.g. `CompletableFuture<Foo>` becomes `Foo`
29+
* - wildcards are replaced by their upper bound, e.g. `? extends Foo` becomes `Foo`
30+
* - primitives are replaced by their boxed class, e.g. `int` becomes `Integer`
2831
*/
2932
fun unwrapGenericType(javaType: JavaType): JavaType {
30-
return when (val type = replaceTypeVariable(javaType)) {
33+
return when (val type = resolveTypeVariables(javaType)) {
3134
is ParameterizedType -> {
3235
val rawType = type.rawType
33-
val genericType = options.genericWrappers.find { it.type == rawType }
36+
val wrapper = options.genericWrappers.find { it.type == rawType }
3437
?: return type
3538

3639
val typeArguments = type.actualTypeArguments
37-
if (typeArguments.size <= genericType.index) {
38-
throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${genericType.index}!")
40+
if (typeArguments.size <= wrapper.index) {
41+
throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${wrapper.index}!")
3942
}
4043

41-
val unwrapsTo = genericType.schemaWrapper.invoke(typeArguments[genericType.index])
44+
val unwrapsTo = wrapper.schemaWrapper.invoke(typeArguments[wrapper.index])
4245
unwrapGenericType(unwrapsTo)
4346
}
44-
is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' relative to ${TypeUtils.toString(mostSpecificType)}")
47+
is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' relative to ${TypeUtils.toString(containingType)}")
4548
is WildcardType -> type.upperBounds.firstOrNull()
4649
?: error("Unable to unwrap type, wildcard has no upper bound: $type")
4750
is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type
4851
else -> error("Unable to unwrap type: $type")
4952
}
5053
}
5154

52-
private fun replaceTypeVariable(type: JavaType, resolving: Set<TypeVariable<*>> = emptySet()): JavaType {
55+
private fun resolveTypeVariables(type: JavaType, resolving: Set<TypeVariable<*>> = emptySet()): JavaType {
5356
return when (type) {
5457
is ParameterizedType -> {
55-
val actualTypeArguments = type.actualTypeArguments.map { replaceTypeVariable(it, resolving) }.toTypedArray()
56-
ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType?.let { replaceTypeVariable(it, resolving) })
58+
val actualTypeArguments = type.actualTypeArguments.map { resolveTypeVariables(it, resolving) }.toTypedArray()
59+
ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType?.let { resolveTypeVariables(it, resolving) })
5760
}
5861
is WildcardType -> TypeUtils.wildcardType()
59-
.withUpperBounds(*type.upperBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray())
60-
.withLowerBounds(*type.lowerBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray())
62+
.withUpperBounds(*type.upperBounds.map { resolveTypeVariables(it, resolving) }.toTypedArray())
63+
.withLowerBounds(*type.lowerBounds.map { resolveTypeVariables(it, resolving) }.toTypedArray())
6164
.build()
6265
is ResolvedType -> {
6366
if (type.typeParameters.isEmpty()) {
6467
type.erasedType
6568
} else {
66-
val actualTypeArguments = type.typeParameters.map { replaceTypeVariable(it) }.toTypedArray()
69+
val actualTypeArguments = type.typeParameters.map { resolveTypeVariables(it) }.toTypedArray()
6770
ParameterizedTypeImpl(type.erasedType, actualTypeArguments, null)
6871
}
6972
}
@@ -73,19 +76,17 @@ internal class GenericType(private val mostSpecificType: JavaType, private val o
7376
// only a variable leaked from a raw type can be bound to a type containing itself (e.g. T -> List<T>),
7477
// erase it like the raw type does instead of expanding it forever
7578
type in resolving -> TypeUtils.getRawType(type.bounds.first(), null) ?: Any::class.java
76-
// the most specific type binds the variables of all its supertypes
77-
genericDeclaration is Class<*> -> generateSequence(mostSpecificType) { (it as? ParameterizedType)?.ownerType }
79+
// the containing type binds the variables of all its supertypes
80+
genericDeclaration is Class<*> -> generateSequence(containingType) { (it as? ParameterizedType)?.ownerType }
7881
// an inner class can also use the variables of its outer class, those are bound by its owner type (e.g. Connection<Owner>.Entry)
7982
.firstNotNullOfOrNull { TypeUtils.getTypeArguments(it, genericDeclaration)?.get(type) }
8083
// keep the full type argument (e.g. List<Foo>) rather than its raw class so nested generics aren't lost
81-
?.let { replaceTypeVariable(it, resolving + type) }
84+
?.let { resolveTypeVariables(it, resolving + type) }
8285
?: type
8386
else -> type
8487
}
8588
}
86-
else -> {
87-
type
88-
}
89+
else -> type
8990
}
9091
}
9192
}

‎src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt‎

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package graphql.kickstart.tools
33
import graphql.execution.DataFetcherResult
44
import graphql.kickstart.tools.util.GraphQLLangType
55
import graphql.kickstart.tools.util.JavaType
6+
import graphql.kickstart.tools.util.isSubtypeOf
67
import graphql.language.*
78
import graphql.schema.idl.ScalarInfo
89
import java.lang.reflect.ParameterizedType
@@ -13,10 +14,6 @@ import java.util.*
1314
*/
1415
internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeDefinition<*>>) {
1516

16-
companion object {
17-
fun isListType(realType: ParameterizedType, generic: GenericType) = generic.isTypeAssignableFromRawClass(realType, Iterable::class.java)
18-
}
19-
2017
private fun error(potentialMatch: PotentialMatch, msg: String) = SchemaClassScannerError("Unable to match type definition (${potentialMatch.graphQLType}) for reference ${potentialMatch.reference} with java type (${potentialMatch.javaType}): $msg")
2118

2219
fun match(potentialMatch: PotentialMatch): Match {
@@ -27,22 +24,22 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
2724

2825
var realType = potentialMatch.generic.unwrapGenericType(javaType)
2926

30-
if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, DataFetcherResult::class.java)) {
27+
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
3128
if (potentialMatch.location != Location.RETURN_TYPE) {
3229
throw error(potentialMatch, "${DataFetcherResult::class.java.name} can only be used as a return type")
3330
}
3431

3532
realType = potentialMatch.generic.unwrapGenericType(realType.actualTypeArguments.first())
3633

37-
if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, DataFetcherResult::class.java)) {
34+
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
3835
throw error(potentialMatch, "${DataFetcherResult::class.java.name} cannot be nested within itself")
3936
}
4037
}
4138

4239
var optional = false
4340

4441
// Handle jdk8 Optionals
45-
if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) {
42+
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
4643
optional = true
4744

4845
if (potentialMatch.location == Location.RETURN_TYPE && !root) {
@@ -51,7 +48,7 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
5148

5249
realType = potentialMatch.generic.unwrapGenericType(realType.actualTypeArguments.first())
5350

54-
if (realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) {
51+
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
5552
throw error(potentialMatch, "${Optional::class.java.name} cannot be nested within itself")
5653
}
5754
}
@@ -66,7 +63,7 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
6663
}
6764

6865
is ListType -> when {
69-
realType is ParameterizedType && isListType(realType, potentialMatch) ->
66+
realType is ParameterizedType && realType.isSubtypeOf(Iterable::class.java) ->
7067
match(potentialMatch, graphQLType.type, realType.actualTypeArguments.first())
7168
realType is Class<*> && realType.isArray ->
7269
match(potentialMatch, graphQLType.type, realType.componentType)
@@ -93,8 +90,6 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
9390
}
9491
}
9592

96-
private fun isListType(realType: ParameterizedType, potentialMatch: PotentialMatch) = isListType(realType, potentialMatch.generic)
97-
9893
internal sealed interface Match
9994

10095
internal data class ScalarMatch(val type: ScalarTypeDefinition) : Match

‎src/main/kotlin/graphql/kickstart/tools/util/Utils.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ internal fun JavaType.unwrap(): Class<out Any> =
4141
this as Class<*>
4242
}
4343

44+
internal fun ParameterizedType.isSubtypeOf(type: Class<*>): Boolean = type.isAssignableFrom(unwrap())
45+
4446
/**
4547
* Replaces a parameterized type whose type arguments are all unbounded wildcards, e.g. Kotlin's Page<*>, by its raw type.
4648
*/

0 commit comments

Comments
 (0)