Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[⚒️compiler] Track schemanticNonNull spec #5577

Merged
merged 6 commits into from
Jan 29, 2024
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
8 changes: 7 additions & 1 deletion .idea/runConfigurations/ValidationTest.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
<p>
Before being referenced, directives and types supported by Apollo Kotlin must be imported by your schema using the <code>@link</code> directive<br>.
For instance, to use the <code>@semanticNonNull</code> directive, import it from the
<a href="https://specs.apollo.dev/nullability/v0.1"><code>nullability</code></a> definitions:
<a href="https://specs.apollo.dev/nullability/v0.2"><code>nullability</code></a> definitions:
<pre>
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.1",
url: "https://specs.apollo.dev/nullability/v0.2",
import: ["@semanticNonNull"]
)
</pre>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.1",
url: "https://specs.apollo.dev/nullability/v0.2",
import: ["@catch"]
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.1",
url: "https://specs.apollo.dev/nullability/v0.2",
import: ["@catch", "CatchTo"]
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.1",
url: "https://specs.apollo.dev/nullability/v0.2",
import: ["@catch", "CatchTo"]
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.1",
url: "https://specs.apollo.dev/nullability/v0.2",
import: ["@catch", "CatchTo"]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extend schema

extend schema
@link(
url: "https://specs.apollo.dev/nullability/v0.1",
url: "https://specs.apollo.dev/nullability/v0.2",
import: ["@catch", "CatchTo"]
)

