estree-toolkit
Version:
Traverser, scope tracker, and more tools for working with ESTree AST
498 lines (497 loc) • 16.8 kB
JavaScript
"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;