/** * @fileoverview A class of the code path. * @author Toru Nagashima */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const CodePathState = require("./code-path-state"); const IdGenerator = require("./id-generator"); //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** * A code path. */ class CodePath { /** * Creates a new instance. * @param {Object} options Options for the function (see below). * @param {string} options.id An identifier. * @param {string} options.origin The type of code path origin. * @param {CodePath|null} options.upper The code path of the upper function scope. * @param {Function} options.onLooped A callback function to notify looping. */ constructor({ id, origin, upper, onLooped }) { /** * The identifier of this code path. * Rules use it to store additional information of each rule. * @type {string} */ this.id = id; /** * The reason that this code path was started. May be "program", * "function", "class-field-initializer", or "class-static-block". * @type {string} */ this.origin = origin; /** * The code path of the upper function scope. * @type {CodePath|null} */ this.upper = upper; /** * The code paths of nested function scopes. * @type {CodePath[]} */ this.childCodePaths = []; // Initializes internal state. Object.defineProperty( this, "internal", { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) } ); // Adds this into `childCodePaths` of `upper`. if (upper) { upper.childCodePaths.push(this); } } /** * Gets the state of a given code path. * @param {CodePath} codePath A code path to get. * @returns {CodePathState} The state of the code path. */ static getState(codePath) { return codePath.internal; } /** * The initial code path segment. This is the segment that is at the head * of the code path. * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment} */ get initialSegment() { return this.internal.initialSegment; } /** * Final code path segments. These are the terminal (tail) segments in the * code path, which is the combination of `returnedSegments` and `thrownSegments`. * All segments in this array are reachable. * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment[]} */ get finalSegments() { return this.internal.finalSegments; } /** * Final code path segments that represent normal completion of the code path. * For functions, this means both explicit `return` statements and implicit returns, * such as the last reachable segment in a function that does not have an * explicit `return` as this implicitly returns `undefined`. For scripts, * modules, class field initializers, and class static blocks, this means * all lines of code have been executed. * These segments are also present in `finalSegments`. * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment[]} */ get returnedSegments() { return this.internal.returnedForkContext; } /** * Final code path segments that represent `throw` statements. * This is a passthrough to the underlying `CodePathState`. * These segments are also present in `finalSegments`. * @type {CodePathSegment[]} */ get thrownSegments() { return this.internal.thrownForkContext; } /** * Tracks the traversal of the code path through each segment. This array * starts empty and segments are added or removed as the code path is * traversed. This array always ends up empty at the end of a code path * traversal. The `CodePathState` uses this to track its progress through * the code path. * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment[]} * @deprecated */ get currentSegments() { return this.internal.currentSegments; } /** * Traverses all segments in this code path. * * codePath.traverseSegments((segment, controller) => { * // do something. * }); * * This method enumerates segments in order from the head. * * The `controller` argument has two methods: * * - `skip()` - skips the following segments in this branch * - `break()` - skips all following segments in the traversal * * A note on the parameters: the `options` argument is optional. This means * the first argument might be an options object or the callback function. * @param {Object} [optionsOrCallback] Optional first and last segments to traverse. * @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse. * @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse. * @param {Function} callback A callback function. * @returns {void} */ traverseSegments(optionsOrCallback, callback) { // normalize the arguments into a callback and options let resolvedOptions; let resolvedCallback; if (typeof optionsOrCallback === "function") { resolvedCallback = optionsOrCallback; resolvedOptions = {}; } else { resolvedOptions = optionsOrCallback || {}; resolvedCallback = callback; } // determine where to start traversing from based on the options const startSegment = resolvedOptions.first || this.internal.initialSegment; const lastSegment = resolvedOptions.last; // set up initial location information let record = null; let index = 0; let end = 0; let segment = null; // segments that have already been visited during traversal const visited = new Set(); // tracks the traversal steps const stack = [[startSegment, 0]]; // tracks the last skipped segment during traversal let skippedSegment = null; // indicates if we exited early from the traversal let broken = false; /** * Maintains traversal state. */ const controller = { /** * Skip the following segments in this branch. * @returns {void} */ skip() { if (stack.length <= 1) { broken = true; } else { skippedSegment = stack[stack.length - 2][0]; } }, /** * Stop traversal completely - do not traverse to any * other segments. * @returns {void} */ break() { broken = true; } }; /** * Checks if a given previous segment has been visited. * @param {CodePathSegment} prevSegment A previous segment to check. * @returns {boolean} `true` if the segment has been visited. */ function isVisited(prevSegment) { return ( visited.has(prevSegment) || segment.isLoopedPrevSegment(prevSegment) ); } // the traversal while (stack.length > 0) { /* * This isn't a pure stack. We use the top record all the time * but don't always pop it off. The record is popped only if * one of the following is true: * * 1) We have already visited the segment. * 2) We have not visited *all* of the previous segments. * 3) We have traversed past the available next segments. * * Otherwise, we just read the value and sometimes modify the * record as we traverse. */ record = stack[stack.length - 1]; segment = record[0]; index = record[1]; if (index === 0) { // Skip if this segment has been visited already. if (visited.has(segment)) { stack.pop(); continue; } // Skip if all previous segments have not been visited. if (segment !== startSegment && segment.prevSegments.length > 0 && !segment.prevSegments.every(isVisited) ) { stack.pop(); continue; } // Reset the skipping flag if all branches have been skipped. if (skippedSegment && segment.prevSegments.includes(skippedSegment)) { skippedSegment = null; } visited.add(segment); /* * If the most recent segment hasn't been skipped, then we call * the callback, passing in the segment and the controller. */ if (!skippedSegment) { resolvedCallback.call(this, segment, controller); // exit if we're at the last segment if (segment === lastSegment) { controller.skip(); } /* * If the previous statement was executed, or if the callback * called a method on the controller, we might need to exit the * loop, so check for that and break accordingly. */ if (broken) { break; } } } // Update the stack. end = segment.nextSegments.length - 1; if (index < end) { /* * If we haven't yet visited all of the next segments, update * the current top record on the stack to the next index to visit * and then push a record for the current segment on top. * * Setting the current top record's index lets us know how many * times we've been here and ensures that the segment won't be * reprocessed (because we only process segments with an index * of 0). */ record[1] += 1; stack.push([segment.nextSegments[index], 0]); } else if (index === end) { /* * If we are at the last next segment, then reset the top record * in the stack to next segment and set its index to 0 so it will * be processed next. */ record[0] = segment.nextSegments[index]; record[1] = 0; } else { /* * If index > end, that means we have no more segments that need * processing. So, we pop that record off of the stack in order to * continue traversing at the next level up. */ stack.pop(); } } } } module.exports = CodePath;