You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
|
|
/** * @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 {
// eslint-disable-next-line jsdoc/require-description
/** * @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;
|