diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt index 8b0ef2f61ed..0dccc1fd034 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt @@ -163,6 +163,12 @@ fun String.parseAsGQLType(options: ParserOptions = ParserOptions.Default): GQLRe } } +internal fun String.parseAsGQLNullability(options: ParserOptions = ParserOptions.Default): GQLResult { + @Suppress("DEPRECATION") + check (!options.useAntlr) + return parseInternal(null, options) { parseNullability() ?: error("No nullability") } +} + fun String.parseAsGQLSelections(options: ParserOptions = ParserOptions.Default): GQLResult> { @Suppress("DEPRECATION") return if (options.useAntlr) { diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt index 488212fb723..775842bf51b 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt @@ -1529,15 +1529,17 @@ class GQLNullDesignator(override val sourceLocation: SourceLocation? = null) : G @ApolloExperimental class GQLListNullability( override val sourceLocation: SourceLocation? = null, - val itemNullability: GQLNullability, + val itemNullability: GQLNullability?, val selfNullability: GQLNullability?, ) : GQLNullability { override val children: List - get() = listOf(itemNullability) + get() = listOfNotNull(itemNullability) override fun writeInternal(writer: SDLWriter) { writer.write("[") - writer.write(itemNullability) + if (itemNullability != null) { + writer.write(itemNullability) + } writer.write("]") if (selfNullability != null) { writer.write(selfNullability) @@ -1552,7 +1554,7 @@ class GQLListNullability( fun copy( sourceLocation: SourceLocation? = this.sourceLocation, - ofNullability: GQLNullability = this.itemNullability, + ofNullability: GQLNullability? = this.itemNullability, selfNullability: GQLNullability? = this.selfNullability, ): GQLListNullability { return GQLListNullability( diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gqltype.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gqltype.kt index 563a590e561..25b71b712eb 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gqltype.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gqltype.kt @@ -35,7 +35,7 @@ internal fun isVariableUsageAllowed(variableDefinition: GQLVariableDefinition, u } internal fun areTypesCompatible(variableType: GQLType, locationType: GQLType): Boolean { - return if(locationType is GQLNonNullType) { + return if (locationType is GQLNonNullType) { if (variableType !is GQLNonNullType) { false } else { @@ -43,7 +43,7 @@ internal fun areTypesCompatible(variableType: GQLType, locationType: GQLType): B } } else if (variableType is GQLNonNullType) { areTypesCompatible(variableType.type, locationType) - } else if (locationType is GQLListType){ + } else if (locationType is GQLListType) { if (variableType !is GQLListType) { false } else { @@ -84,43 +84,77 @@ internal fun GQLType.isOutputType(typeDefinitions: Map this.selfNullability - else -> this +private fun GQLType.withItemNullability(itemNullability: GQLNullability?, validation: NullabilityValidation): GQLType { + if (itemNullability == null) { + return this } -} -private fun GQLType.withListNullability(nullability: GQLNullability?): GQLType { - if (this is GQLListType && nullability is GQLListNullability) { - return copy(type = type.withNullability(nullability.itemNullability)) - } else if (this is GQLListType && nullability !is GQLListNullability) { - return this - } else if (this !is GQLListType && nullability is GQLListNullability) { - return this - } else if (this !is GQLListType && nullability !is GQLListNullability) { - return this - } else { - error("") + if (this !is GQLListType) { + when (validation) { + is NullabilityValidationThrow -> { + check(this is GQLListType) { + "Cannot apply nullability, the nullability list dimension exceeds the one of the field type." + } + } + + is NullabilityValidationIgnore -> { + return this + + } + + is NullabilityValidationRegister -> { + validation.issues.add( + Issue.ValidationError( + "Cannot apply nullability on '${validation.fieldName}', the nullability list dimension exceeds the one of the field type.", + itemNullability.sourceLocation, + ) + ) + return this + } + } } + + return this.copy(type = type.withNullability(itemNullability, validation)) } @ApolloExperimental fun GQLType.withNullability(nullability: GQLNullability?): GQLType { - val selfNullability = nullability.selfNullability() + return withNullability(nullability, NullabilityValidationThrow) +} + +internal sealed interface NullabilityValidation + +internal object NullabilityValidationIgnore: NullabilityValidation +internal object NullabilityValidationThrow: NullabilityValidation +internal class NullabilityValidationRegister(val issues: MutableList, val fieldName: String): NullabilityValidation - if (this is GQLNonNullType && selfNullability == null) { - return this.copy(type = type.withListNullability(nullability)) +internal fun GQLType.withNullability(nullability: GQLNullability?, validation: NullabilityValidation): GQLType { + val selfNullability: GQLNullability? + val itemNullability: GQLNullability? + + when (nullability) { + is GQLListNullability -> { + selfNullability = nullability.selfNullability + itemNullability = nullability.itemNullability + } + + else -> { + selfNullability = nullability + itemNullability = null + } + } + return if (this is GQLNonNullType && selfNullability == null) { + this.copy(type = type.withItemNullability(itemNullability, validation)) } else if (this is GQLNonNullType && selfNullability is GQLNonNullDesignator) { - return this.copy(type = type.withListNullability(nullability)) + this.copy(type = type.withItemNullability(itemNullability, validation)) } else if (this is GQLNonNullType && selfNullability is GQLNullDesignator) { - return this.type.withListNullability(nullability) + this.type.withItemNullability(itemNullability, validation) } else if (this !is GQLNonNullType && selfNullability == null) { - return this.withListNullability(nullability) + this.withItemNullability(itemNullability, validation) } else if (this !is GQLNonNullType && selfNullability is GQLNonNullDesignator) { - return GQLNonNullType(type = this.withListNullability(nullability)) + GQLNonNullType(type = this.withItemNullability(itemNullability, validation)) } else if (this !is GQLNonNullType && selfNullability is GQLNullDesignator) { - return this.withListNullability(nullability) + this.withItemNullability(itemNullability, validation) } else { error("") } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/ExecutableValidationScope.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/ExecutableValidationScope.kt index 776ac1511d7..93e308c5039 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/ExecutableValidationScope.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/ExecutableValidationScope.kt @@ -12,19 +12,15 @@ import com.apollographql.apollo3.ast.GQLFragmentDefinition import com.apollographql.apollo3.ast.GQLFragmentSpread import com.apollographql.apollo3.ast.GQLInlineFragment import com.apollographql.apollo3.ast.GQLIntValue -import com.apollographql.apollo3.ast.GQLListNullability import com.apollographql.apollo3.ast.GQLListType import com.apollographql.apollo3.ast.GQLListValue import com.apollographql.apollo3.ast.GQLNamedType import com.apollographql.apollo3.ast.GQLNode import com.apollographql.apollo3.ast.GQLNonNullType import com.apollographql.apollo3.ast.GQLNullValue -import com.apollographql.apollo3.ast.GQLNullability import com.apollographql.apollo3.ast.GQLObjectTypeDefinition import com.apollographql.apollo3.ast.GQLObjectValue import com.apollographql.apollo3.ast.GQLOperationDefinition -import com.apollographql.apollo3.ast.GQLNullDesignator -import com.apollographql.apollo3.ast.GQLNonNullDesignator import com.apollographql.apollo3.ast.GQLScalarTypeDefinition import com.apollographql.apollo3.ast.GQLSelection import com.apollographql.apollo3.ast.GQLStringValue @@ -34,6 +30,8 @@ import com.apollographql.apollo3.ast.GQLValue import com.apollographql.apollo3.ast.GQLVariableValue import com.apollographql.apollo3.ast.InferredVariable import com.apollographql.apollo3.ast.Issue +import com.apollographql.apollo3.ast.NullabilityValidationIgnore +import com.apollographql.apollo3.ast.NullabilityValidationRegister import com.apollographql.apollo3.ast.Schema import com.apollographql.apollo3.ast.SourceLocation import com.apollographql.apollo3.ast.VariableUsage @@ -218,15 +216,7 @@ internal class ExecutableValidationScope( } if (nullability != null) { - val typeListDimension = fieldDefinition.type.listDimension() - val nullabilityListDimension = nullability.listDimension() - if (typeListDimension < nullabilityListDimension) { - registerIssue( - message = "Cannot apply nullability on '$name', the nullability list dimension exceeds the one of the field type.", - sourceLocation = nullability.sourceLocation - ) - return - } + fieldDefinition.type.withNullability(nullability, NullabilityValidationRegister(issues, name)) } directives.forEach { @@ -236,22 +226,6 @@ internal class ExecutableValidationScope( } } - private fun GQLType.listDimension(): Int { - return when (this) { - is GQLNonNullType -> this.type.listDimension() - is GQLListType -> 1 + this.type.listDimension() - else -> 0 - } - } - - private fun GQLNullability.listDimension(): Int { - return when (this) { - is GQLListNullability -> 1 + this.itemNullability.listDimension() - is GQLNullDesignator -> 0 - is GQLNonNullDesignator -> 0 - } - } - private fun GQLInlineFragment.validate(parentTypeDefinition: GQLTypeDefinition, selectionSetParent: GQLNode, path: String) { val tc = typeCondition?.name ?: parentTypeDefinition.name val inlineFragmentTypeDefinition = typeDefinitions[tc] @@ -517,8 +491,8 @@ internal class ExecutableValidationScope( val fieldA = fieldWithParentA.field val fieldB = fieldWithParentB.field - val typeA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)?.type?.withNullability(fieldA.nullability) - val typeB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)?.type?.withNullability(fieldB.nullability) + val typeA = fieldA.definitionFromScope(schema, parentTypeDefinitionA)?.type?.withNullability(fieldA.nullability, NullabilityValidationIgnore) + val typeB = fieldB.definitionFromScope(schema, parentTypeDefinitionB)?.type?.withNullability(fieldB.nullability, NullabilityValidationIgnore) if (typeA == null || typeB == null) { // will be caught by other validation rules return diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt index 50efb98196f..5ca9a510269 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt @@ -42,6 +42,10 @@ internal class Parser( return parseTopLevel(::parseTypeInternal) } + fun parseNullability(): GQLNullability? { + return parseTopLevel(::parseNullabilityInternal) + } + private fun advance() { lastToken = token if (lookaheadToken != null) { @@ -217,7 +221,7 @@ internal class Parser( val arguments = parseArguments(const = false) var nullability: GQLNullability? = null if (allowClientControlledNullability) { - nullability = parseNullability() + nullability = parseNullabilityInternal() } val directives = parseDirectives(const = false) @@ -253,7 +257,7 @@ internal class Parser( } } - private fun parseNullability(): GQLNullability? { + private fun parseNullabilityInternal(): GQLNullability? { return when (token) { is Token.LeftBracket -> { parseListNullability() @@ -265,17 +269,12 @@ internal class Parser( } private fun parseListNullability(): GQLListNullability { - val start = token val sourceLocation = sourceLocation() expectToken() - val ofNullability = parseNullability() + val ofNullability = parseNullabilityInternal() expectToken() - if (ofNullability == null) { - throw ParserException("List nullability must not be empty", start) - } - return GQLListNullability( sourceLocation = sourceLocation, itemNullability = ofNullability, diff --git a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/GQLTest.kt b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/GQLTest.kt index 32ea23e469a..e7969afa087 100644 --- a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/GQLTest.kt +++ b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/GQLTest.kt @@ -2,6 +2,7 @@ package com.apollographql.apollo3.graphql.ast.test import com.apollographql.apollo3.ast.GQLListNullability import com.apollographql.apollo3.ast.GQLNullDesignator +import com.apollographql.apollo3.ast.parseAsGQLNullability import com.apollographql.apollo3.ast.parseAsGQLType import com.apollographql.apollo3.ast.pretty import com.apollographql.apollo3.ast.withNullability @@ -15,4 +16,15 @@ class GQLTest { assertEquals("[String]!", newType.pretty()) } + + @Test + fun nullability() { + try { + "[[[String]]]".parseAsGQLType().getOrThrow().withNullability("[[[[!]]]]".parseAsGQLNullability().getOrThrow()) + error("an exception was expected") + } catch (e: Exception) { + assertEquals(true, e.message?.contains("the nullability list dimension exceeds the one of the field type")) + } + + } } \ No newline at end of file diff --git a/libraries/apollo-ast/test-fixtures/parser/ccn-empty-list.expected b/libraries/apollo-ast/test-fixtures/parser/ccn-empty-list.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libraries/apollo-ast/test-fixtures/parser/ccn-invalid.graphql b/libraries/apollo-ast/test-fixtures/parser/ccn-empty-list.graphql similarity index 100% rename from libraries/apollo-ast/test-fixtures/parser/ccn-invalid.graphql rename to libraries/apollo-ast/test-fixtures/parser/ccn-empty-list.graphql diff --git a/libraries/apollo-ast/test-fixtures/parser/ccn-invalid.expected b/libraries/apollo-ast/test-fixtures/parser/ccn-invalid.expected deleted file mode 100644 index 9b8b6b97789..00000000000 --- a/libraries/apollo-ast/test-fixtures/parser/ccn-invalid.expected +++ /dev/null @@ -1,2 +0,0 @@ -ERROR: ParsingError (2:7) -List nullability must not be empty \ No newline at end of file diff --git a/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.expected b/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.expected index 54972f54bb1..9e6e8d55b6e 100644 --- a/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.expected +++ b/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.expected @@ -1,5 +1,5 @@ -ERROR: ValidationError (5:13) +ERROR: ValidationError (5:14) Cannot apply nullability on 'nullable', the nullability list dimension exceeds the one of the field type. ------------ -ERROR: ValidationError (6:17) +ERROR: ValidationError (7:21) Cannot apply nullability on 'deepList', the nullability list dimension exceeds the one of the field type. \ No newline at end of file diff --git a/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.graphql b/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.graphql index f4a9e270d8f..a5370b34e18 100644 --- a/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.graphql +++ b/libraries/apollo-compiler/src/test/validation/operation/ccn/ccn.graphql @@ -3,6 +3,7 @@ query Foo { nonNullable? # too much nesting nullable[!]! + # too much nesting a0: deepList[[[[!]]]] # this is ok a1: deepList[[!]] diff --git a/tests/ccn/src/main/graphql/operation.graphql b/tests/ccn/src/main/graphql/operation.graphql index a8b72361565..bc54aff6baa 100644 --- a/tests/ccn/src/main/graphql/operation.graphql +++ b/tests/ccn/src/main/graphql/operation.graphql @@ -11,5 +11,8 @@ query GetList { enemies[!]! { name } + frenemies: enemies[]! { + name + } } } \ No newline at end of file diff --git a/tests/ccn/src/test/kotlin/test/CcnTest.kt b/tests/ccn/src/test/kotlin/test/CcnTest.kt index 78c968bc5df..d02f4b790d9 100644 --- a/tests/ccn/src/test/kotlin/test/CcnTest.kt +++ b/tests/ccn/src/test/kotlin/test/CcnTest.kt @@ -40,7 +40,8 @@ class CcnTest { { "name": "nullability" } - ] + ], + "frenemies": [] } } } @@ -54,5 +55,6 @@ class CcnTest { } assertEquals(null, response.data!!.user!!.friends[0]?.name) assertEquals("nullability", response.data!!.user!!.enemies[0].name) + assertEquals(0, response.data!!.user!!.frenemies.size) } }