UNPKG

11.3 kBJavaScriptView Raw
1import inspect from "../jsutils/inspect.mjs";
2import { isNode } from "./ast.mjs";
3/**
4 * A visitor is provided to visit, it contains the collection of
5 * relevant functions to be called during the visitor's traversal.
6 */
7
8export var QueryDocumentKeys = {
9 Name: [],
10 Document: ['definitions'],
11 OperationDefinition: ['name', 'variableDefinitions', 'directives', 'selectionSet'],
12 VariableDefinition: ['variable', 'type', 'defaultValue', 'directives'],
13 Variable: ['name'],
14 SelectionSet: ['selections'],
15 Field: ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
16 Argument: ['name', 'value'],
17 FragmentSpread: ['name', 'directives'],
18 InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
19 FragmentDefinition: ['name', // Note: fragment variable definitions are experimental and may be changed
20 // or removed in the future.
21 'variableDefinitions', 'typeCondition', 'directives', 'selectionSet'],
22 IntValue: [],
23 FloatValue: [],
24 StringValue: [],
25 BooleanValue: [],
26 NullValue: [],
27 EnumValue: [],
28 ListValue: ['values'],
29 ObjectValue: ['fields'],
30 ObjectField: ['name', 'value'],
31 Directive: ['name', 'arguments'],
32 NamedType: ['name'],
33 ListType: ['type'],
34 NonNullType: ['type'],
35 SchemaDefinition: ['description', 'directives', 'operationTypes'],
36 OperationTypeDefinition: ['type'],
37 ScalarTypeDefinition: ['description', 'name', 'directives'],
38 ObjectTypeDefinition: ['description', 'name', 'interfaces', 'directives', 'fields'],
39 FieldDefinition: ['description', 'name', 'arguments', 'type', 'directives'],
40 InputValueDefinition: ['description', 'name', 'type', 'defaultValue', 'directives'],
41 InterfaceTypeDefinition: ['description', 'name', 'interfaces', 'directives', 'fields'],
42 UnionTypeDefinition: ['description', 'name', 'directives', 'types'],
43 EnumTypeDefinition: ['description', 'name', 'directives', 'values'],
44 EnumValueDefinition: ['description', 'name', 'directives'],
45 InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],
46 DirectiveDefinition: ['description', 'name', 'arguments', 'locations'],
47 SchemaExtension: ['directives', 'operationTypes'],
48 ScalarTypeExtension: ['name', 'directives'],
49 ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
50 InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
51 UnionTypeExtension: ['name', 'directives', 'types'],
52 EnumTypeExtension: ['name', 'directives', 'values'],
53 InputObjectTypeExtension: ['name', 'directives', 'fields']
54};
55export var BREAK = Object.freeze({});
56/**
57 * visit() will walk through an AST using a depth-first traversal, calling
58 * the visitor's enter function at each node in the traversal, and calling the
59 * leave function after visiting that node and all of its child nodes.
60 *
61 * By returning different values from the enter and leave functions, the
62 * behavior of the visitor can be altered, including skipping over a sub-tree of
63 * the AST (by returning false), editing the AST by returning a value or null
64 * to remove the value, or to stop the whole traversal by returning BREAK.
65 *
66 * When using visit() to edit an AST, the original AST will not be modified, and
67 * a new version of the AST with the changes applied will be returned from the
68 * visit function.
69 *
70 * const editedAST = visit(ast, {
71 * enter(node, key, parent, path, ancestors) {
72 * // @return
73 * // undefined: no action
74 * // false: skip visiting this node
75 * // visitor.BREAK: stop visiting altogether
76 * // null: delete this node
77 * // any value: replace this node with the returned value
78 * },
79 * leave(node, key, parent, path, ancestors) {
80 * // @return
81 * // undefined: no action
82 * // false: no action
83 * // visitor.BREAK: stop visiting altogether
84 * // null: delete this node
85 * // any value: replace this node with the returned value
86 * }
87 * });
88 *
89 * Alternatively to providing enter() and leave() functions, a visitor can
90 * instead provide functions named the same as the kinds of AST nodes, or
91 * enter/leave visitors at a named key, leading to four permutations of the
92 * visitor API:
93 *
94 * 1) Named visitors triggered when entering a node of a specific kind.
95 *
96 * visit(ast, {
97 * Kind(node) {
98 * // enter the "Kind" node
99 * }
100 * })
101 *
102 * 2) Named visitors that trigger upon entering and leaving a node of
103 * a specific kind.
104 *
105 * visit(ast, {
106 * Kind: {
107 * enter(node) {
108 * // enter the "Kind" node
109 * }
110 * leave(node) {
111 * // leave the "Kind" node
112 * }
113 * }
114 * })
115 *
116 * 3) Generic visitors that trigger upon entering and leaving any node.
117 *
118 * visit(ast, {
119 * enter(node) {
120 * // enter any node
121 * },
122 * leave(node) {
123 * // leave any node
124 * }
125 * })
126 *
127 * 4) Parallel visitors for entering and leaving nodes of a specific kind.
128 *
129 * visit(ast, {
130 * enter: {
131 * Kind(node) {
132 * // enter the "Kind" node
133 * }
134 * },
135 * leave: {
136 * Kind(node) {
137 * // leave the "Kind" node
138 * }
139 * }
140 * })
141 */
142
143export function visit(root, visitor) {
144 var visitorKeys = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : QueryDocumentKeys;
145
146 /* eslint-disable no-undef-init */
147 var stack = undefined;
148 var inArray = Array.isArray(root);
149 var keys = [root];
150 var index = -1;
151 var edits = [];
152 var node = undefined;
153 var key = undefined;
154 var parent = undefined;
155 var path = [];
156 var ancestors = [];
157 var newRoot = root;
158 /* eslint-enable no-undef-init */
159
160 do {
161 index++;
162 var isLeaving = index === keys.length;
163 var isEdited = isLeaving && edits.length !== 0;
164
165 if (isLeaving) {
166 key = ancestors.length === 0 ? undefined : path[path.length - 1];
167 node = parent;
168 parent = ancestors.pop();
169
170 if (isEdited) {
171 if (inArray) {
172 node = node.slice();
173 } else {
174 var clone = {};
175
176 for (var _i2 = 0, _Object$keys2 = Object.keys(node); _i2 < _Object$keys2.length; _i2++) {
177 var k = _Object$keys2[_i2];
178 clone[k] = node[k];
179 }
180
181 node = clone;
182 }
183
184 var editOffset = 0;
185
186 for (var ii = 0; ii < edits.length; ii++) {
187 var editKey = edits[ii][0];
188 var editValue = edits[ii][1];
189
190 if (inArray) {
191 editKey -= editOffset;
192 }
193
194 if (inArray && editValue === null) {
195 node.splice(editKey, 1);
196 editOffset++;
197 } else {
198 node[editKey] = editValue;
199 }
200 }
201 }
202
203 index = stack.index;
204 keys = stack.keys;
205 edits = stack.edits;
206 inArray = stack.inArray;
207 stack = stack.prev;
208 } else {
209 key = parent ? inArray ? index : keys[index] : undefined;
210 node = parent ? parent[key] : newRoot;
211
212 if (node === null || node === undefined) {
213 continue;
214 }
215
216 if (parent) {
217 path.push(key);
218 }
219 }
220
221 var result = void 0;
222
223 if (!Array.isArray(node)) {
224 if (!isNode(node)) {
225 throw new Error("Invalid AST Node: ".concat(inspect(node), "."));
226 }
227
228 var visitFn = getVisitFn(visitor, node.kind, isLeaving);
229
230 if (visitFn) {
231 result = visitFn.call(visitor, node, key, parent, path, ancestors);
232
233 if (result === BREAK) {
234 break;
235 }
236
237 if (result === false) {
238 if (!isLeaving) {
239 path.pop();
240 continue;
241 }
242 } else if (result !== undefined) {
243 edits.push([key, result]);
244
245 if (!isLeaving) {
246 if (isNode(result)) {
247 node = result;
248 } else {
249 path.pop();
250 continue;
251 }
252 }
253 }
254 }
255 }
256
257 if (result === undefined && isEdited) {
258 edits.push([key, node]);
259 }
260
261 if (isLeaving) {
262 path.pop();
263 } else {
264 var _visitorKeys$node$kin;
265
266 stack = {
267 inArray: inArray,
268 index: index,
269 keys: keys,
270 edits: edits,
271 prev: stack
272 };
273 inArray = Array.isArray(node);
274 keys = inArray ? node : (_visitorKeys$node$kin = visitorKeys[node.kind]) !== null && _visitorKeys$node$kin !== void 0 ? _visitorKeys$node$kin : [];
275 index = -1;
276 edits = [];
277
278 if (parent) {
279 ancestors.push(parent);
280 }
281
282 parent = node;
283 }
284 } while (stack !== undefined);
285
286 if (edits.length !== 0) {
287 newRoot = edits[edits.length - 1][1];
288 }
289
290 return newRoot;
291}
292/**
293 * Creates a new visitor instance which delegates to many visitors to run in
294 * parallel. Each visitor will be visited for each node before moving on.
295 *
296 * If a prior visitor edits a node, no following visitors will see that node.
297 */
298
299export function visitInParallel(visitors) {
300 var skipping = new Array(visitors.length);
301 return {
302 enter: function enter(node) {
303 for (var i = 0; i < visitors.length; i++) {
304 if (skipping[i] == null) {
305 var fn = getVisitFn(visitors[i], node.kind,
306 /* isLeaving */
307 false);
308
309 if (fn) {
310 var result = fn.apply(visitors[i], arguments);
311
312 if (result === false) {
313 skipping[i] = node;
314 } else if (result === BREAK) {
315 skipping[i] = BREAK;
316 } else if (result !== undefined) {
317 return result;
318 }
319 }
320 }
321 }
322 },
323 leave: function leave(node) {
324 for (var i = 0; i < visitors.length; i++) {
325 if (skipping[i] == null) {
326 var fn = getVisitFn(visitors[i], node.kind,
327 /* isLeaving */
328 true);
329
330 if (fn) {
331 var result = fn.apply(visitors[i], arguments);
332
333 if (result === BREAK) {
334 skipping[i] = BREAK;
335 } else if (result !== undefined && result !== false) {
336 return result;
337 }
338 }
339 } else if (skipping[i] === node) {
340 skipping[i] = null;
341 }
342 }
343 }
344 };
345}
346/**
347 * Given a visitor instance, if it is leaving or not, and a node kind, return
348 * the function the visitor runtime should call.
349 */
350
351export function getVisitFn(visitor, kind, isLeaving) {
352 var kindVisitor = visitor[kind];
353
354 if (kindVisitor) {
355 if (!isLeaving && typeof kindVisitor === 'function') {
356 // { Kind() {} }
357 return kindVisitor;
358 }
359
360 var kindSpecificVisitor = isLeaving ? kindVisitor.leave : kindVisitor.enter;
361
362 if (typeof kindSpecificVisitor === 'function') {
363 // { Kind: { enter() {}, leave() {} } }
364 return kindSpecificVisitor;
365 }
366 } else {
367 var specificVisitor = isLeaving ? visitor.leave : visitor.enter;
368
369 if (specificVisitor) {
370 if (typeof specificVisitor === 'function') {
371 // { enter() {}, leave() {} }
372 return specificVisitor;
373 }
374
375 var specificKindVisitor = specificVisitor[kind];
376
377 if (typeof specificKindVisitor === 'function') {
378 // { enter: { Kind() {} }, leave: { Kind() {} } }
379 return specificKindVisitor;
380 }
381 }
382 }
383}