Skip to content

Commit 2afbff7

Browse files
committed
[Validation] Memoize collecting variable usage.
During multiple validation passes we need to know about variable usage within a de-fragmented operation. Memoizing this ensures each pass is O(N) - each fragment is no longer visited per operation, but once total. In doing so, `visitSpreadFragments` is no longer used, which will be cleaned up in a later PR
1 parent 88acc01 commit 2afbff7

File tree

5 files changed

+140
-121
lines changed

5 files changed

+140
-121
lines changed

src/validation/__tests__/NoUndefinedVariables.js

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,12 @@ import { expectPassesRule, expectFailsRule } from './harness';
1212
import {
1313
NoUndefinedVariables,
1414
undefinedVarMessage,
15-
undefinedVarByOpMessage,
1615
} from '../rules/NoUndefinedVariables';
1716

1817

19-
function undefVar(varName, line, column) {
18+
function undefVar(varName, l1, c1, opName, l2, c2) {
2019
return {
21-
message: undefinedVarMessage(varName),
22-
locations: [ { line, column } ],
23-
};
24-
}
25-
26-
function undefVarByOp(varName, l1, c1, opName, l2, c2) {
27-
return {
28-
message: undefinedVarByOpMessage(varName, opName),
20+
message: undefinedVarMessage(varName, opName),
2921
locations: [ { line: l1, column: c1 }, { line: l2, column: c2 } ],
3022
};
3123
}
@@ -139,7 +131,7 @@ describe('Validate: No undefined variables', () => {
139131
field(a: $a, b: $b, c: $c, d: $d)
140132
}
141133
`, [
142-
undefVar('d', 3, 39)
134+
undefVar('d', 3, 39, 'Foo', 2, 7)
143135
]);
144136
});
145137

@@ -149,7 +141,7 @@ describe('Validate: No undefined variables', () => {
149141
field(a: $a)
150142
}
151143
`, [
152-
undefVar('a', 3, 18)
144+
undefVar('a', 3, 18, '', 2, 7)
153145
]);
154146
});
155147

@@ -159,8 +151,8 @@ describe('Validate: No undefined variables', () => {
159151
field(a: $a, b: $b, c: $c)
160152
}
161153
`, [
162-
undefVar('a', 3, 18),
163-
undefVar('c', 3, 32)
154+
undefVar('a', 3, 18, 'Foo', 2, 7),
155+
undefVar('c', 3, 32, 'Foo', 2, 7)
164156
]);
165157
});
166158

@@ -173,7 +165,7 @@ describe('Validate: No undefined variables', () => {
173165
field(a: $a)
174166
}
175167
`, [
176-
undefVar('a', 6, 18)
168+
undefVar('a', 6, 18, '', 2, 7)
177169
]);
178170
});
179171

@@ -196,7 +188,7 @@ describe('Validate: No undefined variables', () => {
196188
field(c: $c)
197189
}
198190
`, [
199-
undefVarByOp('c', 16, 18, 'Foo', 2, 7)
191+
undefVar('c', 16, 18, 'Foo', 2, 7)
200192
]);
201193
});
202194

@@ -219,8 +211,8 @@ describe('Validate: No undefined variables', () => {
219211
field(c: $c)
220212
}
221213
`, [
222-
undefVarByOp('a', 6, 18, 'Foo', 2, 7),
223-
undefVarByOp('c', 16, 18, 'Foo', 2, 7)
214+
undefVar('a', 6, 18, 'Foo', 2, 7),
215+
undefVar('c', 16, 18, 'Foo', 2, 7)
224216
]);
225217
});
226218

@@ -236,8 +228,8 @@ describe('Validate: No undefined variables', () => {
236228
field(a: $a, b: $b)
237229
}
238230
`, [
239-
undefVarByOp('b', 9, 25, 'Foo', 2, 7),
240-
undefVarByOp('b', 9, 25, 'Bar', 5, 7)
231+
undefVar('b', 9, 25, 'Foo', 2, 7),
232+
undefVar('b', 9, 25, 'Bar', 5, 7)
241233
]);
242234
});
243235

@@ -253,8 +245,8 @@ describe('Validate: No undefined variables', () => {
253245
field(a: $a, b: $b)
254246
}
255247
`, [
256-
undefVarByOp('a', 9, 18, 'Foo', 2, 7),
257-
undefVarByOp('b', 9, 25, 'Bar', 5, 7)
248+
undefVar('a', 9, 18, 'Foo', 2, 7),
249+
undefVar('b', 9, 25, 'Bar', 5, 7)
258250
]);
259251
});
260252

