source: node_modules/@swagger-api/apidom-ast/es/traversal/visitor.mjs

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 14.6 KB
RevLine 
[d24f17c]1import { ApiDOMStructuredError } from '@swagger-api/apidom-error';
2
3/**
4 * SPDX-FileCopyrightText: Copyright (c) GraphQL Contributors
5 *
6 * SPDX-License-Identifier: MIT
7 */
8
9// getVisitFn :: (Visitor, String, Boolean) -> Function
10export const getVisitFn = (visitor, type, isLeaving) => {
11 const typeVisitor = visitor[type];
12 if (typeVisitor != null) {
13 if (!isLeaving && typeof typeVisitor === 'function') {
14 // { Type() {} }
15 return typeVisitor;
16 }
17 const typeSpecificVisitor = isLeaving ? typeVisitor.leave : typeVisitor.enter;
18 if (typeof typeSpecificVisitor === 'function') {
19 // { Type: { enter() {}, leave() {} } }
20 return typeSpecificVisitor;
21 }
22 } else {
23 const specificVisitor = isLeaving ? visitor.leave : visitor.enter;
24 if (specificVisitor != null) {
25 if (typeof specificVisitor === 'function') {
26 // { enter() {}, leave() {} }
27 return specificVisitor;
28 }
29 const specificTypeVisitor = specificVisitor[type];
30 if (typeof specificTypeVisitor === 'function') {
31 // { enter: { Type() {} }, leave: { Type() {} } }
32 return specificTypeVisitor;
33 }
34 }
35 }
36 return null;
37};
38export const BREAK = {};
39
40// getNodeType :: Node -> String
41export const getNodeType = node => node === null || node === void 0 ? void 0 : node.type;
42
43// isNode :: Node -> Boolean
44export const isNode = node => typeof getNodeType(node) === 'string';
45
46// cloneNode :: a -> a
47export const cloneNode = node => Object.create(Object.getPrototypeOf(node), Object.getOwnPropertyDescriptors(node));
48
49/**
50 * Creates a new visitor instance which delegates to many visitors to run in
51 * parallel. Each visitor will be visited for each node before moving on.
52 *
53 * If a prior visitor edits a node, no following visitors will see that node.
54 * `exposeEdits=true` can be used to exoise the edited node from the previous visitors.
55 */
56export const mergeAll = (visitors, {
57 visitFnGetter = getVisitFn,
58 nodeTypeGetter = getNodeType,
59 breakSymbol = BREAK,
60 deleteNodeSymbol = null,
61 skipVisitingNodeSymbol = false,
62 exposeEdits = false
63} = {}) => {
64 const skipSymbol = Symbol('skip');
65 const skipping = new Array(visitors.length).fill(skipSymbol);
66 return {
67 enter(node, ...rest) {
68 let currentNode = node;
69 let hasChanged = false;
70 for (let i = 0; i < visitors.length; i += 1) {
71 if (skipping[i] === skipSymbol) {
72 const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(currentNode), false);
73 if (typeof visitFn === 'function') {
74 const result = visitFn.call(visitors[i], currentNode, ...rest);
75 if (result === skipVisitingNodeSymbol) {
76 skipping[i] = node;
77 } else if (result === breakSymbol) {
78 skipping[i] = breakSymbol;
79 } else if (result === deleteNodeSymbol) {
80 return result;
81 } else if (result !== undefined) {
82 if (exposeEdits) {
83 currentNode = result;
84 hasChanged = true;
85 } else {
86 return result;
87 }
88 }
89 }
90 }
91 }
92 return hasChanged ? currentNode : undefined;
93 },
94 leave(node, ...rest) {
95 for (let i = 0; i < visitors.length; i += 1) {
96 if (skipping[i] === skipSymbol) {
97 const visitFn = visitFnGetter(visitors[i], nodeTypeGetter(node), true);
98 if (typeof visitFn === 'function') {
99 const result = visitFn.call(visitors[i], node, ...rest);
100 if (result === breakSymbol) {
101 skipping[i] = breakSymbol;
102 } else if (result !== undefined && result !== skipVisitingNodeSymbol) {
103 return result;
104 }
105 }
106 } else if (skipping[i] === node) {
107 skipping[i] = skipSymbol;
108 }
109 }
110 return undefined;
111 }
112 };
113};
114
115/* eslint-disable no-continue, no-param-reassign */
116/**
117 * visit() will walk through an AST using a preorder depth first traversal, calling
118 * the visitor's enter function at each node in the traversal, and calling the
119 * leave function after visiting that node and all of its child nodes.
120 *
121 * By returning different values from the enter and leave functions, the
122 * behavior of the visitor can be altered, including skipping over a sub-tree of
123 * the AST (by returning false), editing the AST by returning a value or null
124 * to remove the value, or to stop the whole traversal by returning BREAK.
125 *
126 * When using visit() to edit an AST, the original AST will not be modified, and
127 * a new version of the AST with the changes applied will be returned from the
128 * visit function.
129 *
130 * const editedAST = visit(ast, {
131 * enter(node, key, parent, path, ancestors) {
132 * // @return
133 * // undefined: no action
134 * // false: skip visiting this node
135 * // BREAK: stop visiting altogether
136 * // null: delete this node
137 * // any value: replace this node with the returned value
138 * },
139 * leave(node, key, parent, path, ancestors) {
140 * // @return
141 * // undefined: no action
142 * // false: no action
143 * // BREAK: stop visiting altogether
144 * // null: delete this node
145 * // any value: replace this node with the returned value
146 * }
147 * });
148 *
149 * Alternatively to providing enter() and leave() functions, a visitor can
150 * instead provide functions named the same as the kinds of AST nodes, or
151 * enter/leave visitors at a named key, leading to four permutations of
152 * visitor API:
153 *
154 * 1) Named visitors triggered when entering a node a specific kind.
155 *
156 * visit(ast, {
157 * Kind(node) {
158 * // enter the "Kind" node
159 * }
160 * })
161 *
162 * 2) Named visitors that trigger upon entering and leaving a node of
163 * a specific kind.
164 *
165 * visit(ast, {
166 * Kind: {
167 * enter(node) {
168 * // enter the "Kind" node
169 * }
170 * leave(node) {
171 * // leave the "Kind" node
172 * }
173 * }
174 * })
175 *
176 * 3) Generic visitors that trigger upon entering and leaving any node.
177 *
178 * visit(ast, {
179 * enter(node) {
180 * // enter any node
181 * },
182 * leave(node) {
183 * // leave any node
184 * }
185 * })
186 *
187 * 4) Parallel visitors for entering and leaving nodes of a specific kind.
188 *
189 * visit(ast, {
190 * enter: {
191 * Kind(node) {
192 * // enter the "Kind" node
193 * }
194 * },
195 * leave: {
196 * Kind(node) {
197 * // leave the "Kind" node
198 * }
199 * }
200 * })
201 *
202 * @sig visit :: (Node, Visitor, Options)
203 * @sig Options = { keyMap: Object, state: Object }
204 */
205export const visit = (
206// @ts-ignore
207root,
208// @ts-ignore
209visitor, {
210 keyMap = null,
211 state = {},
212 breakSymbol = BREAK,
213 deleteNodeSymbol = null,
214 skipVisitingNodeSymbol = false,
215 visitFnGetter = getVisitFn,
216 nodeTypeGetter = getNodeType,
217 nodePredicate = isNode,
218 nodeCloneFn = cloneNode,
219 detectCycles = true
220} = {}) => {
221 const visitorKeys = keyMap || {};
222 let stack;
223 let inArray = Array.isArray(root);
224 let keys = [root];
225 let index = -1;
226 let parent;
227 let edits = [];
228 let node = root;
229 const path = [];
230 // @ts-ignore
231 const ancestors = [];
232 do {
233 index += 1;
234 const isLeaving = index === keys.length;
235 let key;
236 const isEdited = isLeaving && edits.length !== 0;
237 if (isLeaving) {
238 key = ancestors.length === 0 ? undefined : path.pop();
239 node = parent;
240 // @ts-ignore
241 parent = ancestors.pop();
242 if (isEdited) {
243 if (inArray) {
244 // @ts-ignore; creating clone
245 node = node.slice();
246 let editOffset = 0;
247 for (const [editKey, editValue] of edits) {
248 const arrayKey = editKey - editOffset;
249 if (editValue === deleteNodeSymbol) {
250 node.splice(arrayKey, 1);
251 editOffset += 1;
252 } else {
253 node[arrayKey] = editValue;
254 }
255 }
256 } else {
257 // creating clone
258 node = nodeCloneFn(node);
259 for (const [editKey, editValue] of edits) {
260 node[editKey] = editValue;
261 }
262 }
263 }
264 index = stack.index;
265 keys = stack.keys;
266 // @ts-ignore
267 edits = stack.edits;
268 // @ts-ignore
269 inArray = stack.inArray;
270 // @ts-ignore
271 stack = stack.prev;
272 } else if (parent !== deleteNodeSymbol && parent !== undefined) {
273 key = inArray ? index : keys[index];
274 node = parent[key];
275 if (node === deleteNodeSymbol || node === undefined) {
276 continue;
277 }
278 path.push(key);
279 }
280 let result;
281 if (!Array.isArray(node)) {
282 if (!nodePredicate(node)) {
283 throw new ApiDOMStructuredError(`Invalid AST Node: ${String(node)}`, {
284 node
285 });
286 }
287
288 // cycle detected; skipping over a sub-tree to avoid recursion
289 if (detectCycles && ancestors.includes(node)) {
290 path.pop();
291 continue;
292 }
293 // call appropriate visitor function if available
294 const visitFn = visitFnGetter(visitor, nodeTypeGetter(node), isLeaving);
295 if (visitFn) {
296 // assign state
297 for (const [stateKey, stateValue] of Object.entries(state)) {
298 visitor[stateKey] = stateValue;
299 }
300 // retrieve result
301 result = visitFn.call(visitor, node, key, parent, path, ancestors);
302 }
303 if (result === breakSymbol) {
304 break;
305 }
306 if (result === skipVisitingNodeSymbol) {
307 if (!isLeaving) {
308 path.pop();
309 continue;
310 }
311 } else if (result !== undefined) {
312 edits.push([key, result]);
313 if (!isLeaving) {
314 if (nodePredicate(result)) {
315 node = result;
316 } else {
317 path.pop();
318 continue;
319 }
320 }
321 }
322 }
323 if (result === undefined && isEdited) {
324 edits.push([key, node]);
325 }
326 if (!isLeaving) {
327 var _visitorKeys$nodeType;
328 stack = {
329 inArray,
330 index,
331 keys,
332 edits,
333 prev: stack
334 };
335 inArray = Array.isArray(node);
336 // @ts-ignore
337 keys = inArray ? node : (_visitorKeys$nodeType = visitorKeys[nodeTypeGetter(node)]) !== null && _visitorKeys$nodeType !== void 0 ? _visitorKeys$nodeType : [];
338 index = -1;
339 edits = [];
340 if (parent !== deleteNodeSymbol && parent !== undefined) {
341 ancestors.push(parent);
342 }
343 parent = node;
344 }
345 } while (stack !== undefined);
346 if (edits.length !== 0) {
347 return edits[edits.length - 1][1]; // @TODO(vladimir.gorej@gmail.com): can be replaced by Array.prototype.at in future
348 }
349 return root;
350};
351
352/**
353 * Asynchronous version of visit.
354 */
355// @ts-ignore
356visit[Symbol.for('nodejs.util.promisify.custom')] = async (
357// @ts-ignore
358root,
359// @ts-ignore
360visitor, {
361 keyMap = null,
362 state = {},
363 breakSymbol = BREAK,
364 deleteNodeSymbol = null,
365 skipVisitingNodeSymbol = false,
366 visitFnGetter = getVisitFn,
367 nodeTypeGetter = getNodeType,
368 nodePredicate = isNode,
369 nodeCloneFn = cloneNode,
370 detectCycles = true
371} = {}) => {
372 const visitorKeys = keyMap || {};
373 let stack;
374 let inArray = Array.isArray(root);
375 let keys = [root];
376 let index = -1;
377 let parent;
378 let edits = [];
379 let node = root;
380 const path = [];
381 // @ts-ignore
382 const ancestors = [];
383 do {
384 index += 1;
385 const isLeaving = index === keys.length;
386 let key;
387 const isEdited = isLeaving && edits.length !== 0;
388 if (isLeaving) {
389 key = ancestors.length === 0 ? undefined : path.pop();
390 node = parent;
391 // @ts-ignore
392 parent = ancestors.pop();
393 if (isEdited) {
394 if (inArray) {
395 // @ts-ignore; creating clone
396 node = node.slice();
397 let editOffset = 0;
398 for (const [editKey, editValue] of edits) {
399 const arrayKey = editKey - editOffset;
400 if (editValue === deleteNodeSymbol) {
401 node.splice(arrayKey, 1);
402 editOffset += 1;
403 } else {
404 node[arrayKey] = editValue;
405 }
406 }
407 } else {
408 // creating clone
409 node = nodeCloneFn(node);
410 for (const [editKey, editValue] of edits) {
411 node[editKey] = editValue;
412 }
413 }
414 }
415 index = stack.index;
416 keys = stack.keys;
417 // @ts-ignore
418 edits = stack.edits;
419 // @ts-ignore
420 inArray = stack.inArray;
421 // @ts-ignore
422 stack = stack.prev;
423 } else if (parent !== deleteNodeSymbol && parent !== undefined) {
424 key = inArray ? index : keys[index];
425 node = parent[key];
426 if (node === deleteNodeSymbol || node === undefined) {
427 continue;
428 }
429 path.push(key);
430 }
431 let result;
432 if (!Array.isArray(node)) {
433 if (!nodePredicate(node)) {
434 throw new ApiDOMStructuredError(`Invalid AST Node: ${String(node)}`, {
435 node
436 });
437 }
438
439 // cycle detected; skipping over a sub-tree to avoid recursion
440 if (detectCycles && ancestors.includes(node)) {
441 path.pop();
442 continue;
443 }
444 const visitFn = visitFnGetter(visitor, nodeTypeGetter(node), isLeaving);
445 if (visitFn) {
446 // assign state
447 for (const [stateKey, stateValue] of Object.entries(state)) {
448 visitor[stateKey] = stateValue;
449 }
450
451 // retrieve result
452 result = await visitFn.call(visitor, node, key, parent, path, ancestors); // eslint-disable-line no-await-in-loop
453 }
454 if (result === breakSymbol) {
455 break;
456 }
457 if (result === skipVisitingNodeSymbol) {
458 if (!isLeaving) {
459 path.pop();
460 continue;
461 }
462 } else if (result !== undefined) {
463 edits.push([key, result]);
464 if (!isLeaving) {
465 if (nodePredicate(result)) {
466 node = result;
467 } else {
468 path.pop();
469 continue;
470 }
471 }
472 }
473 }
474 if (result === undefined && isEdited) {
475 edits.push([key, node]);
476 }
477 if (!isLeaving) {
478 var _visitorKeys$nodeType2;
479 stack = {
480 inArray,
481 index,
482 keys,
483 edits,
484 prev: stack
485 };
486 inArray = Array.isArray(node);
487 // @ts-ignore
488 keys = inArray ? node : (_visitorKeys$nodeType2 = visitorKeys[nodeTypeGetter(node)]) !== null && _visitorKeys$nodeType2 !== void 0 ? _visitorKeys$nodeType2 : [];
489 index = -1;
490 edits = [];
491 if (parent !== deleteNodeSymbol && parent !== undefined) {
492 ancestors.push(parent);
493 }
494 parent = node;
495 }
496 } while (stack !== undefined);
497 if (edits.length !== 0) {
498 return edits[edits.length - 1][1]; // @TODO(vladimir.gorej@gmail.com): can be replaced by Array.prototype.at in future
499 }
500 return root;
501};
502
503/* eslint-enable */
Note: See TracBrowser for help on using the repository browser.