estree-toolkit
Version:
Traverser, scope tracker, and more tools for working with ESTree AST
142 lines (141 loc) • 5.34 kB
JavaScript
import { Context, NodePath } from './nodepath.mjs';
import { visitorKeys } from './definitions.mjs';
import { aliases } from './aliases.mjs';
import { getNodeValidationEnabled, setNodeValidationEnabled } from './builders.mjs';
export class Traverser {
constructor(visitors) {
this.visitors = visitors;
}
visitPath(visitorCtx, path, state, visitedPaths, visitOnlyChildren = false) {
const { node, ctx } = path;
if (visitedPaths.has(path) ||
path.removed ||
node == null) {
return;
}
const nodeType = node.type;
const visitor = this.visitors[nodeType] || {};
ctx.newQueue();
const cleanup = () => ctx.popQueue();
if (!visitOnlyChildren) {
// NOTE: If ctx.makeScope is `false`, it can cause the parent scope to reset to `null`
path.init();
if (ctx.shouldSkip(path))
return cleanup();
if (visitor.enter != null) {
visitor.enter.call(visitorCtx, path, state);
if (ctx.shouldSkip(path) || visitorCtx.stopped)
return cleanup();
}
}
const keys = visitorKeys[nodeType] || 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)) {
const childNodePaths = value.map((childNode, index) => (NodePath.for({
node: childNode,
key: index,
listKey: key,
parentPath: path,
ctx: ctx
}).init()));
for (let i = 0; i < childNodePaths.length; i++) {
this.visitPath(visitorCtx, childNodePaths[i], state, visitedPaths);
if (visitorCtx.stopped)
return cleanup();
}
}
else if (typeof value.type === 'string') {
this.visitPath(visitorCtx, NodePath.for({
node: value,
key: key,
listKey: null,
parentPath: path,
ctx: ctx
}).init(), state, visitedPaths);
if (visitorCtx.stopped)
return cleanup();
}
}
if (!visitOnlyChildren && visitor.leave != null) {
visitor.leave.call(visitorCtx, path, state);
if (visitorCtx.stopped)
return cleanup();
}
visitedPaths.add(path);
const { new: newPaths, unSkipped: unSkippedPaths } = cleanup();
for (let i = 0; i < newPaths.length; i++) {
if (visitorCtx.stopped)
break;
this.visitPath(visitorCtx, newPaths[i], state, visitedPaths);
}
for (let i = 0; i < unSkippedPaths.length; i++) {
if (visitorCtx.stopped)
break;
this.visitPath(visitorCtx, unSkippedPaths[i], state, visitedPaths);
}
}
static expandVisitors(visitors) {
const expandedVisitors = Object.create(null);
// You can use functional approach here
// Because this code won't run many times like other code does
const expand = (obj) => {
Object.keys(obj).forEach((keyName) => {
const keys = [].concat(...keyName.split('|').map((key) => key in aliases ? Object.keys(aliases[key]) : [key]));
const visitor = obj[keyName];
if (typeof visitor == 'function') {
keys.forEach((key) => {
expandedVisitors[key] = { enter: visitor };
});
}
else if (typeof visitor == 'object') {
keys.forEach((key) => {
expandedVisitors[key] = {
enter: visitor.enter,
leave: visitor.leave
};
});
}
});
};
if (visitors.comp != null)
expand(visitors.comp);
expand(visitors);
return expandedVisitors;
}
static traverseNode(data) {
const visitorCtx = {
stopped: false,
stop() {
this.stopped = true;
}
};
const prevNodeValidationEnabled = getNodeValidationEnabled();
setNodeValidationEnabled(data.ctx.shouldValidateNodes);
new Traverser(data.expand ? this.expandVisitors(data.visitors) : data.visitors).visitPath(visitorCtx, NodePath.for({
node: data.node,
key: null,
listKey: null,
parentPath: data.parentPath,
ctx: data.ctx
}), data.state, new WeakSet(), data.visitOnlyChildren);
setNodeValidationEnabled(prevNodeValidationEnabled);
}
}
export const traverse = (node, visitors, state) => {
const ctx = new Context(visitors.$);
if (node.type !== 'Program') {
ctx.makeScope = false;
}
Traverser.traverseNode({
node: node,
parentPath: null,
visitors,
state,
ctx,
expand: true,
});
};