2025-09-05 14:59:21 +08:00

130 lines
3.4 KiB
JavaScript

'use strict';
const {
NODE_TYPE_IDS_MAP,
NODE_TYPES_COUNT,
LEAF_NODE_TYPES_COUNT,
} = require('../generated/lazy/types.js');
// Getter for private `#visitorsArr` property of `Visitor` class. Initialized in class body below.
let getVisitorsArr;
/**
* Visitor class, used to visit an AST.
*/
class Visitor {
#visitorsArr;
/**
* Create `Visitor`.
*
* Provide an object where keys are names of AST nodes you want to visit,
* and values are visitor functions which receive AST node objects of that type.
*
* Keys can also be postfixed with `:exit` to visit when exiting the node, rather than entering.
*
* ```js
* const visitor = new Visitor({
* BinaryExpression(binExpr) {
* // Do stuff when entering a `BinaryExpression`
* },
* 'BinaryExpression:exit'(binExpr) {
* // Do stuff when exiting a `BinaryExpression`
* },
* });
* ```
*
* @class
* @param {Object} visitor - Object defining visit functions for AST nodes
* @returns {Visitor}
*/
constructor(visitor) {
this.#visitorsArr = createVisitorsArr(visitor);
}
static {
getVisitorsArr = visitor => visitor.#visitorsArr;
}
}
module.exports = { Visitor, getVisitorsArr };
/**
* Create array of visitors, keyed by node type ID.
*
* Each element of array is one of:
*
* * No visitor for this type = `null`.
* * Visitor for leaf node = visit function.
* * Visitor for non-leaf node = object of form `{ enter, exit }`,
* where each property is either a visitor function or `null`.
*
* @param {Object} visitor - Visitors object from user
* @returns {Array<Object|Function|null>} - Array of visitors
*/
function createVisitorsArr(visitor) {
if (visitor === null || typeof visitor !== 'object') {
throw new Error('`visitor` must be an object');
}
// Create empty visitors array
const visitorsArr = [];
for (let i = NODE_TYPES_COUNT; i !== 0; i--) {
visitorsArr.push(null);
}
// Populate visitors array from provided object
for (let name of Object.keys(visitor)) {
const visitFn = visitor[name];
if (typeof visitFn !== 'function') {
throw new Error(`'${name}' property of \`visitor\` object is not a function`);
}
const isExit = name.endsWith(':exit');
if (isExit) name = name.slice(0, -5);
const typeId = NODE_TYPE_IDS_MAP.get(name);
if (typeId === void 0) throw new Error(`Unknown node type '${name}' in \`visitor\` object`);
if (typeId < LEAF_NODE_TYPES_COUNT) {
// Leaf node. Store just 1 function.
const existingVisitFn = visitorsArr[typeId];
if (existingVisitFn === null) {
visitorsArr[typeId] = visitFn;
} else if (isExit) {
visitorsArr[typeId] = combineVisitFunctions(existingVisitFn, visitFn);
} else {
visitorsArr[typeId] = combineVisitFunctions(visitFn, existingVisitFn);
}
continue;
}
let enterExit = visitorsArr[typeId];
if (enterExit === null) {
enterExit = visitorsArr[typeId] = { enter: null, exit: null };
}
if (isExit) {
enterExit.exit = visitFn;
} else {
enterExit.enter = visitFn;
}
}
return visitorsArr;
}
/**
* Combine 2 visitor functions into 1.
*
* @param {function} visit1 - 1st visitor function
* @param {function} visit2 - 2nd visitor function
* @returns {function} - Combined visitor function
*/
function combineVisitFunctions(visit1, visit2) {
return function(node) {
visit1(node);
visit2(node);
};
}