From 7e2b4fb65abfff69b71659a7fad6e2c2cb6e21e8 Mon Sep 17 00:00:00 2001 From: Tim Ward Date: Mon, 6 Mar 2023 23:55:38 -0800 Subject: [PATCH 1/3] Add support for unused types for all dictionary types --- .../kotlin/graphql/kickstart/tools/SchemaClassScanner.kt | 8 +++----- .../graphql/kickstart/tools/SchemaClassScannerTest.kt | 9 ++++++++- .../kotlin/graphql/kickstart/tools/TestInterfaces.kt | 7 +++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index fd445ff9..887f1f57 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -91,15 +91,13 @@ internal class SchemaClassScanner( do { val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes)) .filter { definition -> definition.name != "PageInfo" } - .filterIsInstance().distinct() + .distinct() if (unusedDefinitions.isEmpty()) { 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." } + handleDictionaryTypes(unusedDefinitions) { "Type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) } @@ -229,7 +227,7 @@ internal class SchemaClassScanner( }.flatten().distinct() } - private fun handleDictionaryTypes(types: List, failureMessage: (ObjectTypeDefinition) -> String) { + private fun handleDictionaryTypes(types: List>, failureMessage: (TypeDefinition<*>) -> String) { types.forEach { type -> val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty() if (!unvalidatedTypes.contains(type) && !dictionaryContainsType) { diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt index 673fbe33..87af797f 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt @@ -497,12 +497,19 @@ class SchemaClassScannerTest { type Implementation implements SomeInterface { value: String } + + union SomeUnion = Unused | Implementation + + enum SomeEnum { + 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, SomeInterface::class, SomeUnion::class, SomeEnum::class) .build() .makeExecutableSchema() diff --git a/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt b/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt index 17a22a91..52e2ff54 100644 --- a/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt +++ b/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt @@ -14,3 +14,10 @@ interface SomeInterface { fun getValue(): String? } +interface SomeUnion + +enum class SomeEnum { + A, + B +} + From 4b83c0b3d9295aa8aec11fbbc3f7b7f392cf1c92 Mon Sep 17 00:00:00 2001 From: Tim Ward Date: Tue, 14 Mar 2023 13:47:06 -0700 Subject: [PATCH 2/3] Restrict to composite and enum types. --- .../kotlin/graphql/kickstart/tools/SchemaClassScanner.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 887f1f57..b7258512 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -91,6 +91,7 @@ internal class SchemaClassScanner( do { val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes)) .filter { definition -> definition.name != "PageInfo" } + .filter { isCompositeOrEnumType(it) } .distinct() if (unusedDefinitions.isEmpty()) { @@ -104,6 +105,10 @@ internal class SchemaClassScanner( return validateAndCreateResult(rootTypeHolder) } + private fun isCompositeOrEnumType(definition: TypeDefinition<*>): Boolean { + return definition is ObjectTypeDefinition || definition is InterfaceTypeDefinition || definition is UnionTypeDefinition || definition is EnumTypeDefinition + } + private fun scanQueue(): Boolean { if (queue.isEmpty()) { return false From f2ba9a8dddd629c9ef5499250a74fafcb05d3e24 Mon Sep 17 00:00:00 2001 From: Oryan Date: Fri, 2 Oct 2026 18:43:37 -0400 Subject: [PATCH 3/3] Include unused types one at a time and don't require classes for interfaces and unions Handling all unused types at once required a dictionary class for types that would have been discovered by scanning another unused type. Unused interfaces and unions are now included without a class, and unused enums missing from the dictionary are only reported as unused instead of failing. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../kickstart/tools/SchemaClassScanner.kt | 43 ++++++++----- .../kickstart/tools/SchemaClassScannerTest.kt | 60 +++++++++++++++++-- .../graphql/kickstart/tools/TestInterfaces.kt | 7 --- 3 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 3c180c10..88c05a94 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -86,29 +86,40 @@ 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" } - .filter { isCompositeOrEnumType(it) } - .distinct() + .firstOrNull { canIncludeUnusedType(it) } + ?: break - if (unusedDefinitions.isEmpty()) { - break - } - - handleDictionaryTypes(unusedDefinitions) { "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 isCompositeOrEnumType(definition: TypeDefinition<*>): Boolean { - return definition is ObjectTypeDefinition || definition is InterfaceTypeDefinition || definition is UnionTypeDefinition || definition is EnumTypeDefinition + 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 { @@ -466,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 e7a2f758..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,19 +526,28 @@ class SchemaClassScannerTest { type Implementation implements SomeInterface { value: String } - + + interface OtherInterface { + value: String + } + union SomeUnion = Unused | Implementation - + enum SomeEnum { - A - B + A + B + } + + enum OtherEnum { + A + B } """) .resolvers(object : GraphQLQueryResolver { fun whatever(): Whatever? = null }) .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) - .dictionary(Unused::class, Implementation::class, SomeInterface::class, SomeUnion::class, SomeEnum::class) + .dictionary(Unused::class, Implementation::class, SomeEnum::class) .build() .makeExecutableSchema() @@ -547,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 } diff --git a/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt b/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt index 52e2ff54..17a22a91 100644 --- a/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt +++ b/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt @@ -14,10 +14,3 @@ interface SomeInterface { fun getValue(): String? } -interface SomeUnion - -enum class SomeEnum { - A, - B -} -