Skip to content

Commit f2ba9a8

Browse files
oryan-blockclaude
andcommitted
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) <noreply@anthropic.com>
1 parent d8413b1 commit f2ba9a8

3 files changed

Lines changed: 83 additions & 27 deletions

File tree

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

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,29 +86,40 @@ internal class SchemaClassScanner(
8686
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." }
8787
} while (scanQueue())
8888

89+
handleDirectives()
90+
8991
// Find unused types and include them if required
9092
if (options.includeUnusedTypes) {
91-
do {
92-
val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes))
93+
// One type at a time, since scanning it can discover other unused types along with their classes
94+
while (true) {
95+
val unusedDefinition = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes))
9396
.filter { definition -> definition.name != "PageInfo" }
94-
.filter { isCompositeOrEnumType(it) }
95-
.distinct()
97+
.firstOrNull { canIncludeUnusedType(it) }
98+
?: break
9699

97-
if (unusedDefinitions.isEmpty()) {
98-
break
99-
}
100-
101-
handleDictionaryTypes(unusedDefinitions) { "Type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." }
102-
} while (scanQueue())
100+
handleUnusedType(unusedDefinition)
101+
scanQueue()
102+
}
103103
}
104104

105-
handleDirectives()
106-
107105
return validateAndCreateResult(rootTypeHolder)
108106
}
109107

110-
private fun isCompositeOrEnumType(definition: TypeDefinition<*>): Boolean {
111-
return definition is ObjectTypeDefinition || definition is InterfaceTypeDefinition || definition is UnionTypeDefinition || definition is EnumTypeDefinition
108+
private fun canIncludeUnusedType(definition: TypeDefinition<*>): Boolean = when (definition) {
109+
is ObjectTypeDefinition, is InterfaceTypeDefinition, is UnionTypeDefinition -> true
110+
// Enums can't be built without a class, so the ones missing from the dictionary are only reported as unused
111+
is EnumTypeDefinition -> initialDictionary.containsKey(definition.name)
112+
else -> false
113+
}
114+
115+
private fun handleUnusedType(type: TypeDefinition<*>) {
116+
val initialEntry = initialDictionary[type.name]
117+
when {
118+
initialEntry != null -> handleFoundType(type, initialEntry.get(), DictionaryReference())
119+
// Interfaces and unions are resolved through the classes of their implementations, so they don't need one
120+
type is InterfaceTypeDefinition || type is UnionTypeDefinition -> handleFoundType(type, null, UnusedTypeReference())
121+
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.")
122+
}
112123
}
113124

114125
private fun scanQueue(): Boolean {
@@ -466,6 +477,10 @@ internal class SchemaClassScanner(
466477
override fun getDescription() = "provided dictionary"
467478
}
468479

480+
private class UnusedTypeReference : Reference() {
481+
override fun getDescription() = "unused type"
482+
}
483+
469484
private class InterfaceReference(private val type: ObjectTypeDefinition) : Reference() {
470485
override fun getDescription() = "interface declarations of ${type.name}"
471486
}

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

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ class SchemaClassScannerTest {
503503
}
504504

505505
@Test
506-
fun `scanner should handle unused types with interfaces when option is true`() {
506+
fun `scanner should handle unused types with interfaces, unions and enums when option is true`() {
507507
val schema = SchemaParser.newParser()
508508
.schemaString(
509509
"""
@@ -526,19 +526,28 @@ class SchemaClassScannerTest {
526526
type Implementation implements SomeInterface {
527527
value: String
528528
}
529-
529+
530+
interface OtherInterface {
531+
value: String
532+
}
533+
530534
union SomeUnion = Unused | Implementation
531-
535+
532536
enum SomeEnum {
533-
A
534-
B
537+
A
538+
B
539+
}
540+
541+
enum OtherEnum {
542+
A
543+
B
535544
}
536545
""")
537546
.resolvers(object : GraphQLQueryResolver {
538547
fun whatever(): Whatever? = null
539548
})
540549
.options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build())
541-
.dictionary(Unused::class, Implementation::class, SomeInterface::class, SomeUnion::class, SomeEnum::class)
550+
.dictionary(Unused::class, Implementation::class, SomeEnum::class)
542551
.build()
543552
.makeExecutableSchema()
544553

@@ -547,12 +556,51 @@ class SchemaClassScannerTest {
547556
assert(objectTypes.any { it.name == "Unused" })
548557
assert(objectTypes.any { it.name == "Implementation" })
549558
assert(interfaceTypes.any { it.name == "SomeInterface" })
559+
assert(schema.getType("OtherInterface") is GraphQLInterfaceType)
560+
assert(schema.getType("SomeUnion") is GraphQLUnionType)
561+
assert(schema.getType("SomeEnum") is GraphQLEnumType)
562+
assert(schema.getType("OtherEnum") == null)
563+
}
564+
565+
@Test
566+
fun `scanner should handle unused enum used as an argument of a missing resolver when option is true`() {
567+
val schema = SchemaParser.newParser()
568+
.schemaString(
569+
"""
570+
type Query {
571+
whatever: Whatever
572+
preview(value: SomeEnum): String
573+
}
574+
575+
type Whatever {
576+
value: String
577+
}
578+
579+
enum SomeEnum {
580+
A
581+
B
582+
}
583+
""")
584+
.resolvers(object : GraphQLQueryResolver {
585+
fun whatever(): Whatever? = null
586+
})
587+
.options(SchemaParserOptions.newOptions()
588+
.includeUnusedTypes(true)
589+
.missingResolverDataFetcherProvider { _, _ -> DataFetcher<Any?> { null } }
590+
.build())
591+
.dictionary(SomeEnum::class)
592+
.build()
593+
.makeExecutableSchema()
594+
595+
assert(schema.queryType.getFieldDefinition("preview").getArgument("value").type is GraphQLEnumType)
550596
}
551597

552598
class Whatever {
553599
var value: String? = null
554600
}
555601

602+
enum class SomeEnum { A, B }
603+
556604
class Unused {
557605
var someInterface: SomeInterface? = null
558606
}

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,3 @@ interface SomeInterface {
1414
fun getValue(): String?
1515
}
1616

17-
interface SomeUnion
18-
19-
enum class SomeEnum {
20-
A,
21-
B
22-
}
23-

0 commit comments

Comments
 (0)