Skip to content

Commit 367714a

Browse files
BoDmartinbonnin
andauthored
Add validation to check schema definitions are compatible with the bundled ones (#5444)
* Fix an NPE when a non-imported @catch directive is used * Validation: check that directives/enums with the same name as those in nullability are semantically equal * Update API dump * Merge IncompatibleDirectiveDefinition and IncompatibleEnumDefinition * Directives are order sensitive * Simplify semanticEquals * Make KOTLIN_LABS_VERSION and NULLABILITY_VERSION internal * Update libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt Co-authored-by: Martin Bonnin <[email protected]> * Improve semanticEquals following review * Add support for GQLInputObjectTypeDefinition and GQLScalarTypeDefinition in semanticEquals * Remove useless and failing test * Revert isDefinedAndMatchesOriginalName --------- Co-authored-by: Martin Bonnin <[email protected]>
1 parent 1c939f9 commit 367714a

File tree

9 files changed

+293
-17
lines changed

9 files changed

+293
-17
lines changed

libraries/apollo-ast/api/apollo-ast.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ public abstract interface class com/apollographql/apollo3/ast/GraphQLIssue : com
827827
public abstract interface class com/apollographql/apollo3/ast/GraphQLValidationIssue : com/apollographql/apollo3/ast/GraphQLIssue {
828828
}
829829

830-
public final class com/apollographql/apollo3/ast/IncompatibleDirectiveDefinition : com/apollographql/apollo3/ast/GraphQLValidationIssue {
830+
public final class com/apollographql/apollo3/ast/IncompatibleDefinition : com/apollographql/apollo3/ast/GraphQLValidationIssue {
831831
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lcom/apollographql/apollo3/ast/SourceLocation;)V
832832
public fun getMessage ()Ljava/lang/String;
833833
public fun getSourceLocation ()Lcom/apollographql/apollo3/ast/SourceLocation;

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/Issue.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ class UnknownDirective @ApolloInternal constructor(
4949
}
5050

5151
/**
52-
* The directive definition is inconsistent with the expected one.
52+
* The definition is inconsistent with the expected one.
5353
*/
54-
class IncompatibleDirectiveDefinition(
55-
directiveName: String,
54+
class IncompatibleDefinition(
55+
name: String,
5656
expectedDefinition: String,
5757
override val sourceLocation: SourceLocation?,
5858
) : GraphQLValidationIssue {
59-
override val message = "Unexpected '@$directiveName' directive definition. Expecting '$expectedDefinition'."
59+
override val message = "Unexpected '$name' definition. Expecting '$expectedDefinition'."
6060
}
6161

6262
/**

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gqldocument.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.apollographql.apollo3.ast
22

33
import com.apollographql.apollo3.annotations.ApolloDeprecatedSince
44
import com.apollographql.apollo3.annotations.ApolloExperimental
5+
import com.apollographql.apollo3.annotations.ApolloInternal
56
import com.apollographql.apollo3.ast.internal.ExtensionsMerger
67
import com.apollographql.apollo3.ast.internal.builtinsDefinitionsStr
78
import com.apollographql.apollo3.ast.internal.ensureSchemaDefinition
@@ -79,23 +80,27 @@ fun builtinDefinitions() = definitionsFromString(builtinsDefinitionsStr)
7980
*/
8081
fun linkDefinitions() = definitionsFromString(linkDefinitionsStr)
8182

83+
@ApolloInternal const val KOTLIN_LABS_VERSION = "v0.2"
84+
8285
/**
8386
* Extra apollo Kotlin specific definitions from https://specs.apollo.dev/kotlin_labs/<[version]>
8487
*/
8588
fun kotlinLabsDefinitions(version: String): List<GQLDefinition> {
8689
return definitionsFromString(when (version) {
87-
"v0.2" -> kotlinLabsDefinitions
88-
else -> error("kotlin_labs/$version definitions are not supported, please use v0.2")
90+
KOTLIN_LABS_VERSION -> kotlinLabsDefinitions
91+
else -> error("kotlin_labs/$version definitions are not supported, please use $KOTLIN_LABS_VERSION")
8992
})
9093
}
9194

95+
@ApolloInternal const val NULLABILITY_VERSION = "v0.1"
96+
9297
/**
9398
* Extra nullability definitions from https://specs.apollo.dev/nullability/<[version]>
9499
*/
95100
fun nullabilityDefinitions(version: String): List<GQLDefinition> {
96101
return definitionsFromString(when (version) {
97-
"v0.1" -> nullabilityDefinitionsStr
98-
else -> error("nullability/$version definitions are not supported, please use v0.1")
102+
NULLABILITY_VERSION -> nullabilityDefinitionsStr
103+
else -> error("nullability/$version definitions are not supported, please use $NULLABILITY_VERSION")
99104
})
100105
}
101106

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ import com.apollographql.apollo3.ast.GQLTypeDefinition
2929
import com.apollographql.apollo3.ast.GQLTypeDefinition.Companion.builtInTypes
3030
import com.apollographql.apollo3.ast.GQLTypeSystemExtension
3131
import com.apollographql.apollo3.ast.GQLUnionTypeDefinition
32-
import com.apollographql.apollo3.ast.IncompatibleDirectiveDefinition
32+
import com.apollographql.apollo3.ast.IncompatibleDefinition
3333
import com.apollographql.apollo3.ast.Issue
34+
import com.apollographql.apollo3.ast.KOTLIN_LABS_VERSION
3435
import com.apollographql.apollo3.ast.MergeOptions
36+
import com.apollographql.apollo3.ast.NULLABILITY_VERSION
3537
import com.apollographql.apollo3.ast.NoQueryType
3638
import com.apollographql.apollo3.ast.OtherValidationIssue
3739
import com.apollographql.apollo3.ast.Schema
@@ -63,7 +65,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
6365

6466
var directivesToStrip = foreignSchemas.flatMap { it.directivesToStrip }
6567

66-
val kotlinLabsDefinitions = kotlinLabsDefinitions("v0.2")
68+
val kotlinLabsDefinitions = kotlinLabsDefinitions(KOTLIN_LABS_VERSION)
6769

6870
if (requiresApolloDefinitions && foreignSchemas.none { it.name == "kotlin_labs" }) {
6971
/**
@@ -132,9 +134,33 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
132134
}
133135
}
134136

137+
nullabilityDefinitions(NULLABILITY_VERSION).forEach { definition ->
138+
when (definition) {
139+
is GQLDirectiveDefinition -> {
140+
val existing = directiveDefinitions[definition.name]
141+
if (existing != null) {
142+
if (!existing.semanticEquals(definition)) {
143+
issues.add(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
144+
}
145+
}
146+
}
147+
148+
is GQLEnumTypeDefinition -> {
149+
val existing = typeDefinitions[definition.name]
150+
if (existing != null) {
151+
if (!existing.semanticEquals(definition)) {
152+
issues.add(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
153+
}
154+
}
155+
}
156+
157+
else -> {}
158+
}
159+
}
160+
135161
directiveDefinitions[Schema.ONE_OF]?.let {
136162
if (it.locations != listOf(GQLDirectiveLocation.INPUT_OBJECT) || it.arguments.isNotEmpty() || it.repeatable) {
137-
issues.add(IncompatibleDirectiveDefinition(Schema.ONE_OF, "directive @oneOf on INPUT_OBJECT", it.sourceLocation))
163+
issues.add(IncompatibleDefinition(Schema.ONE_OF, "directive @oneOf on INPUT_OBJECT", it.sourceLocation))
138164
}
139165
}
140166

@@ -491,6 +517,7 @@ private fun ValidationScope.validateCatch(schemaDefinition: GQLSchemaDefinition?
491517
}
492518

493519
}
520+
494521
private fun ValidationScope.validateInputObjects() {
495522
typeDefinitions.values.filterIsInstance<GQLInputObjectTypeDefinition>().forEach { o ->
496523
if (o.inputFields.isEmpty()) {
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package com.apollographql.apollo3.ast.internal
2+
3+
import com.apollographql.apollo3.ast.GQLArgument
4+
import com.apollographql.apollo3.ast.GQLBooleanValue
5+
import com.apollographql.apollo3.ast.GQLDirective
6+
import com.apollographql.apollo3.ast.GQLDirectiveDefinition
7+
import com.apollographql.apollo3.ast.GQLEnumTypeDefinition
8+
import com.apollographql.apollo3.ast.GQLEnumValue
9+
import com.apollographql.apollo3.ast.GQLEnumValueDefinition
10+
import com.apollographql.apollo3.ast.GQLFloatValue
11+
import com.apollographql.apollo3.ast.GQLInputObjectTypeDefinition
12+
import com.apollographql.apollo3.ast.GQLInputValueDefinition
13+
import com.apollographql.apollo3.ast.GQLIntValue
14+
import com.apollographql.apollo3.ast.GQLListType
15+
import com.apollographql.apollo3.ast.GQLListValue
16+
import com.apollographql.apollo3.ast.GQLNamed
17+
import com.apollographql.apollo3.ast.GQLNamedType
18+
import com.apollographql.apollo3.ast.GQLNode
19+
import com.apollographql.apollo3.ast.GQLNonNullType
20+
import com.apollographql.apollo3.ast.GQLNullValue
21+
import com.apollographql.apollo3.ast.GQLObjectValue
22+
import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
23+
import com.apollographql.apollo3.ast.GQLStringValue
24+
import com.apollographql.apollo3.ast.GQLVariableValue
25+
import com.apollographql.apollo3.ast.toUtf8
26+
27+
/**
28+
* Returns true if the two nodes are semantically equal, which ignores the source location and the description.
29+
* Note that not all cases are implemented - currently [GQLEnumTypeDefinition] and [GQLDirectiveDefinition] are fully supported, and
30+
* unsupported types will throw.
31+
*/
32+
internal fun GQLNode.semanticEquals(other: GQLNode?): Boolean {
33+
if (other == null) return false
34+
when (this) {
35+
is GQLDirectiveDefinition -> {
36+
if (other !is GQLDirectiveDefinition) {
37+
return false
38+
}
39+
40+
if (locations != other.locations) {
41+
return false
42+
}
43+
44+
if (repeatable != other.repeatable) {
45+
return false
46+
}
47+
}
48+
49+
is GQLInputValueDefinition -> {
50+
if (other !is GQLInputValueDefinition) {
51+
return false
52+
}
53+
54+
if (!type.semanticEquals(other.type)) {
55+
return false
56+
}
57+
58+
if (defaultValue != null) {
59+
if (!defaultValue.semanticEquals(other.defaultValue)) {
60+
return false
61+
}
62+
} else if (other.defaultValue != null) {
63+
return false
64+
}
65+
}
66+
67+
is GQLNonNullType -> {
68+
if (other !is GQLNonNullType) {
69+
return false
70+
}
71+
}
72+
73+
is GQLListType -> {
74+
if (other !is GQLListType) {
75+
return false
76+
}
77+
}
78+
79+
is GQLNamedType -> {
80+
if (other !is GQLNamedType) {
81+
return false
82+
}
83+
}
84+
85+
is GQLNullValue -> {
86+
if (other !is GQLNullValue) {
87+
return false
88+
}
89+
}
90+
91+
is GQLListValue -> {
92+
if (other !is GQLListValue) {
93+
return false
94+
}
95+
}
96+
97+
is GQLObjectValue -> {
98+
if (other !is GQLObjectValue) {
99+
return false
100+
}
101+
}
102+
103+
is GQLStringValue -> {
104+
if (other !is GQLStringValue) {
105+
return false
106+
}
107+
if (value != other.value) {
108+
return false
109+
}
110+
}
111+
112+
is GQLBooleanValue -> {
113+
if (other !is GQLBooleanValue) {
114+
return false
115+
}
116+
if (value != other.value) {
117+
return false
118+
}
119+
}
120+
121+
is GQLIntValue -> {
122+
if (other !is GQLIntValue) {
123+
return false
124+
}
125+
if (value != other.value) {
126+
return false
127+
}
128+
}
129+
130+
is GQLFloatValue -> {
131+
if (other !is GQLFloatValue) {
132+
return false
133+
}
134+
if (value != other.value) {
135+
return false
136+
}
137+
}
138+
139+
is GQLEnumValue -> {
140+
if (other !is GQLEnumValue) {
141+
return false
142+
}
143+
if (value != other.value) {
144+
return false
145+
}
146+
}
147+
148+
is GQLVariableValue -> {
149+
if (other !is GQLVariableValue) {
150+
return false
151+
}
152+
}
153+
154+
is GQLEnumTypeDefinition -> {
155+
if (other !is GQLEnumTypeDefinition) {
156+
return false
157+
}
158+
}
159+
160+
is GQLDirective -> {
161+
if (other !is GQLDirective) {
162+
return false
163+
}
164+
}
165+
166+
is GQLArgument -> {
167+
if (other !is GQLArgument) {
168+
return false
169+
}
170+
}
171+
172+
is GQLEnumValueDefinition -> {
173+
if (other !is GQLEnumValueDefinition) {
174+
return false
175+
}
176+
}
177+
178+
is GQLInputObjectTypeDefinition -> {
179+
if (other !is GQLInputObjectTypeDefinition) {
180+
return false
181+
}
182+
}
183+
184+
is GQLScalarTypeDefinition -> {
185+
if (other !is GQLScalarTypeDefinition) {
186+
return false
187+
}
188+
}
189+
190+
else -> {
191+
TODO("semanticEquals not supported for ${this::class.simpleName}")
192+
}
193+
}
194+
195+
if (this is GQLNamed) {
196+
if (other !is GQLNamed) {
197+
return false
198+
}
199+
if (name != other.name) {
200+
return false
201+
}
202+
}
203+
204+
if (children.size != other.children.size) {
205+
return false
206+
}
207+
for (i in children.indices) {
208+
if (!children[i].semanticEquals(other.children[i])) {
209+
return false
210+
}
211+
}
212+
return true
213+
}
214+
215+
internal fun GQLDirectiveDefinition.toSemanticSdl(): String {
216+
return copy(description = null, arguments = arguments.map { it.copy(description = null) }).toUtf8().trim()
217+
}
218+
219+
internal fun GQLEnumTypeDefinition.toSemanticSdl(): String {
220+
return copy(description = null, enumValues = enumValues.map { it.copy(description = null) }).toUtf8().replace(Regex("[\\n ]+"), " ").trim()
221+
}

libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ApolloCompiler.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import com.apollographql.apollo3.ast.GQLOperationDefinition
1212
import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
1313
import com.apollographql.apollo3.ast.GQLSchemaDefinition
1414
import com.apollographql.apollo3.ast.GQLTypeDefinition
15-
import com.apollographql.apollo3.ast.IncompatibleDirectiveDefinition
15+
import com.apollographql.apollo3.ast.IncompatibleDefinition
1616
import com.apollographql.apollo3.ast.Issue
17+
import com.apollographql.apollo3.ast.KOTLIN_LABS_VERSION
1718
import com.apollographql.apollo3.ast.ParserOptions
1819
import com.apollographql.apollo3.ast.QueryDocumentMinifier
1920
import com.apollographql.apollo3.ast.Schema
@@ -563,7 +564,7 @@ internal fun List<Issue>.group(
563564
val ignored = mutableListOf<Issue>()
564565
val warnings = mutableListOf<Issue>()
565566
val errors = mutableListOf<Issue>()
566-
val apolloDirectives = kotlinLabsDefinitions("v0.2").mapNotNull { (it as? GQLDirectiveDefinition)?.name }.toSet()
567+
val apolloDirectives = kotlinLabsDefinitions(KOTLIN_LABS_VERSION).mapNotNull { (it as? GQLDirectiveDefinition)?.name }.toSet()
567568

568569
forEach {
569570
val severity = when (it) {
@@ -576,7 +577,7 @@ internal fun List<Issue>.group(
576577
* Because some users might have added the apollo directive to their schema, we just let that through for now
577578
*/
578579
is DirectiveRedefinition -> if (it.name in apolloDirectives) Severity.None else Severity.Warning
579-
is IncompatibleDirectiveDefinition -> Severity.Warning
580+
is IncompatibleDefinition -> Severity.Warning
580581
else -> Severity.Error
581582
}
582583

libraries/apollo-compiler/src/test/validation/schema/oneof-input-object-unexpected.expected

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)