Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,28 +86,42 @@
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<ObjectTypeDefinition>().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
Expand Down Expand Up @@ -290,7 +304,7 @@
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
}
})
?: throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")

Check warning on line 307 in src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless elvis operation ?:, it always succeeds.

See more on https://sonarcloud.io/project/issues?id=graphql-java-kickstart_graphql-java-tools&issues=AaD-nEfTgc5m8bxkobc1&open=AaD-nEfTgc5m8bxkobc1&pullRequest=737

scanResolverInfoForPotentialMatches(item.type, resolverInfo)
}
Expand Down Expand Up @@ -463,6 +477,10 @@
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}"
}
Expand Down
59 changes: 57 additions & 2 deletions src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"""
Expand All @@ -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()

Expand All @@ -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<Any?> { 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
}
Expand Down
Loading