UNPKG

estree-toolkit

Version:

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

142 lines (141 loc) 5.34 kB
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, }); };