Skip to content

Commit 07d415f

Browse files
feature: Adds VariablesInAllowedPositionRule
1 parent f7bb35a commit 07d415f

File tree

4 files changed

+496
-2
lines changed

4 files changed

+496
-2
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
2+
/**
3+
* Variables in allowed position
4+
*
5+
* Variable usages must be compatible with the arguments they are passed to.
6+
*
7+
* See https://spec.graphql.org/draft/#sec-All-Variable-Usages-are-Allowed
8+
*/
9+
func VariablesInAllowedPositionRule(context: ValidationContext) -> Visitor {
10+
var varDefMap: [String: VariableDefinition] = [:]
11+
return Visitor(
12+
enter: { node, _, _, _, _ in
13+
switch node {
14+
case _ as OperationDefinition:
15+
varDefMap = [:]
16+
case let variableDefinition as VariableDefinition:
17+
varDefMap[variableDefinition.variable.name.value] = variableDefinition
18+
default:
19+
break
20+
}
21+
return .continue
22+
},
23+
leave: { node, _, _, _, _ in
24+
switch node {
25+
case let operation as OperationDefinition:
26+
let usages = context.getRecursiveVariableUsages(operation: operation)
27+
28+
for usage in usages {
29+
let varName = usage.node.name.value
30+
let schema = context.schema
31+
32+
if
33+
let varDef = varDefMap[varName],
34+
let type = usage.type,
35+
let varType = typeFromAST(schema: schema, inputTypeAST: varDef.type)
36+
{
37+
// A var type is allowed if it is the same or more strict (e.g. is
38+
// a subtype of) than the expected type. It can be more strict if
39+
// the variable type is non-null when the expected type is nullable.
40+
// If both are list types, the variable item type can be more strict
41+
// than the expected item type (contravariant).
42+
let isAllowed = (try? allowedVariableUsage(
43+
schema: schema,
44+
varType: varType,
45+
varDefaultValue: varDef.defaultValue,
46+
locationType: type,
47+
locationDefaultValue: usage.defaultValue
48+
)) ?? false
49+
if !isAllowed {
50+
context.report(
51+
error: GraphQLError(
52+
message: "Variable \"$\(varName)\" of type \"\(varType)\" used in position expecting type \"\(type)\".",
53+
nodes: [varDef, usage.node]
54+
)
55+
)
56+
}
57+
}
58+
}
59+
default:
60+
break
61+
}
62+
return .continue
63+
}
64+
)
65+
}
66+
67+
/**
68+
* Returns true if the variable is allowed in the location it was found,
69+
* which includes considering if default values exist for either the variable
70+
* or the location at which it is located.
71+
*/
72+
func allowedVariableUsage(
73+
schema: GraphQLSchema,
74+
varType: GraphQLType,
75+
varDefaultValue: Value?,
76+
locationType: GraphQLType,
77+
locationDefaultValue: Map?
78+
) throws -> Bool {
79+
if let locationType = locationType as? GraphQLNonNull, !(varType is GraphQLNonNull) {
80+
let hasNonNullVariableDefaultValue = varDefaultValue != nil && varDefaultValue?
81+
.kind != .nullValue
82+
let hasLocationDefaultValue = locationDefaultValue != .undefined
83+
if !hasNonNullVariableDefaultValue && !hasLocationDefaultValue {
84+
return false
85+
}
86+
let nullableLocationType = locationType.ofType
87+
return try isTypeSubTypeOf(schema, varType, nullableLocationType)
88+
}
89+
return try isTypeSubTypeOf(schema, varType, locationType)
90+
}

Sources/GraphQL/Validation/SpecifiedRules.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public let specifiedRules: [(ValidationContext) -> Visitor] = [
2929
ValuesOfCorrectTypeRule,
3030
// ProvidedRequiredArgumentsRule,
3131
ProvidedNonNullArgumentsRule,
32-
// VariablesInAllowedPositionRule,
32+
VariablesInAllowedPositionRule,
3333
// OverlappingFieldsCanBeMergedRule,
3434
UniqueInputFieldNamesRule,
3535
]

Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ class StarWarsQueryTests: XCTestCase {
186186

187187
let query =
188188
"""
189-
query FetchHeroByEpisodeQuery($episode: String) {
189+
query FetchHeroByEpisodeQuery($episode: Episode) {
190190
hero(episode: $episode) {
191191
name
192192
}

0 commit comments

Comments
 (0)