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