Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 19 additions & 43 deletions src/main/kotlin/graphql/kickstart/tools/GenericType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import graphql.kickstart.tools.util.JavaType
import graphql.kickstart.tools.util.ParameterizedTypeImpl
import graphql.kickstart.tools.util.Primitives
import graphql.kickstart.tools.util.unwrap
import org.apache.commons.lang3.reflect.TypeUtils
import java.lang.reflect.ParameterizedType
import java.lang.reflect.TypeVariable
Expand Down Expand Up @@ -56,7 +55,7 @@
TypeUtils.isAssignable(genericInterface, targetInterface)
} ?: raw.interfaces.find { iface ->
TypeUtils.isAssignable(iface, targetInterface)
} ?: getGenericInterface(raw.genericSuperclass, targetInterface) ?: return null

Check warning on line 58 in src/main/kotlin/graphql/kickstart/tools/GenericType.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless elvis operation ?:, it always succeeds.

See more on https://sonarcloud.io/project/issues?id=graphql-java-kickstart_graphql-java-tools&issues=AaEM6lWbK7f85vLbwDF6&open=AaEM6lWbK7f85vLbwDF6&pullRequest=825

return getGenericInterface(possibleSubInterface, targetInterface)
}
Expand Down Expand Up @@ -97,51 +96,24 @@
val unwrapsTo = genericType.schemaWrapper.invoke(typeArguments[genericType.index])
return unwrapGenericType(unwrapsTo)
}
is TypeVariable<*> -> {
val parameterizedDeclaringType = parameterizedDeclaringTypeOrSuperType(declaringType)
if (parameterizedDeclaringType != null) {
unwrapGenericType(parameterizedDeclaringType, type)
} else {
error("Could not resolve type variable '${TypeUtils.toLongString(type)}' because declaring type is not parameterized: ${TypeUtils.toString(declaringType)}")
}
}
is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' of ${TypeUtils.toString(declaringType)} relative to ${TypeUtils.toString(mostSpecificType)}")
is WildcardType -> type.upperBounds.firstOrNull()
?: error("Unable to unwrap type, wildcard has no upper bound: $type")
is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type
else -> error("Unable to unwrap type: $type")
}
}

private fun parameterizedDeclaringTypeOrSuperType(declaringType: JavaType): ParameterizedType? =
if (declaringType is ParameterizedType) {
declaringType
} else {
val superclass = declaringType.unwrap().genericSuperclass
if (superclass != null) {
parameterizedDeclaringTypeOrSuperType(superclass)
} else {
null
}
}

private fun unwrapGenericType(declaringType: ParameterizedType, type: TypeVariable<*>): JavaType {
val rawClass = getRawClass(mostSpecificType)
val arguments = TypeUtils.determineTypeArguments(rawClass, declaringType)
val matchingType = arguments
.filter { it.key.name == type.name }
.values
.firstOrNull()
?: error("No type variable found for: ${TypeUtils.toLongString(type)}")

return unwrapGenericType(matchingType)
}

