UNPKG

estree-toolkit

Version:

Traverser, scope tracker, and more tools for working with ESTree AST

498 lines (497 loc) 16.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodePath = exports.Context = void 0; const traverse_1 = require("./traverse"); const scope_1 = require("./scope"); const is_1 = require("./is"); const definitions_1 = require("./definitions"); const builders_1 = require("./builders"); // * Tip: Fold the regions for better experience const mapSet = (map, key, value) => { map.set(key, value); return value; }; class Context { constructor(options) { /** * Don't depend on `pathCache` to get children, * because it may not be initialized when you call it */ this.pathCache = new Map(); this.scopeCache = new Map(); this.makeScope = false; this.shouldValidateNodes = (0, builders_1.getNodeValidationEnabled)(); this.cloneFunction = (node) => structuredClone(node); this.currentSkipPaths = new Set(); this.skipPathSetStack = [this.currentSkipPaths]; /** Store newly added nodes to this queue for traversal */ this.queueStack = []; this.makeScope = (options === null || options === void 0 ? void 0 : options.scope) === true; if ((options === null || options === void 0 ? void 0 : options.validateNodes) != null) { this.shouldValidateNodes = options.validateNodes; } if (typeof (options === null || options === void 0 ? void 0 : options.cloneFunction) === 'function') { this.cloneFunction = options.cloneFunction; } } setSkipped(path) { this.currentSkipPaths.add(path); } setNotSkipped(path) { this.currentSkipPaths.delete(path); } shouldSkip(path) { return this.currentSkipPaths.has(path); } updateCurrentSkipPaths() { this.currentSkipPaths = this.skipPathSetStack[this.skipPathSetStack.length - 1]; } newSkipPathStack() { this.skipPathSetStack.push(new Set()); this.updateCurrentSkipPaths(); } restorePrevSkipPathStack() { this.skipPathSetStack.pop(); this.updateCurrentSkipPaths(); } pushToQueue(paths, stackName) { const last = this.queueStack[this.queueStack.length - 1]; if (last != null) last[stackName].push(...paths); } newQueue() { this.queueStack.push({ new: [], unSkipped: [] }); } popQueue() { return this.queueStack.pop(); } } exports.Context = Context; const runInsertionValidation = (node, key, listKey, parent) => { if (!(0, builders_1.getNodeValidationEnabled)()) return; const definition = definitions_1.definitions[node.type]; if (definition != null && definition.insertionValidate != null) { const errorMsg = definition.insertionValidate(node, key, listKey, parent); if (errorMsg != null) { throw new Error(errorMsg); } } }; class NodePath { // accessKey = ''; constructor(data) { this.node = data.node; this.type = this.node && this.node.type; this.key = data.key; this.listKey = data.listKey; this.parentPath = data.parentPath; this.parent = this.parentPath && this.parentPath.node; this.container = this.listKey ? this.parent[this.listKey] : this.parent; this.removed = false; this.ctx = data.ctx; this.scope = undefined; // this.accessKey = (this.parentPath?.accessKey || '') + '.' + this.type; } /** Get the cached NodePath object or create new if cache is not available */ static for(data) { if (data.node == null) { // Don't cache a null NodePath return new this(data); } const { ctx: { pathCache }, parentPath } = data; const children = pathCache.get(parentPath) || mapSet(pathCache, parentPath, new Map()); return (children.get(data.node) || mapSet(children, data.node, new NodePath(data))); } init(parentScope) { var _a; if (this.ctx.makeScope) { this.scope = scope_1.Scope.for(this, parentScope || ((_a = this.parentPath) === null || _a === void 0 ? void 0 : _a.scope) || null); if (this.scope != null) this.scope.init(); } return this; } throwNoParent(methodName) { throw new Error(`Can not use \`${methodName}\` on a NodePath which does not have a parent`); } assertNotRemoved() { /* istanbul ignore next */ if (this.removed) { throw new Error('Path is removed and it is now read-only'); } } assertNotNull(methodName) { /* istanbul ignore next */ if (this.node == null) { throw new Error(`Can not use method \`${methodName}\` on a null NodePath`); } } get parentKey() { return this.listKey != null ? this.listKey : this.key; } cloneNode() { return this.ctx.cloneFunction(this.node); } //#region Traversal skip() { this.ctx.setSkipped(this); } skipChildren() { this.assertNotNull('skipChildren'); const node = this.node; const keys = definitions_1.visitorKeys[this.type] || Object.keys(node); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = node[key]; if (value == null) continue; if (Array.isArray(value)) { this.get(key).forEach((p) => p.skip()); } else if (typeof value.type === 'string') { this.get(key).skip(); } } } unSkip() { this.assertNotRemoved(); this.ctx.setNotSkipped(this); this.ctx.pushToQueue([this], 'unSkipped'); } unskip() { this.unSkip(); } unSkipChildren() { var _a; this.assertNotRemoved(); this.assertNotNull('unSkipChildren'); // We can use `pathCache` here because it has already been // built when `skipChildren` was used // And if `pathCache` has not been built that means // the children were not skipped in the first place (_a = this.ctx.pathCache.get(this)) === null || _a === void 0 ? void 0 : _a.forEach((p) => p.unSkip()); } unskipChildren() { this.unSkipChildren(); } traverse(visitors, state) { this.assertNotNull('traverse'); traverse_1.Traverser.traverseNode({ node: this.node, parentPath: this.parentPath, visitors, state, ctx: this.ctx, expand: true }); } //#endregion //#region Ancestry findParent(predicate) { let parent = this.parentPath; while (parent != null) { if (predicate(parent)) return parent; parent = parent.parentPath; } return null; } find(predicate) { // eslint-disable-next-line @typescript-eslint/no-this-alias let nodePath = this; while (nodePath != null) { if (predicate(nodePath)) return nodePath; nodePath = nodePath.parentPath; } return null; } getFunctionParent() { return this.findParent((p) => is_1.is.function(p)); } getAncestry() { const ancestors = []; // eslint-disable-next-line @typescript-eslint/no-this-alias let ancestor = this; while (ancestor != null) { ancestors.push(ancestor); ancestor = ancestor.parentPath; } return ancestors; } isAncestorOf(path) { return path.isDescendantOf(this); } isDescendantOf(path) { return this.findParent((p) => p === path) != null; } //#endregion //#region Modification updateSiblingIndex(fromIndex, incrementBy) { var _a; if (this.container.length === 0) return; (_a = this.ctx.pathCache.get(this.parentPath)) === null || _a === void 0 ? void 0 : _a.forEach((path) => { if (path.key >= fromIndex) { path.key += incrementBy; } }); } insertBefore(nodes) { this.assertNotRemoved(); // TODO: Handle more cases if (!Array.isArray(this.container)) { throw new Error('Can not insert before a node where `container` is not an Array'); } const key = this.key; for (let i = 0; i < nodes.length; i++) { runInsertionValidation(nodes[i], key + i, this.listKey, this.parent); } this.container.splice(key, 0, ...nodes); this.updateSiblingIndex(key, nodes.length); const newPaths = nodes.map((node, idx) => (NodePath.for({ node, key: key + idx, listKey: this.listKey, parentPath: this.parentPath, ctx: this.ctx }).init())); this.ctx.pushToQueue(newPaths, 'new'); return newPaths; } insertAfter(nodes) { this.assertNotRemoved(); // TODO: Handle more cases if (!Array.isArray(this.container)) { throw new Error('Can not insert after a node where `container` is not an Array'); } const key = this.key; for (let i = 0; i < nodes.length; i++) { runInsertionValidation(nodes[i], key + i + 1, this.listKey, this.parent); } this.container.splice(key + 1, 0, ...nodes); this.updateSiblingIndex(key + 1, nodes.length); const newPaths = nodes.map((node, idx) => (NodePath.for({ node, key: key + idx + 1, listKey: this.listKey, parentPath: this.parentPath, ctx: this.ctx }).init())); this.ctx.pushToQueue(newPaths, 'new'); return newPaths; } unshiftContainer(listKey, nodes) { this.assertNotRemoved(); const firstNode = this.node[listKey][0]; // Create a virtual NodePath const lastNodePath = NodePath.for({ node: firstNode, key: 0, listKey, parentPath: this, ctx: this.ctx }); const newPaths = lastNodePath.insertBefore(nodes); return newPaths; } pushContainer(listKey, nodes) { this.assertNotRemoved(); const container = this.node[listKey]; const lastNode = container[container.length - 1]; // Create a virtual NodePath const lastNodePath = NodePath.for({ node: lastNode, key: container.length - 1, listKey, parentPath: this, ctx: this.ctx }); const newPaths = lastNodePath.insertAfter(nodes); return newPaths; } get(key) { if (this.node == null) { throw new Error('Can not use method `get` on a null NodePath'); } const value = this.node[key]; if (Array.isArray(value)) { return value.map((node, index) => (NodePath.for({ node, key: index, listKey: key, parentPath: this, ctx: this.ctx }).init())); } else if (value != null && typeof value.type == 'string') { return NodePath.for({ node: value, key: key, listKey: null, parentPath: this, ctx: this.ctx }).init(); } return NodePath.for({ node: null, key: key, listKey: null, parentPath: this, ctx: this.ctx }).init(); } getSibling(key) { if (this.parentPath == null) { this.throwNoParent('getSibling'); } if (typeof key === 'string') { return this.parentPath.get(key); } else if (this.listKey != null) { return this.parentPath.get(this.listKey)[key]; } } getOpposite() { switch (this.key) { case 'left': return this.getSibling('right'); case 'right': return this.getSibling('left'); } } getPrevSibling() { return this.getSibling(this.key - 1); } getNextSibling() { return this.getSibling(this.key + 1); } getAllPrevSiblings() { if (this.parentPath == null) { this.throwNoParent('getAllPrevSiblings'); } return this.parentPath .get(this.listKey) .slice(0, this.key) .reverse(); } getAllNextSiblings() { if (this.parentPath == null) { this.throwNoParent('getAllNextSiblings'); } return this.parentPath .get(this.listKey) .slice(this.key + 1); } has(key) { var _a; const value = (_a = this.node) === null || _a === void 0 ? void 0 : _a[key]; if (value != null && Array.isArray(value) && value.length === 0) { return false; } return !!value; } is(key) { var _a; return !!((_a = this.node) === null || _a === void 0 ? void 0 : _a[key]); } //#endregion //#region Removal onRemove() { const { parent, key, listKey } = this; const parentT = parent.type; const parentPath = this.parentPath; this.ctx.newSkipPathStack(); switch (true) { case parentT === 'ExpressionStatement' && key === 'expression': case is_1.is.exportDeclaration(parent) && key === 'declaration': case (parentT === 'WhileStatement' || parentT === 'SwitchCase') && key === 'test': case parentT === 'LabeledStatement' && key === 'body': case (parentT === 'VariableDeclaration' && listKey === 'declarations' && parent.declarations.length === 1): parentPath.remove(); return true; case parentT === 'BinaryExpression': parentPath.replaceWith(parent[key === 'right' ? 'left' : 'right']); return true; case parentT === 'IfStatement' && key === 'consequent': case (parentT === 'ArrowFunctionExpression' || is_1.is.loop(parent)) && key === 'body': if (parentT === 'ArrowFunctionExpression') { parent.expression = false; } this.replaceWith({ type: 'BlockStatement', body: [] }); return true; } this.ctx.restorePrevSkipPathStack(); if (this.scope != null) scope_1.Scope.handleRemoval(this.scope, this); return false; } markRemoved() { var _a; (_a = this.ctx.pathCache.get(this.parentPath)) === null || _a === void 0 ? void 0 : _a.delete(this.node); this.removed = true; } remove() { if (this.removed) { throw new Error('Node is already removed'); } if (this.container == null) { this.throwNoParent('remove'); } if (this.onRemove()) { // Things are handled by `onRemove` function. return this.markRemoved(); } if (this.listKey != null) { const key = this.key; const container = this.container; container.splice(key, 1); this.markRemoved(); this.updateSiblingIndex(key + 1, -1); } else if (this.key != null) { this.container[this.key] = null; this.markRemoved(); } } //#endregion //#region Replacement replaceWith(node) { if (this.container == null) { this.throwNoParent('replaceWith'); } if (this.removed) { throw new Error('Node is already removed'); } runInsertionValidation(node, this.key, this.listKey, this.parent); this.container[this.key] = node; this.markRemoved(); const newPath = NodePath.for({ node, key: this.key, listKey: this.listKey, parentPath: this.parentPath, ctx: this.ctx }).init(); this.ctx.pushToQueue([newPath], 'new'); return newPath; } replaceWithMultiple(nodes) { if (this.container == null) { this.throwNoParent('replaceWith'); } if (this.removed) { throw new Error('Node is already removed'); } const newPath = this.replaceWith(nodes[0]); return [newPath].concat(newPath.insertAfter(nodes.slice(1))); } } exports.NodePath = NodePath;