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.
247 lines
7.3 KiB
247 lines
7.3 KiB
/** |
|
* @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. |
|
* @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.includes(skippedSegment)) { |
|
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;
|
|
|