Spaces:
Running
Running
; | |
Object.defineProperty(exports, '__esModule', { | |
value: true, | |
}); | |
exports.BREAK = void 0; | |
exports.getEnterLeaveForKind = getEnterLeaveForKind; | |
exports.getVisitFn = getVisitFn; | |
exports.visit = visit; | |
exports.visitInParallel = visitInParallel; | |
var _devAssert = require('../jsutils/devAssert.js'); | |
var _inspect = require('../jsutils/inspect.js'); | |
var _ast = require('./ast.js'); | |
var _kinds = require('./kinds.js'); | |
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 | |
* } | |
* }) | |
* ``` | |
*/ | |
exports.BREAK = BREAK; | |
function visit(root, visitor, visitorKeys = _ast.QueryDocumentKeys) { | |
const enterLeaveMap = new Map(); | |
for (const kind of Object.values(_kinds.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; | |
(0, _ast.isNode)(node) || | |
(0, _devAssert.devAssert)( | |
false, | |
`Invalid AST Node: ${(0, _inspect.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 ((0, _ast.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. | |
*/ | |
function visitInParallel(visitors) { | |
const skipping = new Array(visitors.length).fill(null); | |
const mergedVisitor = Object.create(null); | |
for (const kind of Object.values(_kinds.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. | |
*/ | |
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 */ | |
function getVisitFn(visitor, kind, isLeaving) { | |
const { enter, leave } = getEnterLeaveForKind(visitor, kind); | |
return isLeaving ? leave : enter; | |
} | |