Skip to content

Commit 81e7596

Browse files
committed
Merge pull request #238 from graphql/parallel-validator
[Validation] Parallelize validation rules.
2 parents 439a3e2 + 9577041 commit 81e7596

File tree

2 files changed

+99
-67
lines changed

2 files changed

+99
-67
lines changed

src/language/visitor.js

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export function visit(root, visitor, keyMap) {
208208
if (!isNode(node)) {
209209
throw new Error('Invalid AST Node: ' + JSON.stringify(node));
210210
}
211-
var visitFn = getVisitFn(visitor, isLeaving, node.kind);
211+
var visitFn = getVisitFn(visitor, node.kind, isLeaving);
212212
if (visitFn) {
213213
result = visitFn.call(visitor, node, key, parent, path, ancestors);
214214

@@ -263,7 +263,83 @@ function isNode(maybeNode) {
263263
return maybeNode && typeof maybeNode.kind === 'string';
264264
}
265265

266-
export function getVisitFn(visitor, isLeaving, kind) {
266+
267+
/**
268+
* Creates a new visitor instance which delegates to many visitors to run in
269+
* parallel. Each visitor will be visited for each node before moving on.
270+
*
271+
* Visitors must not directly modify the AST nodes and only returning false to
272+
* skip sub-branches is supported.
273+
*/
274+
export function visitInParallel(visitors) {
275+
const skipping = new Array(visitors.length);
276+
277+
return {
278+
enter(node) {
279+
for (let i = 0; i < visitors.length; i++) {
280+
if (!skipping[i]) {
281+
const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ false);
282+
if (fn) {
283+
const result = fn.apply(visitors[i], arguments);
284+
if (result === false) {
285+
skipping[i] = node;
286+
}
287+
}
288+
}
289+
}
290+
},
291+
leave(node) {
292+
for (let i = 0; i < visitors.length; i++) {
293+
if (!skipping[i]) {
294+
const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ true);
295+
if (fn) {
296+
fn.apply(visitors[i], arguments);
297+
}
298+
} else {
299+
skipping[i] = null;
300+
}
301+
}
302+
}
303+
};
304+
}
305+
306+
307+
/**
308+
* Creates a new visitor instance which maintains a provided TypeInfo instance
309+
* along with visiting visitor.
310+
*
311+
* Visitors must not directly modify the AST nodes and only returning false to
312+
* skip sub-branches is supported.
313+
*/
314+
export function visitWithTypeInfo(typeInfo, visitor) {
315+
return {
316+
enter(node) {
317+
typeInfo.enter(node);
318+
const fn = getVisitFn(visitor, node.kind, /* isLeaving */ false);
319+
if (fn) {
320+
const result = fn.apply(visitor, arguments);
321+
if (result === false) {
322+
typeInfo.leave(node);
323+
return false;
324+
}
325+
}
326+
},
327+
leave(node) {
328+
const fn = getVisitFn(visitor, node.kind, /* isLeaving */ true);
329+
if (fn) {
330+
fn.apply(visitor, arguments);
331+
}
332+
typeInfo.leave(node);
333+
}
334+
};
335+
}
336+
337+
338+
/**
339+
* Given a visitor instance, if it is leaving or not, and a node kind, return
340+
* the function the visitor runtime should call.
341+
*/
342+
function getVisitFn(visitor, kind, isLeaving) {
267343
var kindVisitor = visitor[kind];
268344
if (kindVisitor) {
269345
if (!isLeaving && typeof kindVisitor === 'function') {
@@ -275,18 +351,18 @@ export function getVisitFn(visitor, isLeaving, kind) {
275351
// { Kind: { enter() {}, leave() {} } }
276352
return kindSpecificVisitor;
277353
}
278-
return;
279-
}
280-
var specificVisitor = isLeaving ? visitor.leave : visitor.enter;
281-
if (specificVisitor) {
282-
if (typeof specificVisitor === 'function') {
283-
// { enter() {}, leave() {} }
284-
return specificVisitor;
285-
}
286-
var specificKindVisitor = specificVisitor[kind];
287-
if (typeof specificKindVisitor === 'function') {
288-
// { enter: { Kind() {} }, leave: { Kind() {} } }
289-
return specificKindVisitor;
354+
} else {
355+
var specificVisitor = isLeaving ? visitor.leave : visitor.enter;
356+
if (specificVisitor) {
357+
if (typeof specificVisitor === 'function') {
358+
// { enter() {}, leave() {} }
359+
return specificVisitor;
360+
}
361+
var specificKindVisitor = specificVisitor[kind];
362+
if (typeof specificKindVisitor === 'function') {
363+
// { enter: { Kind() {} }, leave: { Kind() {} } }
364+
return specificKindVisitor;
365+
}
290366
}
291367
}
292368
}

src/validation/validate.js

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import invariant from '../jsutils/invariant';
1212
import { GraphQLError } from '../error';
13-
import { visit, getVisitFn } from '../language/visitor';
13+
import { visit, visitInParallel, visitWithTypeInfo } from '../language/visitor';
1414
import * as Kind from '../language/kinds';
1515
import type {
1616
Document,
@@ -74,47 +74,10 @@ export function visitUsingRules(
7474
documentAST: Document,
7575
rules: Array<any>
7676
): Array<GraphQLError> {
77-
var context = new ValidationContext(schema, documentAST, typeInfo);
78-
79-
function visitInstance(ast, instance) {
80-
visit(ast, {
81-
enter(node) {
82-
// Collect type information about the current position in the AST.
83-
typeInfo.enter(node);
84-
85-
// Get the visitor function from the validation instance, and if it
86-
// exists, call it with the visitor arguments.
87-
var enter = getVisitFn(instance, false, node.kind);
88-
var result = enter ? enter.apply(instance, arguments) : undefined;
89-
90-
// If the result is "false", we're not visiting any descendent nodes,
91-
// but need to update typeInfo.
92-
if (result === false) {
93-
typeInfo.leave(node);
94-
}
95-
96-
return result;
97-
},
98-
leave(node) {
99-
// Get the visitor function from the validation instance, and if it
100-
// exists, call it with the visitor arguments.
101-
var leave = getVisitFn(instance, true, node.kind);
102-
var result = leave ? leave.apply(instance, arguments) : undefined;
103-
104-
// Update typeInfo.
105-
typeInfo.leave(node);
106-
107-
return result;
108-
}
109-
});
110-
}
111-
77+
const context = new ValidationContext(schema, documentAST, typeInfo);
78+
const visitors = rules.map(rule => rule(context));
11279
// Visit the whole document with each instance of all provided rules.
113-
var instances = rules.map(rule => rule(context));
114-
instances.forEach(instance => {
115-
visitInstance(documentAST, instance);
116-
});
117-
80+
visit(documentAST, visitWithTypeInfo(typeInfo, visitInParallel(visitors)));
11881
return context.getErrors();
11982
}
12083

@@ -233,19 +196,12 @@ export class ValidationContext {
233196
if (!usages) {
234197
usages = [];
235198
const typeInfo = new TypeInfo(this._schema);
236-
visit(node, {
237-
enter(subnode) {
238-
typeInfo.enter(subnode);
239-
if (subnode.kind === Kind.VARIABLE_DEFINITION) {
240-
return false;
241-
} else if (subnode.kind === Kind.VARIABLE) {
242-
usages.push({ node: subnode, type: typeInfo.getInputType() });
243-
}
244-
},
245-
leave(subnode) {
246-
typeInfo.leave(subnode);
199+
visit(node, visitWithTypeInfo(typeInfo, {
200+
VariableDefinition: () => false,
201+
Variable(variable) {
202+
usages.push({ node: variable, type: typeInfo.getInputType() });
247203
}
248-
});
204+
}));
249205
this._variableUsages.set(node, usages);
250206
}
251207
return usages;

0 commit comments

Comments
 (0)