Skip to content

Commit 3386722

Browse files
authored
Merge pull request #890 from graphql-java-kickstart/feature/418-warn-argument-on-context
Warn at build when a resolver method is missing a parameter for an argument
2 parents ad4ad8a + 57151da commit 3386722

3 files changed

Lines changed: 84 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+
warnIfParameterMissing(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 warnIfParameterMissing(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}' has no parameter for its argument '${argument.name}: " +
572+
"${TypeUtil.simplePrint(argument.type)}' in ${method.declaringClass.name}.${method.name}, so the ${parameter.simpleName} " +
573+
"parameter gets the argument instead. Add a parameter for each 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: 55 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,60 @@ class MethodFieldResolverTest {
283284
))
284285
}
285286

287+
@Test
288+
fun `should warn when a resolver method is missing a parameter for an argument`() {
289+
val (_, warnings) = withWarnings {
290+
SchemaParser.newParser()
291+
.schemaString(
292+
"""
293+
type Query {
294+
user(id: ID!, active: Boolean!): String
295+
users(active: Boolean, first: Int): String
296+
image: Image
297+
search(filter: Filter): String
298+
count(active: Boolean): String
299+
}
300+
301+
type Image {
302+
thumbnail(size: Int): String
303+
}
304+
305+
input Filter {
306+
name: String
307+
}
308+
""")
309+
.resolvers(MissingParameterQuery(), ImageResolver())
310+
.options(SchemaParserOptions.newOptions().contextClass(Map::class).build())
311+
.build()
312+
.makeExecutableSchema()
313+
}
314+
315+
val query = MissingParameterQuery::class.java.name
316+
assertEquals(warnings, listOf(
317+
"Field 'Query.user' has no parameter for its argument 'active: Boolean!' in $query.user, so the Map parameter gets the " +
318+
"argument instead. Add a parameter for each argument, in order, before the Map parameter.",
319+
"Field 'Query.users' has no parameter for its argument 'active: Boolean' in $query.users, so the GraphQLContext parameter " +
320+
"gets the argument instead. Add a parameter for each argument, in order, before the GraphQLContext parameter.",
321+
"Field 'Image.thumbnail' has no parameter for its argument 'size: Int' in ${ImageResolver::class.java.name}.thumbnail, so the " +
322+
"DataFetchingEnvironment parameter gets the argument instead. Add a parameter for each argument, in order, before the " +
323+
"DataFetchingEnvironment parameter."
324+
))
325+
}
326+
327+
class MissingParameterQuery : GraphQLQueryResolver {
328+
fun user(id: String, context: Map<String, Any>) = "$id $context"
329+
fun users(context: GraphQLContext?, first: Int?) = "$context $first"
330+
fun image() = Image()
331+
fun search(filter: Map<String, Any>?) = filter.toString()
332+
fun count(active: Boolean?, context: Map<String, Any>) = "$active $context"
333+
}
334+
335+
class Image
336+
337+
class ImageResolver : GraphQLResolver<Image> {
338+
fun thumbnail(image: Image, env: DataFetchingEnvironment?) = env.toString()
339+
}
340+
286341
sealed class Maybe<out T> {
287342
data class Some<T>(val value: T) : Maybe<T>()
288343
object None : Maybe<Nothing>()

0 commit comments

Comments
 (0)