Skip to content

Commit ec2f92c

Browse files
committed
Invert VariablesInAllowedPositionRule to visit every variable, rather than every operation
1 parent 454a654 commit ec2f92c

File tree

2 files changed

+65
-44
lines changed

2 files changed

+65
-44
lines changed

src/validation/ValidationContext.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import type { ObjMap } from '../jsutils/ObjMap.js';
44
import type { GraphQLError } from '../error/GraphQLError.js';
55

66
import type {
7+
ConstValueNode,
78
DocumentNode,
89
FragmentDefinitionNode,
910
FragmentSpreadNode,
1011
OperationDefinitionNode,
1112
SelectionSetNode,
13+
VariableDefinitionNode,
1214
VariableNode,
1315
} from '../language/ast.js';
1416
import { Kind } from '../language/kinds.js';
@@ -34,6 +36,10 @@ interface VariableUsage {
3436
readonly type: Maybe<GraphQLInputType>;
3537
readonly defaultValue: Maybe<unknown>;
3638
}
39+
interface DefinedVariable {
40+
readonly variableDefinition: VariableDefinitionNode;
41+
readonly operation: OperationDefinitionNode;
42+
}
3743

3844
/**
3945
* An instance of this class is passed as the "this" context to all validators,
@@ -50,11 +56,16 @@ export class ASTValidationContext {
5056
Array<FragmentDefinitionNode>
5157
>;
5258

59+
private _variableDefinedOperations:
60+
| ObjMap<ReadonlyArray<DefinedVariable>>
61+
| undefined;
62+
5363
constructor(ast: DocumentNode, onError: (error: GraphQLError) => void) {
5464
this._ast = ast;
5565
this._fragments = undefined;
5666
this._fragmentSpreads = new Map();
5767
this._recursivelyReferencedFragments = new Map();
68+
this._variableDefinedOperations = undefined;
5869
this._onError = onError;
5970
}
6071

@@ -134,6 +145,28 @@ export class ASTValidationContext {
134145
}
135146
return fragments;
136147
}
148+
149+
getVariableDefinitions(node: VariableNode): ReadonlyArray<DefinedVariable> {
150+
if (!this._variableDefinedOperations) {
151+
const variableDefinedOperations: ObjMap<Array<DefinedVariable>> = {};
152+
visit(this._ast, {
153+
OperationDefinition(operation) {
154+
for (const varDef of operation.variableDefinitions ?? []) {
155+
if (!variableDefinedOperations[varDef.variable.name.value]) {
156+
variableDefinedOperations[varDef.variable.name.value] = [];
157+
}
158+
variableDefinedOperations[varDef.variable.name.value].push({
159+
variableDefinition: varDef,
160+
operation,
161+
});
162+
}
163+
},
164+
});
165+
166+
this._variableDefinedOperations = variableDefinedOperations;
167+
}
168+
return this._variableDefinedOperations[node.name.value] ?? [];
169+
}
137170
}
138171

139172
export type ASTValidationRule = (context: ASTValidationContext) => ASTVisitor;
@@ -245,6 +278,10 @@ export class ValidationContext extends ASTValidationContext {
245278
return this._typeInfo.getInputType();
246279
}
247280

281+
getDefaultValue(): Maybe<ConstValueNode> {
282+
return this._typeInfo.getDefaultValue() as Maybe<ConstValueNode>;
283+
}
284+
248285
getParentInputType(): Maybe<GraphQLInputType> {
249286
return this._typeInfo.getParentInputType();
250287
}

src/validation/rules/VariablesInAllowedPositionRule.ts

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,52 +26,36 @@ import type { ValidationContext } from '../ValidationContext.js';
2626
export function VariablesInAllowedPositionRule(
2727
context: ValidationContext,
2828
): ASTVisitor {
29-
let varDefMap = Object.create(null);
30-
3129
return {
32-
OperationDefinition: {
33-
enter() {
34-
varDefMap = Object.create(null);
35-
},
36-
leave(operation) {
37-
const usages = context.getRecursiveVariableUsages(operation);
38-
39-
for (const { node, type, defaultValue } of usages) {
40-
const varName = node.name.value;
41-
const varDef = varDefMap[varName];
42-
if (varDef && type) {
43-
// A var type is allowed if it is the same or more strict (e.g. is
44-
// a subtype of) than the expected type. It can be more strict if
45-
// the variable type is non-null when the expected type is nullable.
46-
// If both are list types, the variable item type can be more strict
47-
// than the expected item type (contravariant).
48-
const schema = context.getSchema();
49-
const varType = typeFromAST(schema, varDef.type);
50-
if (
51-
varType &&
52-
!allowedVariableUsage(
53-
schema,
54-
varType,
55-
varDef.defaultValue,
56-
type,
57-
defaultValue,
58-
)
59-
) {
60-
const varTypeStr = inspect(varType);
61-
const typeStr = inspect(type);
62-
context.reportError(
63-
new GraphQLError(
64-
`Variable "$${varName}" of type "${varTypeStr}" used in position expecting type "${typeStr}".`,
65-
{ nodes: [varDef, node] },
66-
),
67-
);
68-
}
69-
}
30+
Variable(node) {
31+
const varDefs = context.getVariableDefinitions(node);
32+
const type = context.getInputType();
33+
const defaultValue = context.getDefaultValue();
34+
const varName = node.name.value;
35+
for (const { variableDefinition } of varDefs) {
36+
const schema = context.getSchema();
37+
const varType = typeFromAST(schema, variableDefinition.type);
38+
if (
39+
type &&
40+
varType &&
41+
!allowedVariableUsage(
42+
schema,
43+
varType,
44+
variableDefinition.defaultValue,
45+
type,
46+
defaultValue,
47+
)
48+
) {
49+
const varTypeStr = inspect(varType);
50+
const typeStr = inspect(type);
51+
context.reportError(
52+
new GraphQLError(
53+
`Variable "$${varName}" of type "${varTypeStr}" used in position expecting type "${typeStr}".`,
54+
{ nodes: [variableDefinition, node] },
55+
),
56+
);
7057
}
71-
},
72-
},
73-
VariableDefinition(node) {
74-
varDefMap[node.variable.name.value] = node;
58+
}
7559
},
7660
};
7761
}

0 commit comments

Comments
 (0)