@@ -273,8 +265,8 @@ describe('Validate: No undefined variables', () => {
273265
field(b: $b)
274266
}
275267
`, [
276-
undefVarByOp('a', 9, 18, 'Foo', 2, 7),
277-
undefVarByOp('b', 12, 18, 'Bar', 5, 7)
268+
undefVar('a', 9, 18, 'Foo', 2, 7),
269+
undefVar('b', 12, 18, 'Bar', 5, 7)
278270
]);
279271
});
280272

@@ -295,12 +287,12 @@ describe('Validate: No undefined variables', () => {
295287
field2(c: $c)
296288
}
297289
`, [
298-
undefVarByOp('a', 9, 19, 'Foo', 2, 7),
299-
undefVarByOp('c', 14, 19, 'Foo', 2, 7),
300-
undefVarByOp('a', 11, 19, 'Foo', 2, 7),
301-
undefVarByOp('b', 9, 26, 'Bar', 5, 7),
302-
undefVarByOp('c', 14, 19, 'Bar', 5, 7),
303-
undefVarByOp('b', 11, 26, 'Bar', 5, 7),
290+
undefVar('a', 9, 19, 'Foo', 2, 7),
291+
undefVar('a', 11, 19, 'Foo', 2, 7),
292+
undefVar('c', 14, 19, 'Foo', 2, 7),
293+
undefVar('b', 9, 26, 'Bar', 5, 7),
294+
undefVar('b', 11, 26, 'Bar', 5, 7),
295+
undefVar('c', 14, 19, 'Bar', 5, 7),
304296
]);
305297
});
306298

src/validation/rules/NoUndefinedVariables.js

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11+
import type { ValidationContext } from '../index';
1112
import { GraphQLError } from '../../error';
12-
import { FRAGMENT_DEFINITION } from '../../language/kinds';
1313

1414

