Skip to content

Commit 5f94793

Browse files
committed
Warn at build when an argument is bound to a context parameter
1 parent ad4ad8a commit 5f94793

3 files changed

Lines changed: 85 additions & 1 deletion

File tree

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import graphql.kickstart.tools.util.*
99
import graphql.language.*
1010
import graphql.schema.GraphQLScalarType
1111
import graphql.schema.idl.ScalarInfo
12+
import graphql.schema.idl.TypeUtil
1213
import org.apache.commons.lang3.ClassUtils
1314
import org.apache.commons.lang3.reflect.TypeUtils
1415
import org.slf4j.LoggerFactory
@@ -528,6 +529,7 @@ internal class SchemaClassScanner(
528529
fieldResolver.scanForMatches().forEach { potentialMatch ->
529530
handleFoundType(typeClassMatcher.match(potentialMatch))
530531
}
532+
warnIfArgumentPassedToContext(type, fieldResolver)
531533
}
532534
}
533535

@@ -552,6 +554,32 @@ internal class SchemaClassScanner(
552554
)
553555
}
554556

557+
// TODO throw a FieldResolverError instead in 15.0.0 (#889)
558+
private fun warnIfArgumentPassedToContext(type: ObjectTypeDefinition, fieldResolver: FieldResolver) {
559+
if (fieldResolver !is MethodFieldResolver) {
560+
return
561+
}
562+
563+
fieldResolver.field.inputValueDefinitions.forEachIndexed { index, argument ->
564+
val parameter = fieldResolver.getMethodParameterType(index)?.let(fieldResolver.typeResolver::getRawClass) ?: return@forEachIndexed
565+
if (!isContextParameter(parameter, argument)) {
566+
return@forEachIndexed
567+
}
568+
569+
val method = fieldResolver.method
570+
log.warn(
571+
"Field '${type.name}.${fieldResolver.field.name}' passes its argument '${argument.name}: ${TypeUtil.simplePrint(argument.type)}' " +
572+
"to the ${parameter.name} parameter of ${method.declaringClass.name}.${method.name}, so the field fails or gets null for " +
573+
"that parameter. Declare one parameter per argument, in order, before the ${parameter.simpleName} parameter."
574+
)
575+
}
576+
}
577+
578+
// the context class can also be an argument's type (e.g. a Map for an input object), so only spec scalars count for it
579+
private fun isContextParameter(parameter: Class<*>, argument: InputValueDefinition): Boolean =
580+
parameter in fieldResolverScanner.allowedTrailingParameterTypes &&
581+
(parameter != options.contextClass || argument.type.unwrap().name in ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS)
582+
555583
private fun handleFoundType(match: TypeClassMatcher.Match) = when (match) {
556584
is TypeClassMatcher.ScalarMatch -> handleFoundScalarType(match.type)
557585
is TypeClassMatcher.ValidMatch -> handleFoundType(match.type, match.javaType, match.reference)

‎src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
2525

2626
private val log = LoggerFactory.getLogger(javaClass)
2727

28-
private val allowedTrailingParameterTypes = listOfNotNull(DataFetchingEnvironment::class.java, GraphQLContext::class.java, options.contextClass)
28+
val allowedTrailingParameterTypes = listOfNotNull(DataFetchingEnvironment::class.java, GraphQLContext::class.java, options.contextClass)
2929

3030
private val methodsByNameCache = mutableMapOf<Pair<Class<out Any>, Boolean>, Map<String, List<Method>>>()
3131

‎src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import graphql.language.Value
1010
import graphql.schema.Coercing
1111
import graphql.schema.CoercingParseLiteralException
1212
import graphql.schema.CoercingSerializeException
13+
import graphql.schema.DataFetchingEnvironment
1314
import graphql.schema.GraphQLScalarType
1415
import org.junit.Test
1516
import java.lang.reflect.InvocationHandler
@@ -283,6 +284,61 @@ class MethodFieldResolverTest {
283284
))
284285
}
285286

287+
@Test
288+
fun `should warn when an argument is passed to a context parameter`() {
289+
val (_, warnings) = withWarnings {
290+
SchemaParser.newParser()
291+
.schemaString(
292+
"""
293+
type Query {
294+
context(id: ID!, persist: Boolean!): String
295+
graphQLContext(persist: Boolean, id: ID): String
296+
image: Image
297+
input(input: Input): String
298+
extra(persist: Boolean): String
299+
}
300+
301+
type Image {
302+
thumbnail(size: Int): String
303+
}
304+
305+
input Input {
306+
value: String
307+
}
308+
""")
309+
.resolvers(ContextParameterQuery(), ImageResolver())
310+
.options(SchemaParserOptions.newOptions().contextClass(Map::class).build())
311+
.build()
312+
.makeExecutableSchema()
313+
}
314+
315+
val query = ContextParameterQuery::class.java.name
316+
assertEquals(warnings, listOf(
317+
"Field 'Query.context' passes its argument 'persist: Boolean!' to the java.util.Map parameter of $query.context, so the field " +
318+
"fails or gets null for that parameter. Declare one parameter per argument, in order, before the Map parameter.",
319+
"Field 'Query.graphQLContext' passes its argument 'persist: Boolean' to the graphql.GraphQLContext parameter of " +
320+
"$query.graphQLContext, so the field fails or gets null for that parameter. Declare one parameter per argument, in order, " +
321+
"before the GraphQLContext parameter.",
322+
"Field 'Image.thumbnail' passes its argument 'size: Int' to the graphql.schema.DataFetchingEnvironment parameter of " +
323+
"${ImageResolver::class.java.name}.thumbnail, so the field fails or gets null for that parameter. Declare one parameter per " +
324+
"argument, in order, before the DataFetchingEnvironment parameter."
325+
))
326+
}
327+
328+
class ContextParameterQuery : GraphQLQueryResolver {
329+
fun context(id: String, context: Map<String, Any>) = "$id $context"
330+
fun graphQLContext(context: GraphQLContext?, id: String?) = "$context $id"
331+
fun image() = Image()
332+
fun input(input: Map<String, Any>?) = input.toString()
333+
fun extra(persist: Boolean?, context: Map<String, Any>) = "$persist $context"
334+
}
335+
336+
class Image
337+
338+
class ImageResolver : GraphQLResolver<Image> {
339+
fun thumbnail(image: Image, env: DataFetchingEnvironment?) = env.toString()
340+
}
341+
286342
sealed class Maybe<out T> {
287343
data class Some<T>(val value: T) : Maybe<T>()
288344
object None : Maybe<Nothing>()

0 commit comments

Comments
 (0)