| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 | /** * @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 {    /**     * @param {string} id - An identifier.     * @param {CodePath|null} upper - The code path of the upper function scope.     * @param {Function} onLooped - A callback function to notify looping.     */    constructor(id, upper, onLooped) {        /**         * The identifier of this code path.         * Rules use it to store additional information of each rule.         * @type {string}         */        this.id = id;        /**         * 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.     * @type {CodePathSegment}     */    get initialSegment() {        return this.internal.initialSegment;    }    /**     * Final code path segments.     * This array is a mix of `returnedSegments` and `thrownSegments`.     * @type {CodePathSegment[]}     */    get finalSegments() {        return this.internal.finalSegments;    }    /**     * Final code path segments which is with `return` statements.     * This array contains the last path segment if it's reachable.     * Since the reachable last path returns `undefined`.     * @type {CodePathSegment[]}     */    get returnedSegments() {        return this.internal.returnedForkContext;    }    /**     * Final code path segments which is with `throw` statements.     * @type {CodePathSegment[]}     */    get thrownSegments() {        return this.internal.thrownForkContext;    }    /**     * Current code path segments.     * @type {CodePathSegment[]}     */    get currentSegments() {        return this.internal.currentSegments;    }    /**     * Traverses all segments in this code path.     *     *     codePath.traverseSegments(function(segment, controller) {     *         // do something.     *     });     *     * This method enumerates segments in order from the head.     *     * The `controller` object has two methods.     *     * - `controller.skip()` - Skip the following segments in this branch.     * - `controller.break()` - Skip all following segments.     *     * @param {Object} [options] - Omittable.     * @param {CodePathSegment} [options.first] - The first segment to traverse.     * @param {CodePathSegment} [options.last] - The last segment to traverse.     * @param {Function} callback - A callback function.     * @returns {void}     */    traverseSegments(options, callback) {        let resolvedOptions;        let resolvedCallback;        if (typeof options === "function") {            resolvedCallback = options;            resolvedOptions = {};        } else {            resolvedOptions = options || {};            resolvedCallback = callback;        }        const startSegment = resolvedOptions.first || this.internal.initialSegment;        const lastSegment = resolvedOptions.last;        let item = null;        let index = 0;        let end = 0;        let segment = null;        const visited = Object.create(null);        const stack = [[startSegment, 0]];        let skippedSegment = null;        let broken = false;        const controller = {            skip() {                if (stack.length <= 1) {                    broken = true;                } else {                    skippedSegment = stack[stack.length - 2][0];                }            },            break() {                broken = true;            }        };        /**         * Checks 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[prevSegment.id] ||                segment.isLoopedPrevSegment(prevSegment)            );        }        while (stack.length > 0) {            item = stack[stack.length - 1];            segment = item[0];            index = item[1];            if (index === 0) {                // Skip if this segment has been visited already.                if (visited[segment.id]) {                    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 flag of skipping if all branches have been skipped.                if (skippedSegment && segment.prevSegments.indexOf(skippedSegment) !== -1) {                    skippedSegment = null;                }                visited[segment.id] = true;                // Call the callback when the first time.                if (!skippedSegment) {                    resolvedCallback.call(this, segment, controller);                    if (segment === lastSegment) {                        controller.skip();                    }                    if (broken) {                        break;                    }                }            }            // Update the stack.            end = segment.nextSegments.length - 1;            if (index < end) {                item[1] += 1;                stack.push([segment.nextSegments[index], 0]);            } else if (index === end) {                item[0] = segment.nextSegments[index];                item[1] = 0;            } else {                stack.pop();            }        }    }}module.exports = CodePath;
 |