private fun replaceTypeVariable(type: JavaType): JavaType {
private fun replaceTypeVariable(type: JavaType, resolving: Set<TypeVariable<*>> = emptySet()): JavaType {
return when (type) {
is ParameterizedType -> {
val actualTypeArguments = type.actualTypeArguments.map { replaceTypeVariable(it) }.toTypedArray()
ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType)
val actualTypeArguments = type.actualTypeArguments.map { replaceTypeVariable(it, resolving) }.toTypedArray()
ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType?.let { replaceTypeVariable(it, resolving) })
}
is WildcardType -> TypeUtils.wildcardType()
.withUpperBounds(*type.upperBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray())
.withLowerBounds(*type.lowerBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray())
.build()
is ResolvedType -> {
if (type.typeParameters.isEmpty()) {
type.erasedType
Expand All @@ -152,14 +124,18 @@
}
is TypeVariable<*> -> {
val genericDeclaration = type.genericDeclaration
if (declaringType is ParameterizedType && genericDeclaration is Class<*>) {
// keep the full type argument (e.g. List<Foo>) rather than its raw class so nested generics aren't lost
TypeUtils.getTypeArguments(declaringType, genericDeclaration)?.get(type)
?.takeIf { it != type }
?.let { replaceTypeVariable(it) }
when {
// only a variable leaked from a raw type can be bound to a type containing itself (e.g. T -> List<T>),
// erase it like the raw type does instead of expanding it forever
type in resolving -> TypeUtils.getRawType(type.bounds.first(), null) ?: Any::class.java
// the most specific type binds the variables of its supertypes and its owner types those of outer classes,
// the declaring type may not
genericDeclaration is Class<*> -> generateSequence(mostSpecificType) { (it as? ParameterizedType)?.ownerType }
.firstNotNullOfOrNull { TypeUtils.getTypeArguments(it, genericDeclaration)?.get(type) }
// keep the full type argument (e.g. List<Foo>) rather than its raw class so nested generics aren't lost
?.let { replaceTypeVariable(it, resolving + type) }
?: type
} else {
type
else -> type
}
}
else -> {
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ internal class SchemaClassScanner(
if (java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) {
throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}")
}
throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.unwrap()}:\n| ${reference.getDescription()}")
throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.typeName}:\n| ${reference.getDescription()}")
}
}
}
Expand Down Expand Up @@ -471,7 +471,7 @@ internal class SchemaClassScanner(
references.add(reference)
}

fun joinReferences() = "- ${typeClass()}:\n| " + references.joinToString("\n| ") { it.getDescription() }
fun joinReferences() = "- ${javaType?.typeName}:\n| " + references.joinToString("\n| ") { it.getDescription() }

fun hasResolverRef(): Boolean {
references.filterIsInstance<ReturnValueReference>().forEach { reference ->
Expand Down
50 changes: 50 additions & 0 deletions src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,39 @@ class GenericInputTypesTest {
assertEquals(data["audit"], "why:op:1")
}

@Test
fun `generic list fields inherited from parameterized superclasses are parsed`() {
val schema = SchemaParser.newParser()
.schemaString(
"""
type Query {
mutate(input: MutationInput!): String!
}

input MutationInput {
items: [Item!]!
}

input Item {
prop: String!
}
""")
.resolvers(InheritedListQueryResolver())
.build()
.makeExecutableSchema()
val gql = GraphQL.newGraphQL(schema).build()

val data = assertNoGraphQlErrors(gql) {
"""
query {
mutate(input: { items: [{ prop: "a" }, { prop: "b" }] })
}
"""
}

assertEquals(data["mutate"], "a,b")
}

class QueryResolver : GraphQLQueryResolver {
fun audit(input: AuditWrapper<LanguageInput>): String = "${input.operator}:${input.content?.id}"
fun audits(input: AuditWrapper<List<LanguageInput>>): String = "${input.operator}:${input.content?.joinToString(",") { it.id.orEmpty() }}"
Expand All @@ -184,6 +217,10 @@ class GenericInputTypesTest {
fun audit(input: AuditRequest): String = "${input.reason}:${input.audit?.operator}:${input.audit?.content?.id}"
}

class InheritedListQueryResolver : GraphQLQueryResolver {
fun mutate(input: MutationInput): String = input.items.orEmpty().joinToString(",") { it.prop.orEmpty() }
}

open class AuditWrapper<T> {
var content: T? = null
var operator: String? = null
Expand All @@ -196,6 +233,19 @@ class GenericInputTypesTest {
var reason: String? = null
}

abstract class GenericMutationInput<T> {
@JvmField
var items: List<T>? = null
}

abstract class RenamedMutationInput<U> : GenericMutationInput<U>()

class MutationInput : RenamedMutationInput<MutationInput.Item>() {
class Item {
var prop: String? = null
}
}

class LanguageInput {
var id: String? = null
}
Expand Down
Loading
Loading