diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index ccfc23ae..88c05a94 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -86,28 +86,42 @@ internal class SchemaClassScanner( handleDictionaryTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) + handleDirectives() + // Find unused types and include them if required if (options.includeUnusedTypes) { - do { - val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes)) + // One type at a time, since scanning it can discover other unused types along with their classes + while (true) { + val unusedDefinition = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes)) .filter { definition -> definition.name != "PageInfo" } - .filterIsInstance().distinct() - - if (unusedDefinitions.isEmpty()) { - break - } + .firstOrNull { canIncludeUnusedType(it) } + ?: break - val unusedDefinition = unusedDefinitions.first() - - handleDictionaryTypes(listOf(unusedDefinition)) { "Object type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." } - } while (scanQueue()) + handleUnusedType(unusedDefinition) + scanQueue() + } } - handleDirectives() - return validateAndCreateResult(rootTypeHolder) } + private fun canIncludeUnusedType(definition: TypeDefinition<*>): Boolean = when (definition) { + is ObjectTypeDefinition, is InterfaceTypeDefinition, is UnionTypeDefinition -> true + // Enums can't be built without a class, so the ones missing from the dictionary are only reported as unused + is EnumTypeDefinition -> initialDictionary.containsKey(definition.name) + else -> false + } + + private fun handleUnusedType(type: TypeDefinition<*>) { + val initialEntry = initialDictionary[type.name] + when { + initialEntry != null -> handleFoundType(type, initialEntry.get(), DictionaryReference()) + // Interfaces and unions are resolved through the classes of their implementations, so they don't need one + type is InterfaceTypeDefinition || type is UnionTypeDefinition -> handleFoundType(type, null, UnusedTypeReference()) + else -> throw SchemaClassScannerError("Object type '${type.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${type.name}' in the parser's dictionary.") + } + } + private fun scanQueue(): Boolean { if (queue.isEmpty()) { return false @@ -463,6 +477,10 @@ internal class SchemaClassScanner( override fun getDescription() = "provided dictionary" } + private class UnusedTypeReference : Reference() { + override fun getDescription() = "unused type" + } + private class InterfaceReference(private val type: ObjectTypeDefinition) : Reference() { override fun getDescription() = "interface declarations of ${type.name}" } diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt index 9dff7011..da710f1c 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt @@ -503,7 +503,7 @@ class SchemaClassScannerTest { } @Test - fun `scanner should handle unused types with interfaces when option is true`() { + fun `scanner should handle unused types with interfaces, unions and enums when option is true`() { val schema = SchemaParser.newParser() .schemaString( """ @@ -526,12 +526,28 @@ class SchemaClassScannerTest { type Implementation implements SomeInterface { value: String } + + interface OtherInterface { + value: String + } + + union SomeUnion = Unused | Implementation + + enum SomeEnum { + A + B + } + + enum OtherEnum { + A + B + } """) .resolvers(object : GraphQLQueryResolver { fun whatever(): Whatever? = null }) .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) - .dictionary(Unused::class, Implementation::class) + .dictionary(Unused::class, Implementation::class, SomeEnum::class) .build() .makeExecutableSchema() @@ -540,12 +556,51 @@ class SchemaClassScannerTest { assert(objectTypes.any { it.name == "Unused" }) assert(objectTypes.any { it.name == "Implementation" }) assert(interfaceTypes.any { it.name == "SomeInterface" }) + assert(schema.getType("OtherInterface") is GraphQLInterfaceType) + assert(schema.getType("SomeUnion") is GraphQLUnionType) + assert(schema.getType("SomeEnum") is GraphQLEnumType) + assert(schema.getType("OtherEnum") == null) + } + + @Test + fun `scanner should handle unused enum used as an argument of a missing resolver when option is true`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + whatever: Whatever + preview(value: SomeEnum): String + } + + type Whatever { + value: String + } + + enum SomeEnum { + A + B + } + """) + .resolvers(object : GraphQLQueryResolver { + fun whatever(): Whatever? = null + }) + .options(SchemaParserOptions.newOptions() + .includeUnusedTypes(true) + .missingResolverDataFetcherProvider { _, _ -> DataFetcher { null } } + .build()) + .dictionary(SomeEnum::class) + .build() + .makeExecutableSchema() + + assert(schema.queryType.getFieldDefinition("preview").getArgument("value").type is GraphQLEnumType) } class Whatever { var value: String? = null } + enum class SomeEnum { A, B } + class Unused { var someInterface: SomeInterface? = null }