15-
export function undefinedVarMessage(varName: any): string {
16-
return `Variable "$${varName}" is not defined.`;
17-
}
18-
19-
export function undefinedVarByOpMessage(varName: any, opName: any): string {
20-
return `Variable "$${varName}" is not defined by operation "${opName}".`;
15+
export function undefinedVarMessage(varName: string, opName: ?string): string {
16+
return opName ?
17+
`Variable "$${varName}" is not defined by operation "${opName}".` :
18+
`Variable "$${varName}" is not defined.`;
2119
}
2220

2321
/**
@@ -26,47 +24,40 @@ export function undefinedVarByOpMessage(varName: any, opName: any): string {
2624
* A GraphQL operation is only valid if all variables encountered, both directly
2725
* and via fragment spreads, are defined by that operation.
2826
*/
29-
export function NoUndefinedVariables(): any {
30-
var operation;
31-
var visitedFragmentNames = {};
32-
var definedVariableNames = {};
27+
export function NoUndefinedVariables(context: ValidationContext): any {
28+
let variableNameDefined = Object.create(null);
3329

3430
return {
35-
// Visit FragmentDefinition after visiting FragmentSpread
36-
visitSpreadFragments: true,
31+
OperationDefinition: {
32+
enter() {
33+
variableNameDefined = Object.create(null);
34+
},
35+
leave(operation) {
36+
const usages = context.getRecursiveVariableUsages(operation);
37+
const errors = [];
3738

38-
OperationDefinition(node) {
39-
operation = node;
40-
visitedFragmentNames = {};
41-
definedVariableNames = {};
42-
},
43-
VariableDefinition(def) {
44-
definedVariableNames[def.variable.name.value] = true;
45-
},
46-
Variable(variable, key, parent, path, ancestors) {
47-
var varName = variable.name.value;
48-
if (definedVariableNames[varName] !== true) {
49-
var withinFragment = ancestors.some(
50-
node => node.kind === FRAGMENT_DEFINITION
51-
);
52-
if (withinFragment && operation && operation.name) {
53-
return new GraphQLError(
54-
undefinedVarByOpMessage(varName, operation.name.value),
55-
[ variable, operation ]
56-
);
39+
usages.forEach(({ node }) => {
40+
var varName = node.name.value;
41+
if (variableNameDefined[varName] !== true) {
42+
errors.push(
43+
new GraphQLError(
44+
undefinedVarMessage(
45+
varName,
46+
operation.name && operation.name.value
47+
),
48+
[ node, operation ]
49+
)
50+
);
51+
}
52+
});
53+
54+
if (errors.length > 0) {
55+
return errors;
5756
}
58-
return new GraphQLError(
59-
undefinedVarMessage(varName),
60-
[ variable ]
61-
);
6257
}
6358
},
64-
FragmentSpread(spreadAST) {
65-
// Only visit fragments of a particular name once per operation
66-
if (visitedFragmentNames[spreadAST.name.value] === true) {
67-
return false;
68-
}
69-
visitedFragmentNames[spreadAST.name.value] = true;
59+
VariableDefinition(varDefAST) {
60+
variableNameDefined[varDefAST.variable.name.value] = true;
7061
}
7162
};
7263
}

src/validation/rules/NoUnusedVariables.js

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11+
import type { ValidationContext } from '../index';
1112
import { GraphQLError } from '../../error';
1213

1314

@@ -21,22 +22,23 @@ export function unusedVariableMessage(varName: any): string {
2122
* A GraphQL operation is only valid if all variables defined by an operation
2223
* are used, either directly or within a spread fragment.
2324
*/
24-
export function NoUnusedVariables(): any {
25-
var visitedFragmentNames = {};
26-
var variableDefs = [];
27-
var variableNameUsed = {};
25+
export function NoUnusedVariables(context: ValidationContext): any {
26+
let variableDefs = [];
2827

2928
return {
30-
// Visit FragmentDefinition after visiting FragmentSpread
31-
visitSpreadFragments: true,
32-
3329
OperationDefinition: {
3430
enter() {
35-
visitedFragmentNames = {};
3631
variableDefs = [];
37-
variableNameUsed = {};
3832
},
39-
leave() {
33+
leave(operation) {
34+
35+
const variableNameUsed = Object.create(null);
36+
const usages = context.getRecursiveVariableUsages(operation);
37+
38+
usages.forEach(({ node }) => {
39+
variableNameUsed[node.name.value] = true;
40+
});
41+
4042
var errors = variableDefs
4143
.filter(def => variableNameUsed[def.variable.name.value] !== true)
4244
.map(def => new GraphQLError(
@@ -50,18 +52,6 @@ export function NoUnusedVariables(): any {
5052
},
5153
VariableDefinition(def) {
5254
variableDefs.push(def);
53-
// Do not visit deeper, or else the defined variable name will be visited.
54-
return false;
55-
},
56-
Variable(variable) {
57-
variableNameUsed[variable.name.value] = true;
58-
},
59-
FragmentSpread(spreadAST) {
60-
// Only visit fragments of a particular name once per operation
61-
if (visitedFragmentNames[spreadAST.name.value] === true) {
62-
return false;
63-
}
64-
visitedFragmentNames[spreadAST.name.value] = true;
6555
}
6656
};
6757
}

src/validation/rules/VariablesInAllowedPosition.js

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,39 +27,38 @@ export function badVarPosMessage(
2727
* Variables passed to field arguments conform to type
2828
*/
2929
export function VariablesInAllowedPosition(context: ValidationContext): any {
30-
var varDefMap = {};
31-
var visitedFragmentNames = {};
30+
let varDefMap = Object.create(null);
3231

3332
return {
34-
// Visit FragmentDefinition after visiting FragmentSpread
35-
visitSpreadFragments: true,
33+
OperationDefinition: {
34+
enter() {
35+
varDefMap = Object.create(null);
36+
},
37+
leave(operation) {
38+
const usages = context.getRecursiveVariableUsages(operation);
39+
const errors = [];
3640

37-
OperationDefinition() {
38-
varDefMap = {};
39-
visitedFragmentNames = {};
41+
usages.forEach(({ node, type }) => {
42+
const varName = node.name.value;
43+
const varDef = varDefMap[varName];
44+
const varType =
45+
varDef && typeFromAST(context.getSchema(), varDef.type);
46+
if (varType && type &&
47+
!varTypeAllowedForType(effectiveType(varType, varDef), type)) {
48+
errors.push(new GraphQLError(
49+
badVarPosMessage(varName, varType, type),
50+
[ node ]
51+
));
52+
}
53+
});
54+
55+
if (errors.length > 0) {
56+
return errors;
57+
}
58+
}
4059
},
4160
VariableDefinition(varDefAST) {
4261
varDefMap[varDefAST.variable.name.value] = varDefAST;
43-
},
44-
FragmentSpread(spreadAST) {
45-
// Only visit fragments of a particular name once per operation
46-
if (visitedFragmentNames[spreadAST.name.value]) {
47-
return false;
48-
}
49-
visitedFragmentNames[spreadAST.name.value] = true;
50-
},
51-
Variable(variableAST) {
52-
var varName = variableAST.name.value;
53-
var varDef = varDefMap[varName];
54-
var varType = varDef && typeFromAST(context.getSchema(), varDef.type);
55-
var inputType = context.getInputType();
56-
if (varType && inputType &&
57-
!varTypeAllowedForType(effectiveType(varType, varDef), inputType)) {
58-
return new GraphQLError(
59-
badVarPosMessage(varName, varType, inputType),
60-
[ variableAST ]
61-
);
62-
}
6362
}
6463
};
6564
}

0 commit comments

Comments
 (0)