333 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			333 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/**
 | 
						|
 * @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;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * 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;
 | 
						|
		let index;
 | 
						|
		let end;
 | 
						|
		let segment = null;
 | 
						|
 | 
						|
		// segments that have already been visited during traversal
 | 
						|
		const visited = new Set();
 | 
						|
 | 
						|
		// tracks the traversal steps
 | 
						|
		const stack = [[startSegment, 0]];
 | 
						|
 | 
						|
		// segments that have been skipped during traversal
 | 
						|
		const skipped = new Set();
 | 
						|
 | 
						|
		// 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() {
 | 
						|
				skipped.add(segment);
 | 
						|
			},
 | 
						|
 | 
						|
			/**
 | 
						|
			 * 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)
 | 
						|
			);
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Checks if a given previous segment has been skipped.
 | 
						|
		 * @param {CodePathSegment} prevSegment A previous segment to check.
 | 
						|
		 * @returns {boolean} `true` if the segment has been skipped.
 | 
						|
		 */
 | 
						|
		function isSkipped(prevSegment) {
 | 
						|
			return (
 | 
						|
				skipped.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.at(-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;
 | 
						|
				}
 | 
						|
 | 
						|
				visited.add(segment);
 | 
						|
 | 
						|
				// Skips the segment if all previous segments have been skipped.
 | 
						|
				const shouldSkip =
 | 
						|
					skipped.size > 0 &&
 | 
						|
					segment.prevSegments.length > 0 &&
 | 
						|
					segment.prevSegments.every(isSkipped);
 | 
						|
 | 
						|
				/*
 | 
						|
				 * If the most recent segment hasn't been skipped, then we call
 | 
						|
				 * the callback, passing in the segment and the controller.
 | 
						|
				 */
 | 
						|
				if (!shouldSkip) {
 | 
						|
					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;
 | 
						|
					}
 | 
						|
				} else {
 | 
						|
					// If the most recent segment has been skipped, then mark it as skipped.
 | 
						|
					skipped.add(segment);
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// 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;
 |