406 lines
13 KiB
JavaScript
406 lines
13 KiB
JavaScript
// Functions to compile 1 or more visitor objects into a single compiled visitor.
|
|
//
|
|
// # Visitor objects
|
|
//
|
|
// Visitor objects which are generated by rules' `create` functions have keys being either:
|
|
// * Name of an AST type. or
|
|
// * Name of an AST type postfixed with `:exit`.
|
|
//
|
|
// Each property value must be a function that handles that AST node.
|
|
//
|
|
// e.g.:
|
|
//
|
|
// ```
|
|
// {
|
|
// BinaryExpression(node) {
|
|
// // Do stuff on enter
|
|
// },
|
|
// 'BinaryExpression:exit'(node) {
|
|
// // Do stuff on exit
|
|
// },
|
|
// }
|
|
// ```
|
|
//
|
|
// # Compiled visitor
|
|
//
|
|
// Compiled visitor is an array with `NODE_TYPES_COUNT` length, keyed by the ID of the node type.
|
|
// `NODE_TYPE_IDS_MAP` maps from type name to ID.
|
|
//
|
|
// Each element of compiled 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`.
|
|
//
|
|
// e.g.:
|
|
//
|
|
// ```
|
|
// [
|
|
// // Leaf nodes
|
|
// function(node) { /* do stuff */ },
|
|
// // ...
|
|
//
|
|
// // Non-leaf nodes
|
|
// {
|
|
// enter: function(node) { /* do stuff */ },
|
|
// exit: null,
|
|
// },
|
|
// // ...
|
|
// ]
|
|
// ```
|
|
//
|
|
// # Object reuse
|
|
//
|
|
// No more than 1 compiled visitor exists at any time, so we reuse a single array `compiledVisitor`,
|
|
// rather than creating a new array for each file being linted.
|
|
//
|
|
// To compile visitors, call:
|
|
// * `initCompiledVisitor` once.
|
|
// * `addVisitorToCompiled` with each visitor object.
|
|
// * `finalizeCompiledVisitor` once.
|
|
//
|
|
// After this sequence of calls, `compiledVisitor` is ready to be used to walk the AST.
|
|
//
|
|
// We also recycle:
|
|
//
|
|
// * `{ enter, exit }` objects which are stored in compiled visitor.
|
|
// * Temporary arrays used to store multiple visit functions, which are merged into a single function
|
|
// in `finalizeCompiledVisitor`.
|
|
//
|
|
// The aim is to reduce pressure on the garbage collector. All these recycled objects are long-lived
|
|
// and will graduate to "old space", which leaves as much capacity as possible in "new space"
|
|
// for objects created by user code in visitors. If ephemeral user-created objects all fit in new space,
|
|
// it will avoid full GC runs, which should greatly improve performance.
|
|
|
|
import {
|
|
LEAF_NODE_TYPES_COUNT,
|
|
NODE_TYPE_IDS_MAP,
|
|
NODE_TYPES_COUNT,
|
|
} from "../../generated/visit/type_ids.js";
|
|
|
|
const { isArray } = Array;
|
|
|
|
// Compiled visitor used for visiting each file.
|
|
// Same array is reused for each file.
|
|
//
|
|
// Initialized with `.push()` to ensure V8 treats the array as "packed" (linear array),
|
|
// not "holey" (hash map). This is critical, as looking up elements in this array is a very hot path
|
|
// during AST visitation, and holey arrays are much slower.
|
|
// https://v8.dev/blog/elements-kinds
|
|
let compiledVisitor;
|
|
|
|
export function createCompiledVisitor() {
|
|
// Create a new compiled visitor array
|
|
compiledVisitor = [];
|
|
for (let i = NODE_TYPES_COUNT; i !== 0; i--) {
|
|
compiledVisitor.push(null);
|
|
}
|
|
return compiledVisitor;
|
|
}
|
|
|
|
// Arrays containing type IDs of types which have multiple visit functions defined for them.
|
|
//
|
|
// Filled with `0` initially up to maximum size they could ever need to be so:
|
|
// 1. These arrays never need to grow.
|
|
// 2. V8 treats these arrays as "PACKED_SMI_ELEMENTS".
|
|
const mergedLeafVisitorTypeIds = [],
|
|
mergedEnterVisitorTypeIds = [],
|
|
mergedExitVisitorTypeIds = [];
|
|
|
|
for (let i = LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
|
|
mergedLeafVisitorTypeIds.push(0);
|
|
}
|
|
|
|
for (let i = NODE_TYPES_COUNT - LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
|
|
mergedEnterVisitorTypeIds.push(0);
|
|
mergedExitVisitorTypeIds.push(0);
|
|
}
|
|
|
|
mergedLeafVisitorTypeIds.length = 0;
|
|
mergedEnterVisitorTypeIds.length = 0;
|
|
mergedExitVisitorTypeIds.length = 0;
|
|
|
|
// `true` if `addVisitor` has been called with a visitor which visits at least one AST type
|
|
let hasActiveVisitors = false;
|
|
|
|
// Enter+exit object cache.
|
|
//
|
|
// `compiledVisitor` may contain many `{ enter, exit }` objects.
|
|
// Use this cache to reuse those objects across all visitor compilations.
|
|
//
|
|
// `enterExitObjectCacheNextIndex` is the index of first object in cache which is currently unused.
|
|
// It may point to the end of the cache array.
|
|
const enterExitObjectCache = [];
|
|
let enterExitObjectCacheNextIndex = 0;
|
|
|
|
function getEnterExitObject() {
|
|
if (enterExitObjectCacheNextIndex < enterExitObjectCache.length) {
|
|
return enterExitObjectCache[enterExitObjectCacheNextIndex++];
|
|
}
|
|
|
|
const enterExit = { enter: null, exit: null };
|
|
enterExitObjectCache.push(enterExit);
|
|
enterExitObjectCacheNextIndex++;
|
|
return enterExit;
|
|
}
|
|
|
|
// Visit function arrays cache.
|
|
//
|
|
// During compilation, many arrays may be used temporarily to store multiple visit functions for same AST type.
|
|
// The functions in each array are merged into a single function in `finalizeCompiledVisitor`,
|
|
// after which these arrays aren't used again.
|
|
//
|
|
// Use this cache to reuse these arrays across each visitor compilation.
|
|
//
|
|
// `visitFnArrayCacheNextIndex` is the index of first array in cache which is currently unused.
|
|
// It may point to the end of the cache array.
|
|
const visitFnArrayCache = [];
|
|
let visitFnArrayCacheNextIndex = 0;
|
|
|
|
function createVisitFnArray(visit1, visit2) {
|
|
if (visitFnArrayCacheNextIndex < visitFnArrayCache.length) {
|
|
const arr = visitFnArrayCache[visitFnArrayCacheNextIndex++];
|
|
arr.push(visit1, visit2);
|
|
return arr;
|
|
}
|
|
|
|
const arr = [visit1, visit2];
|
|
visitFnArrayCache.push(arr);
|
|
visitFnArrayCacheNextIndex++;
|
|
return arr;
|
|
}
|
|
|
|
/**
|
|
* Initialize compiled visitor, ready for calls to `addVisitor`.
|
|
*/
|
|
export function initCompiledVisitor() {
|
|
// Reset `compiledVisitor` array after previous compilation
|
|
for (let i = 0; i < NODE_TYPES_COUNT; i++) {
|
|
compiledVisitor[i] = null;
|
|
}
|
|
|
|
// Reset enter+exit objects which were used in previous compilation
|
|
for (let i = 0; i < enterExitObjectCacheNextIndex; i++) {
|
|
const enterExit = enterExitObjectCache[i];
|
|
enterExit.enter = null;
|
|
enterExit.exit = null;
|
|
}
|
|
enterExitObjectCacheNextIndex = 0;
|
|
}
|
|
|
|
/**
|
|
* Add a visitor to compiled visitor.
|
|
*
|
|
* @param visitor - Visitor object
|
|
*/
|
|
export function addVisitorToCompiled(visitor) {
|
|
if (visitor === null || typeof visitor !== "object")
|
|
throw new TypeError("Visitor must be an object");
|
|
|
|
// Exit if is empty visitor
|
|
const keys = Object.keys(visitor),
|
|
keysLen = keys.length;
|
|
if (keysLen === 0) return;
|
|
|
|
hasActiveVisitors = true;
|
|
|
|
// Populate visitors array from provided object
|
|
for (let i = 0; i < keysLen; i++) {
|
|
let name = keys[i];
|
|
|
|
const visitFn = visitor[name];
|
|
if (typeof visitFn !== "function") {
|
|
throw new TypeError(`'${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`);
|
|
|
|
const existing = compiledVisitor[typeId];
|
|
if (typeId < LEAF_NODE_TYPES_COUNT) {
|
|
// Leaf node - store just 1 function, not enter+exit pair
|
|
if (existing === null) {
|
|
compiledVisitor[typeId] = visitFn;
|
|
} else if (isArray(existing)) {
|
|
if (isExit) {
|
|
existing.push(visitFn);
|
|
} else {
|
|
// Insert before last in array in case last was enter visit function from the current rule,
|
|
// to ensure enter is called before exit.
|
|
// It could also be either an enter or exit visitor function for another rule, but the order
|
|
// rules are called in doesn't matter. We only need to make sure that a rule's exit visitor
|
|
// isn't called before enter visitor *for that same rule*.
|
|
existing.splice(existing.length - 1, 0, visitFn);
|
|
}
|
|
} else {
|
|
// Same as above, enter visitor is put to front of list to make sure enter is called before exit
|
|
compiledVisitor[typeId] = isExit
|
|
? createVisitFnArray(existing, visitFn)
|
|
: createVisitFnArray(visitFn, existing);
|
|
mergedLeafVisitorTypeIds.push(typeId);
|
|
}
|
|
} else {
|
|
// Not leaf node - store enter+exit pair
|
|
if (existing === null) {
|
|
const enterExit = (compiledVisitor[typeId] = getEnterExitObject());
|
|
if (isExit) {
|
|
enterExit.exit = visitFn;
|
|
} else {
|
|
enterExit.enter = visitFn;
|
|
}
|
|
} else if (isExit) {
|
|
const { exit } = existing;
|
|
if (exit === null) {
|
|
existing.exit = visitFn;
|
|
} else if (isArray(exit)) {
|
|
exit.push(visitFn);
|
|
} else {
|
|
existing.exit = createVisitFnArray(exit, visitFn);
|
|
mergedExitVisitorTypeIds.push(typeId);
|
|
}
|
|
} else {
|
|
const { enter } = existing;
|
|
if (enter === null) {
|
|
existing.enter = visitFn;
|
|
} else if (isArray(enter)) {
|
|
enter.push(visitFn);
|
|
} else {
|
|
existing.enter = createVisitFnArray(enter, visitFn);
|
|
mergedEnterVisitorTypeIds.push(typeId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finalize compiled visitor.
|
|
*
|
|
* After calling this function, `compiledVisitor` is ready to be used to walk the AST.
|
|
*
|
|
* @returns {boolean} - `true` if compiled visitor visits at least 1 AST type
|
|
*/
|
|
export function finalizeCompiledVisitor() {
|
|
if (hasActiveVisitors === false) return false;
|
|
|
|
// Merge visit functions for node types which have multiple visitors from different rules,
|
|
// or enter+exit functions for leaf nodes
|
|
for (let i = mergedLeafVisitorTypeIds.length - 1; i >= 0; i--) {
|
|
const typeId = mergedLeafVisitorTypeIds[i];
|
|
compiledVisitor[typeId] = mergeVisitFns(compiledVisitor[typeId]);
|
|
}
|
|
|
|
for (let i = mergedEnterVisitorTypeIds.length - 1; i >= 0; i--) {
|
|
const typeId = mergedEnterVisitorTypeIds[i];
|
|
const enterExit = compiledVisitor[typeId];
|
|
enterExit.enter = mergeVisitFns(enterExit.enter);
|
|
}
|
|
|
|
for (let i = mergedExitVisitorTypeIds.length - 1; i >= 0; i--) {
|
|
const typeId = mergedExitVisitorTypeIds[i];
|
|
const enterExit = compiledVisitor[typeId];
|
|
enterExit.exit = mergeVisitFns(enterExit.exit);
|
|
}
|
|
|
|
// Reset state, ready for next time
|
|
mergedLeafVisitorTypeIds.length = 0;
|
|
mergedEnterVisitorTypeIds.length = 0;
|
|
mergedExitVisitorTypeIds.length = 0;
|
|
|
|
// Note: Visit function arrays have been emptied in `mergeVisitFns`, so all arrays in `visitFnArrayCache`
|
|
// are now empty and ready for reuse. We just need to reset the index.
|
|
visitFnArrayCacheNextIndex = 0;
|
|
|
|
hasActiveVisitors = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Merge array of visit functions into a single function, which calls each of input functions in turn.
|
|
*
|
|
* The array passed is cleared (length set to 0), so the array can be reused.
|
|
*
|
|
* The merged function is statically defined and does not contain a loop, to hopefully allow
|
|
* JS engine to heavily optimize it.
|
|
*
|
|
* `mergers` contains pre-defined functions to merge up to 5 visit functions.
|
|
* Merger functions for merging more than 5 visit functions are created dynamically on demand.
|
|
*
|
|
* @param visitFns - Array of visit functions
|
|
* @returns Function which calls all of `visitFns` in turn.
|
|
*/
|
|
function mergeVisitFns(visitFns) {
|
|
const numVisitFns = visitFns.length;
|
|
|
|
// Get or create merger for merging `numVisitFns` functions
|
|
let merger;
|
|
if (mergers.length <= numVisitFns) {
|
|
while (mergers.length < numVisitFns) {
|
|
mergers.push(null);
|
|
}
|
|
merger = createMerger(numVisitFns);
|
|
mergers.push(merger);
|
|
} else {
|
|
merger = mergers[numVisitFns];
|
|
if (merger === null) merger = mergers[numVisitFns] = createMerger(numVisitFns);
|
|
}
|
|
|
|
// Merge functions
|
|
const mergedFn = merger(...visitFns);
|
|
|
|
// Empty `visitFns` array, so it can be reused
|
|
visitFns.length = 0;
|
|
|
|
return mergedFn;
|
|
}
|
|
|
|
/**
|
|
* Create a merger function that merges `fnCount` functions.
|
|
*
|
|
* @param fnCount - Number of functions to be merged
|
|
* @returns Function to merge `fnCount` functions
|
|
*/
|
|
function createMerger(fnCount) {
|
|
const args = [];
|
|
let body = "return node=>{";
|
|
for (let i = 1; i <= fnCount; i++) {
|
|
args.push(`visit${i}`);
|
|
body += `visit${i}(node);`;
|
|
}
|
|
body += "}";
|
|
args.push(body);
|
|
// oxlint-disable-next-line typescript/no-implied-eval
|
|
return new Function(...args);
|
|
}
|
|
|
|
// Pre-defined mergers for merging up to 5 functions
|
|
const mergers = [
|
|
null, // No merger for 0 functions
|
|
null, // No merger for 1 function
|
|
(visit1, visit2) => (node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
},
|
|
(visit1, visit2, visit3) => (node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
visit3(node);
|
|
},
|
|
(visit1, visit2, visit3, visit4) => (node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
visit3(node);
|
|
visit4(node);
|
|
},
|
|
(visit1, visit2, visit3, visit4, visit5) => (node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
visit3(node);
|
|
visit4(node);
|
|
visit5(node);
|
|
},
|
|
];
|