Expand Down
1 change: 1 addition & 0 deletions libraries/apollo-ast/api/apollo-ast.api
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,7 @@ public final class com/apollographql/apollo3/ast/Schema {
public static final field OPTIONAL Ljava/lang/String;
public static final field REQUIRES_OPT_IN Ljava/lang/String;
public static final field SEMANTIC_NON_NULL Ljava/lang/String;
public static final field SEMANTIC_NON_NULL_FIELD Ljava/lang/String;
public static final field TYPE_POLICY Ljava/lang/String;
public final fun getDirectiveDefinitions ()Ljava/util/Map;
public final fun getErrorAware ()Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ class Schema internal constructor(
@ApolloExperimental
const val SEMANTIC_NON_NULL = "semanticNonNull"
@ApolloExperimental
const val SEMANTIC_NON_NULL_FIELD = "semanticNonNullField"
@ApolloExperimental
const val IGNORE_ERRORS = "ignoreErrors"

const val FIELD_POLICY_FOR_FIELD = "forField"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ enum class CatchTo {
}

@ApolloInternal
data class Catch(val to: CatchTo, val level: Int?)
data class Catch(val to: CatchTo, val levels: List<Int>)

private fun GQLDirectiveDefinition.getArgumentDefaultValue(argName: String): GQLValue? {
return arguments.firstOrNull { it.name == argName }?.defaultValue
}

@ApolloInternal
fun GQLDirective.getArgument(argName: String, schema: Schema): GQLValue? {
fun GQLDirective.getArgumentValueOrDefault(argName: String, schema: Schema): GQLValue? {
val directiveDefinition: GQLDirectiveDefinition = schema.directiveDefinitions.get(name)!!
val argument = arguments.firstOrNull { it.name == argName }
if (argument == null) {
Expand All @@ -99,6 +99,18 @@ fun GQLDirective.getArgument(argName: String, schema: Schema): GQLValue? {
return argument.value
}

private fun GQLValue.toListOfInt(): List<Int> {
check(this is GQLListValue) {
error("${sourceLocation}: expected a list value")
}
return this.values.map {
check(it is GQLIntValue) {
error("${it.sourceLocation}: expected an int value")
}
it.value.toInt()
}
}

private fun GQLValue.toIntOrNull(): Int? {
return when (this) {
is GQLNullValue -> null
Expand Down Expand Up @@ -136,32 +148,39 @@ private fun GQLValue?.toCatchTo(): CatchTo {
}

@ApolloInternal
fun List<GQLDirective>.findCatches(schema: Schema): List<Catch> {
fun List<GQLDirective>.findCatch(schema: Schema): Catch? {
return filter {
schema.originalDirectiveName(it.name) == Schema.CATCH
}.map {
Catch(
to = it.getArgument("to", schema).toCatchTo(),
level = it.getArgument("level", schema)?.toIntOrNull(),
to = it.getArgumentValueOrDefault("to", schema).toCatchTo(),
levels = it.getArgumentValueOrDefault("levels", schema)!!.toListOfInt(),
)
}
}.singleOrNull()
}

@ApolloInternal
fun GQLFieldDefinition.findSemanticNonNulls(schema: Schema): List<Int?> {
return directives.filter {
fun GQLFieldDefinition.findSemanticNonNulls(schema: Schema): List<Int> {
val semanticNonNulls = directives.filter {
schema.originalDirectiveName(it.name) == Schema.SEMANTIC_NON_NULL
}.map {
it.getArgument("level", schema)?.toIntOrNull()
}

val semanticNonNull = semanticNonNulls.singleOrNull()
if (semanticNonNull == null) {
return emptyList()
}
return semanticNonNull.getArgumentValueOrDefault("levels", schema)!!.toListOfInt()
}

@ApolloInternal
fun GQLTypeDefinition.findSemanticNonNulls(fieldName: String, schema: Schema): List<Int?> {
return directives.filter {
schema.originalDirectiveName(it.name) == Schema.SEMANTIC_NON_NULL
&& it.getArgument("field", schema)?.toStringOrNull() == fieldName
}.map {
it.getArgument("level", schema)?.toIntOrNull()
fun GQLTypeDefinition.findSemanticNonNulls(fieldName: String, schema: Schema): List<Int> {
val semanticNonNulls = directives.filter {
schema.originalDirectiveName(it.name) == Schema.SEMANTIC_NON_NULL_FIELD
&& it.getArgumentValueOrDefault("name", schema)?.toStringOrNull() == fieldName
}
val semanticNonNull = semanticNonNulls.singleOrNull()
if (semanticNonNull == null) {
return emptyList()
}
return semanticNonNull.getArgumentValueOrDefault("levels", schema)!!.toListOfInt()
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fun kotlinLabsDefinitions(version: String): List<GQLDefinition> {
})
}

@ApolloInternal const val NULLABILITY_VERSION = "v0.1"
@ApolloInternal const val NULLABILITY_VERSION = "v0.2"

/**
* Extra nullability definitions from https://specs.apollo.dev/nullability/<[version]>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,56 @@
package com.apollographql.apollo3.ast.internal

import com.apollographql.apollo3.annotations.ApolloInternal
import com.apollographql.apollo3.ast.*
import com.apollographql.apollo3.ast.AnonymousOperation
import com.apollographql.apollo3.ast.Catch
import com.apollographql.apollo3.ast.DeprecatedUsage
import com.apollographql.apollo3.ast.DifferentShape
import com.apollographql.apollo3.ast.ExecutableValidationResult
import com.apollographql.apollo3.ast.GQLArgument
import com.apollographql.apollo3.ast.GQLBooleanValue
import com.apollographql.apollo3.ast.GQLDirective
import com.apollographql.apollo3.ast.GQLDocument
import com.apollographql.apollo3.ast.GQLEnumTypeDefinition
import com.apollographql.apollo3.ast.GQLEnumValue
import com.apollographql.apollo3.ast.GQLField
import com.apollographql.apollo3.ast.GQLFieldDefinition
import com.apollographql.apollo3.ast.GQLFloatValue
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.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.GQLObjectTypeDefinition
import com.apollographql.apollo3.ast.GQLObjectValue
import com.apollographql.apollo3.ast.GQLOperationDefinition
import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
import com.apollographql.apollo3.ast.GQLSelection
import com.apollographql.apollo3.ast.GQLStringValue
import com.apollographql.apollo3.ast.GQLType
import com.apollographql.apollo3.ast.GQLTypeDefinition
import com.apollographql.apollo3.ast.GQLValue
import com.apollographql.apollo3.ast.GQLVariableValue
import com.apollographql.apollo3.ast.Issue
import com.apollographql.apollo3.ast.OtherValidationIssue
import com.apollographql.apollo3.ast.Schema
import com.apollographql.apollo3.ast.UnusedFragment
import com.apollographql.apollo3.ast.UnusedVariable
import com.apollographql.apollo3.ast.VariableUsage
import com.apollographql.apollo3.ast.definitionFromScope
import com.apollographql.apollo3.ast.findCatch
import com.apollographql.apollo3.ast.findDeprecationReason
import com.apollographql.apollo3.ast.getArgumentValueOrDefault
import com.apollographql.apollo3.ast.internal.validation.validateDeferLabels
import com.apollographql.apollo3.ast.pretty
import com.apollographql.apollo3.ast.rawType
import com.apollographql.apollo3.ast.responseName
import com.apollographql.apollo3.ast.rootTypeDefinition
import com.apollographql.apollo3.ast.sharesPossibleTypesWith


@OptIn(ApolloInternal::class)
Expand Down Expand Up @@ -187,8 +235,6 @@ internal class ExecutableValidationScope(
}
}

private class CatchUsage(val level: Int?, val catchTo: String, val sourceLocation: SourceLocation?)

private fun GQLType.maxDimension(): Int {
var dimension = 0
var type = this
Expand All @@ -209,57 +255,42 @@ internal class ExecutableValidationScope(
}

private fun GQLField.validateCatches(fieldDefinition: GQLFieldDefinition) {
val levelToCatch = directives.filter {
val catches = directives.filter {
schema.originalDirectiveName(it.name) == Schema.CATCH
}.mapNotNull {
val to = it.getArgument("to", schema) as? GQLEnumValue
if (to == null) {
// caught by other validation rules
return@mapNotNull null
}
val levelInt = when (val levelValue = it.getArgument("level", schema)) {
is GQLNullValue -> null
is GQLIntValue -> levelValue.value.toIntOrNull() ?: error("`@catch` level too big: ${levelValue.value}")
else -> return@mapNotNull null
}
}

if (levelInt != null) {
val maxDimension = fieldDefinition.type.maxDimension()
if (levelInt > maxDimension) {
registerIssue(
message = "Invalid 'level' value '$levelInt' for `@catch` usage: this type has a max list level of $maxDimension",
sourceLocation = it.sourceLocation
)
return@mapNotNull null
}
}
CatchUsage(levelInt, to.value, it.sourceLocation)
}.groupBy {
it.level
}.mapNotNull {
val sameLevel = it.value.distinct()
if (sameLevel.size > 1) {
registerIssue(
message = "Conflicting `@catch` usages for level '${it.key}': a given list level must have a single CatchTo value",
sourceLocation = it.value.first().sourceLocation
)
return@mapNotNull null
}
if (catches.size > 1) {
// "caught" (ahah) by other validation rules
return
}

it.value.single()
}.associateBy {
it.level
val catch = catches.singleOrNull()
if (catch == null) {
return
}

val default = levelToCatch[null]?.catchTo
levelToCatch.filter {
it.key != null
}.forEach {
if (default != null && it.value.catchTo != default) {
registerIssue(
message = "Conflicting `@catch` usages for level '${it.key}': this level conflicts with the default 'null' level",
sourceLocation = it.value.sourceLocation
)
val levels = catch.getArgumentValueOrDefault("levels", schema)
val maxDimension = fieldDefinition.type.maxDimension()
if (levels is GQLListValue) {
levels.values.forEach {
if (it is GQLIntValue) {
val asInt = it.value.toIntOrNull()
if (asInt == null) {
registerIssue("Invalid value: '${it.value}'", it.sourceLocation)
return@forEach
}
if (asInt > maxDimension) {
registerIssue(
message = "Invalid 'levels' value '$asInt' for `@catch` usage: this field has a max list level of $maxDimension",
sourceLocation = it.sourceLocation
)
} else if (asInt < 0) {
registerIssue(
message = "'levels' values must be positive ints",
sourceLocation = it.sourceLocation
)
}
}
}
}
}
Expand Down Expand Up @@ -481,7 +512,7 @@ internal class ExecutableValidationScope(
addFieldMergingIssue(fieldWithParentA.field, fieldWithParentB.field, "they have different types")
return
}
if (hasCatch && !areCatchesEqual(fieldA.directives.findCatches(schema), fieldB.directives.findCatches(schema))) {
if (hasCatch && !areCatchesEqual(fieldA.directives.findCatch(schema), fieldB.directives.findCatch(schema))) {
addFieldMergingIssue(fieldWithParentA.field, fieldWithParentB.field, "they have different `@catch` directives")
return
}
Expand Down Expand Up @@ -533,8 +564,8 @@ internal class ExecutableValidationScope(
}
}

private fun areCatchesEqual(catchesA: List<Catch>, catchesB: List<Catch>): Boolean {
return catchesA.toSet() == catchesB.toSet()
private fun areCatchesEqual(catchA: Catch?, catchesB: Catch?): Boolean {
return catchA == catchesB
}

private fun areArgumentsEqual(argumentsA: List<GQLArgument>, argumentsB: List<GQLArgument>): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
val existing = directiveDefinitions[definition.name]
if (existing != null) {
if (!existing.semanticEquals(definition)) {
issues.add(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
issues.add(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), existing.sourceLocation))
}
}
}
Expand All @@ -149,7 +149,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
val existing = typeDefinitions[definition.name]
if (existing != null) {
if (!existing.semanticEquals(definition)) {
issues.add(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
issues.add(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), existing.sourceLocation))
}
}
}
Expand Down
Loading
Loading