Skip to content

Commit b6feaf7

Browse files
authored
Merge pull request #737 from timward60/timward/unused-types
Add support for unused types for all dictionary types
2 parents f1d12cc + f2ba9a8 commit b6feaf7

2 files changed

Lines changed: 88 additions & 15 deletions

File tree

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,28 +86,42 @@ 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-
.filterIsInstance<ObjectTypeDefinition>().distinct()
95-
96-
if (unusedDefinitions.isEmpty()) {
97-
break
98-
}
97+
.firstOrNull { canIncludeUnusedType(it) }
98+
?: break
9999

100-
val unusedDefinition = unusedDefinitions.first()
101-
102-
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." }
103-
} while (scanQueue())
100+
handleUnusedType(unusedDefinition)
101+
scanQueue()
102+
}
104103
}
105104

106-
handleDirectives()
107-
108105
return validateAndCreateResult(rootTypeHolder)
109106
}
110107

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+
}
123+
}
124+
111125
private fun scanQueue(): Boolean {
112126
if (queue.isEmpty()) {
113127
return false
@@ -463,6 +477,10 @@ internal class SchemaClassScanner(
463477
override fun getDescription() = "provided dictionary"
464478
}
465479

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

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

Lines changed: 57 additions & 2 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,12 +526,28 @@ class SchemaClassScannerTest {
526526
type Implementation implements SomeInterface {
527527
value: String
528528
}
529+
530+
interface OtherInterface {
531+
value: String
532+
}
533+
534+
union SomeUnion = Unused | Implementation
535+
536+
enum SomeEnum {
537+
A
538+
B
539+
}
540+
541+
enum OtherEnum {
542+
A
543+
B
544+
}
529545
""")
530546
.resolvers(object : GraphQLQueryResolver {
531547
fun whatever(): Whatever? = null
532548
})
533549
.options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build())
534-
.dictionary(Unused::class, Implementation::class)
550+
.dictionary(Unused::class, Implementation::class, SomeEnum::class)
535551
.build()
536552
.makeExecutableSchema()
537553

@@ -540,12 +556,51 @@ class SchemaClassScannerTest {
540556
assert(objectTypes.any { it.name == "Unused" })
541557
assert(objectTypes.any { it.name == "Implementation" })
542558
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)
543596
}
544597

545598
class Whatever {
546599
var value: String? = null
547600
}
548601

602+
enum class SomeEnum { A, B }
603+
549604
class Unused {
550605
var someInterface: SomeInterface? = null
551606
}

0 commit comments

Comments
 (0)