| import { devAssert } from '../jsutils/devAssert.mjs'; |
| import { inspect } from '../jsutils/inspect.mjs'; |
| import { isNode, QueryDocumentKeys } from './ast.mjs'; |
| import { Kind } from './kinds.mjs'; |
| /** |
| * A visitor is provided to visit, it contains the collection of |
| * relevant functions to be called during the visitor's traversal. |
| */ |
|
|
| export const BREAK = Object.freeze({}); |
| /** |
| * visit() will walk through an AST using a depth-first traversal, calling |
| * the visitor's enter function at each node in the traversal, and calling the |
| * leave function after visiting that node and all of its child nodes. |
| * |
| * By returning different values from the enter and leave functions, the |
| * behavior of the visitor can be altered, including skipping over a sub-tree of |
| * the AST (by returning false), editing the AST by returning a value or null |
| * to remove the value, or to stop the whole traversal by returning BREAK. |
| * |
| * When using visit() to edit an AST, the original AST will not be modified, and |
| * a new version of the AST with the changes applied will be returned from the |
| * visit function. |
| * |
| * ```ts |
| * const editedAST = visit(ast, { |
| * enter(node, key, parent, path, ancestors) { |
| * // @return |
| * // undefined: no action |
| * // false: skip visiting this node |
| * // visitor.BREAK: stop visiting altogether |
| * // null: delete this node |
| * // any value: replace this node with the returned value |
| * }, |
| * leave(node, key, parent, path, ancestors) { |
| * // @return |
| * // undefined: no action |
| * // false: no action |
| * // visitor.BREAK: stop visiting altogether |
| * // null: delete this node |
| * // any value: replace this node with the returned value |
| * } |
| * }); |
| * ``` |
| * |
| * Alternatively to providing enter() and leave() functions, a visitor can |
| * instead provide functions named the same as the kinds of AST nodes, or |
| * enter/leave visitors at a named key, leading to three permutations of the |
| * visitor API: |
| * |
| * 1) Named visitors triggered when entering a node of a specific kind. |
| * |
| * ```ts |
| * visit(ast, { |
| * Kind(node) { |
| * // enter the "Kind" node |
| * } |
| * }) |
| * ``` |
| * |
| * 2) Named visitors that trigger upon entering and leaving a node of a specific kind. |
| * |
| * ```ts |
| * visit(ast, { |
| * Kind: { |
| * enter(node) { |
| * // enter the "Kind" node |
| * } |
| * leave(node) { |
| * // leave the "Kind" node |
| * } |
| * } |
| * }) |
| * ``` |
| * |
| * 3) Generic visitors that trigger upon entering and leaving any node. |
| * |
| * ```ts |
| * visit(ast, { |
| * enter(node) { |
| * // enter any node |
| * }, |
| * leave(node) { |
| * // leave any node |
| * } |
| * }) |
| * ``` |
| */ |
|
|
| export function visit(root, visitor, visitorKeys = QueryDocumentKeys) { |
| const enterLeaveMap = new Map(); |
|
|
| for (const kind of Object.values(Kind)) { |
| enterLeaveMap.set(kind, getEnterLeaveForKind(visitor, kind)); |
| } |
| /* eslint-disable no-undef-init */ |
|
|
| let stack = undefined; |
| let inArray = Array.isArray(root); |
| let keys = [root]; |
| let index = -1; |
| let edits = []; |
| let node = root; |
| let key = undefined; |
| let parent = undefined; |
| const path = []; |
| const ancestors = []; |
| /* eslint-enable no-undef-init */ |
|
|
| do { |
| index++; |
| const isLeaving = index === keys.length; |
| const isEdited = isLeaving && edits.length !== 0; |
|
|
| if (isLeaving) { |
| key = ancestors.length === 0 ? undefined : path[path.length - 1]; |
| node = parent; |
| parent = ancestors.pop(); |
|
|
| if (isEdited) { |
| if (inArray) { |
| node = node.slice(); |
| let editOffset = 0; |
|
|
| for (const [editKey, editValue] of edits) { |
| const arrayKey = editKey - editOffset; |
|
|
| if (editValue === null) { |
| node.splice(arrayKey, 1); |
| editOffset++; |
| } else { |
| node[arrayKey] = editValue; |
| } |
| } |
| } else { |
| node = Object.defineProperties( |
| {}, |
| Object.getOwnPropertyDescriptors(node), |
| ); |
|
|
| for (const [editKey, editValue] of edits) { |
| node[editKey] = editValue; |
| } |
| } |
| } |
|
|
| index = stack.index; |
| keys = stack.keys; |
| edits = stack.edits; |
| inArray = stack.inArray; |
| stack = stack.prev; |
| } else if (parent) { |
| key = inArray ? index : keys[index]; |
| node = parent[key]; |
|
|
| if (node === null || node === undefined) { |
| continue; |
| } |
|
|
| path.push(key); |
| } |
|
|
| let result; |
|
|
| if (!Array.isArray(node)) { |
| var _enterLeaveMap$get, _enterLeaveMap$get2; |
|
|
| isNode(node) || devAssert(false, `Invalid AST Node: ${inspect(node)}.`); |
| const visitFn = isLeaving |
| ? (_enterLeaveMap$get = enterLeaveMap.get(node.kind)) === null || |
| _enterLeaveMap$get === void 0 |
| ? void 0 |
| : _enterLeaveMap$get.leave |
| : (_enterLeaveMap$get2 = enterLeaveMap.get(node.kind)) === null || |
| _enterLeaveMap$get2 === void 0 |
| ? void 0 |
| : _enterLeaveMap$get2.enter; |
| result = |
| visitFn === null || visitFn === void 0 |
| ? void 0 |
| : visitFn.call(visitor, node, key, parent, path, ancestors); |
|
|
| if (result === BREAK) { |
| break; |
| } |
|
|
| if (result === false) { |
| if (!isLeaving) { |
| path.pop(); |
| continue; |
| } |
| } else if (result !== undefined) { |
| edits.push([key, result]); |
|
|
| if (!isLeaving) { |
| if (isNode(result)) { |
| node = result; |
| } else { |
| path.pop(); |
| continue; |
| } |
| } |
| } |
| } |
|
|
| if (result === undefined && isEdited) { |
| edits.push([key, node]); |
| } |
|
|
| if (isLeaving) { |
| path.pop(); |
| } else { |
| var _node$kind; |
|
|
| stack = { |
| inArray, |
| index, |
| keys, |
| edits, |
| prev: stack, |
| }; |
| inArray = Array.isArray(node); |
| keys = inArray |
| ? node |
| : (_node$kind = visitorKeys[node.kind]) !== null && |
| _node$kind !== void 0 |
| ? _node$kind |
| : []; |
| index = -1; |
| edits = []; |
|
|
| if (parent) { |
| ancestors.push(parent); |
| } |
|
|
| parent = node; |
| } |
| } while (stack !== undefined); |
|
|
| if (edits.length !== 0) { |
| // New root |
| return edits[edits.length - 1][1]; |
| } |
|
|
| return root; |
| } |
| /** |
| * Creates a new visitor instance which delegates to many visitors to run in |
| * parallel. Each visitor will be visited for each node before moving on. |
| * |
| * If a prior visitor edits a node, no following visitors will see that node. |
| */ |
|
|
| export function visitInParallel(visitors) { |
| const skipping = new Array(visitors.length).fill(null); |
| const mergedVisitor = Object.create(null); |
|
|
| for (const kind of Object.values(Kind)) { |
| let hasVisitor = false; |
| const enterList = new Array(visitors.length).fill(undefined); |
| const leaveList = new Array(visitors.length).fill(undefined); |
|
|
| for (let i = 0; i < visitors.length; ++i) { |
| const { enter, leave } = getEnterLeaveForKind(visitors[i], kind); |
| hasVisitor || (hasVisitor = enter != null || leave != null); |
| enterList[i] = enter; |
| leaveList[i] = leave; |
| } |
|
|
| if (!hasVisitor) { |
| continue; |
| } |
|
|
| const mergedEnterLeave = { |
| enter(...args) { |
| const node = args[0]; |
|
|
| for (let i = 0; i < visitors.length; i++) { |
| if (skipping[i] === null) { |
| var _enterList$i; |
|
|
| const result = |
| (_enterList$i = enterList[i]) === null || _enterList$i === void 0 |
| ? void 0 |
| : _enterList$i.apply(visitors[i], args); |
|
|
| if (result === false) { |
| skipping[i] = node; |
| } else if (result === BREAK) { |
| skipping[i] = BREAK; |
| } else if (result !== undefined) { |
| return result; |
| } |
| } |
| } |
| }, |
|
|
| leave(...args) { |
| const node = args[0]; |
|
|
| for (let i = 0; i < visitors.length; i++) { |
| if (skipping[i] === null) { |
| var _leaveList$i; |
|
|
| const result = |
| (_leaveList$i = leaveList[i]) === null || _leaveList$i === void 0 |
| ? void 0 |
| : _leaveList$i.apply(visitors[i], args); |
|
|
| if (result === BREAK) { |
| skipping[i] = BREAK; |
| } else if (result !== undefined && result !== false) { |
| return result; |
| } |
| } else if (skipping[i] === node) { |
| skipping[i] = null; |
| } |
| } |
| }, |
| }; |
| mergedVisitor[kind] = mergedEnterLeave; |
| } |
|
|
| return mergedVisitor; |
| } |
| /** |
| * Given a visitor instance and a node kind, return EnterLeaveVisitor for that kind. |
| */ |
|
|
| export function getEnterLeaveForKind(visitor, kind) { |
| const kindVisitor = visitor[kind]; |
|
|
| if (typeof kindVisitor === 'object') { |
| // { Kind: { enter() {}, leave() {} } } |
| return kindVisitor; |
| } else if (typeof kindVisitor === 'function') { |
| // { Kind() {} } |
| return { |
| enter: kindVisitor, |
| leave: undefined, |
| }; |
| } // { enter() {}, leave() {} } |
|
|
| return { |
| enter: visitor.enter, |
| leave: visitor.leave, |
| }; |
| } |
| /** |
| * Given a visitor instance, if it is leaving or not, and a node kind, return |
| * the function the visitor runtime should call. |
| * |
| * @deprecated Please use `getEnterLeaveForKind` instead. Will be removed in v17 |
| */ |
|
|
| /* c8 ignore next 8 */ |
|
|
| export function getVisitFn(visitor, kind, isLeaving) { |
| const { enter, leave } = getEnterLeaveForKind(visitor, kind); |
| return isLeaving ? leave : enter; |
| } |