|                                                                                                                                                                                                                                                                                                                                                      |  | /** * @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;
 |