From 6208907f1b655cd758ce08666a5a50dd16f4e2f1 Mon Sep 17 00:00:00 2001 From: Oryan Date: Sun, 18 Oct 2026 21:43:44 -0400 Subject: [PATCH] Resolve type variables in resolver return types --- .../tools/resolver/MethodFieldResolver.kt | 13 ++++++++----- .../graphql/kickstart/tools/ReactiveTest.kt | 16 +++++++++++++--- .../graphql/kickstart/tools/SchemaParserTest.kt | 9 +++++++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt index 2d721bc8..49ec637c 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt @@ -130,9 +130,9 @@ internal class MethodFieldResolver( } // the class the return type unwraps to, so a value that happens to implement Future isn't mistaken for a wrapper. - // Object (e.g. for a union) can't tell them apart, so then every future of a method declared to return Future is checked + // Object (e.g. for a union) can't tell them apart, so then every future of a method whose return type is Future is checked val valueClass = (TypeUtils.getRawType(getResolvedReturnType(), null) ?: Any::class.java) - .takeUnless { it == Any::class.java && TypeUtils.getRawType(getReturnType(), null) == Future::class.java } + .takeUnless { it == Any::class.java && TypeUtils.getRawType(returnType, null) == Future::class.java } return if (numberOfParameters > 0 || isSuspendFunction) { // requires arguments and environment or is a suspend function @@ -180,15 +180,18 @@ internal class MethodFieldResolver( ?.let { TypeUtils.parameterize(Publisher::class.java, it) } ?: this - private fun getReturnType(): JavaType = - try { + // the declared return type with type variables resolved, e.g. Future for get(): T in a subclass of Base> + private val returnType: JavaType by lazy { + val declaredType = try { method.kotlinFunction?.returnType?.javaType ?: method.genericReturnType } catch (e: InternalError) { method.genericReturnType } + typeResolver.resolveTypeVariables(declaredType) + } private fun getResolvedReturnType(): JavaType = - getReturnType().let { typeResolver.resolve(if (search.isSubscription) it.asPublisher() else it) } + typeResolver.resolve(if (search.isSubscription) returnType.asPublisher() else returnType) private fun getIndexOffset(): Int { return if (resolverInfo is DataClassTypeResolverInfo && !method.declaringClass.isAssignableFrom(resolverInfo.dataClassType)) { diff --git a/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt index 6ef9f762..fbc30609 100644 --- a/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt @@ -127,6 +127,7 @@ class ReactiveTest { item: Item boxedItem: Item search: SearchResult + inheritedSearch: SearchResult } union SearchResult = Item @@ -151,16 +152,21 @@ class ReactiveTest { search { ... on Item { id } } + inheritedSearch { + ... on Item { id } + } } """) - assertEquals(result.getData(), mapOf("item" to null, "boxedItem" to null, "search" to null)) + assertEquals(result.getData(), mapOf("item" to null, "boxedItem" to null, "search" to null, "inheritedSearch" to null)) assertEquals(result.errorMessagesByPath(), mapOf( "item" to "Resolver method '${ItemQuery::class.java.name}.item' resolved to a java.util.concurrent.FutureTask, " + "which graphql-java can't wait on. Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead.", "boxedItem" to "Resolver method '${FutureItemQuery::class.java.name}.boxedItem' resolved to a java.util.concurrent.FutureTask, " + "which graphql-java can't wait on. Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead.", "search" to "Resolver method '${FutureItemQuery::class.java.name}.search' resolved to a java.util.concurrent.FutureTask, " + + "which graphql-java can't wait on. Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead.", + "inheritedSearch" to "Resolver method '${ItemQuery::class.java.name}.inheritedSearch' resolved to a java.util.concurrent.FutureTask, " + "which graphql-java can't wait on. Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead." )) } @@ -232,11 +238,15 @@ class ReactiveTest { fun nullItem(): Future = FutureTask { null }.also { it.run() } } - open class ItemQuery(private val item: () -> R) { + open class ItemQuery(private val item: () -> R, private val search: () -> S) { fun item(): R = item.invoke() + fun inheritedSearch(): S = search.invoke() } - private class FutureItemQuery : ItemQuery>({ FutureTask { Item(1) }.also { it.run() } }), GraphQLQueryResolver { + private class FutureItemQuery : ItemQuery, Future>( + { FutureTask { Item(1) }.also { it.run() } }, + { FutureTask { Item(4) }.also { it.run() } } + ), GraphQLQueryResolver { fun boxedItem(): Box = Box(Item(2)) fun search(): Future = FutureTask { Item(3) }.also { it.run() } } diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt index e70da4bf..51506cf1 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt @@ -1297,7 +1297,7 @@ class SchemaParserTest { @Test fun `parser should accept subscription resolvers returning publisher subtypes`() { - class Subscription : OverriddenPublisherSubscription { + class Subscription : InheritedPublisherSubscription(SingleEventPublisher("onInheritedPublisher")), OverriddenPublisherSubscription { fun onCustomPublisher(): EventPublisher = SingleEventPublisher("onCustomPublisher") fun onConcretePublisher() = SingleEventPublisher("onConcretePublisher") override fun onOverriddenPublisher() = SingleEventPublisher("onOverriddenPublisher") @@ -1314,6 +1314,7 @@ class SchemaParserTest { onCustomPublisher: Event! onConcretePublisher: Event! onOverriddenPublisher: Event! + onInheritedPublisher: Event! } type Event { @@ -1330,7 +1331,7 @@ class SchemaParserTest { .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() - listOf("onCustomPublisher", "onConcretePublisher", "onOverriddenPublisher").forEach { field -> + listOf("onCustomPublisher", "onConcretePublisher", "onOverriddenPublisher", "onInheritedPublisher").forEach { field -> assertEquals(gql.firstSubscriptionEvent("subscription { $field { name } }"), mapOf(field to mapOf("name" to field))) } } @@ -1435,4 +1436,8 @@ class SchemaParserTest { interface OverriddenPublisherSubscription : GraphQLSubscriptionResolver { fun onOverriddenPublisher(): Publisher } + + open class InheritedPublisherSubscription

(private val publisher: P) { + fun onInheritedPublisher(): P = publisher + } }