1/** 2 * @fileoverview A class of the code path. 3 * @author Toru Nagashima 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const CodePathState = require("./code-path-state"); 13const IdGenerator = require("./id-generator"); 14 15//------------------------------------------------------------------------------ 16// Public Interface 17//------------------------------------------------------------------------------ 18 19/** 20 * A code path. 21 */ 22class CodePath { 23 24 // eslint-disable-next-line jsdoc/require-description 25 /** 26 * @param {string} id An identifier. 27 * @param {CodePath|null} upper The code path of the upper function scope. 28 * @param {Function} onLooped A callback function to notify looping. 29 */ 30 constructor(id, upper, onLooped) { 31 32 /** 33 * The identifier of this code path. 34 * Rules use it to store additional information of each rule. 35 * @type {string} 36 */ 37 this.id = id; 38 39 /** 40 * The code path of the upper function scope. 41 * @type {CodePath|null} 42 */ 43 this.upper = upper; 44 45 /** 46 * The code paths of nested function scopes. 47 * @type {CodePath[]} 48 */ 49 this.childCodePaths = []; 50 51 // Initializes internal state. 52 Object.defineProperty( 53 this, 54 "internal", 55 { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) } 56 ); 57 58 // Adds this into `childCodePaths` of `upper`. 59 if (upper) { 60 upper.childCodePaths.push(this); 61 } 62 } 63 64 /** 65 * Gets the state of a given code path. 66 * @param {CodePath} codePath A code path to get. 67 * @returns {CodePathState} The state of the code path. 68 */ 69 static getState(codePath) { 70 return codePath.internal; 71 } 72 73 /** 74 * The initial code path segment. 75 * @type {CodePathSegment} 76 */ 77 get initialSegment() { 78 return this.internal.initialSegment; 79 } 80 81 /** 82 * Final code path segments. 83 * This array is a mix of `returnedSegments` and `thrownSegments`. 84 * @type {CodePathSegment[]} 85 */ 86 get finalSegments() { 87 return this.internal.finalSegments; 88 } 89 90 /** 91 * Final code path segments which is with `return` statements. 92 * This array contains the last path segment if it's reachable. 93 * Since the reachable last path returns `undefined`. 94 * @type {CodePathSegment[]} 95 */ 96 get returnedSegments() { 97 return this.internal.returnedForkContext; 98 } 99 100 /** 101 * Final code path segments which is with `throw` statements. 102 * @type {CodePathSegment[]} 103 */ 104 get thrownSegments() { 105 return this.internal.thrownForkContext; 106 } 107 108 /** 109 * Current code path segments. 110 * @type {CodePathSegment[]} 111 */ 112 get currentSegments() { 113 return this.internal.currentSegments; 114 } 115 116 /** 117 * Traverses all segments in this code path. 118 * 119 * codePath.traverseSegments(function(segment, controller) { 120 * // do something. 121 * }); 122 * 123 * This method enumerates segments in order from the head. 124 * 125 * The `controller` object has two methods. 126 * 127 * - `controller.skip()` - Skip the following segments in this branch. 128 * - `controller.break()` - Skip all following segments. 129 * @param {Object} [options] Omittable. 130 * @param {CodePathSegment} [options.first] The first segment to traverse. 131 * @param {CodePathSegment} [options.last] The last segment to traverse. 132 * @param {Function} callback A callback function. 133 * @returns {void} 134 */ 135 traverseSegments(options, callback) { 136 let resolvedOptions; 137 let resolvedCallback; 138 139 if (typeof options === "function") { 140 resolvedCallback = options; 141 resolvedOptions = {}; 142 } else { 143 resolvedOptions = options || {}; 144 resolvedCallback = callback; 145 } 146 147 const startSegment = resolvedOptions.first || this.internal.initialSegment; 148 const lastSegment = resolvedOptions.last; 149 150 let item = null; 151 let index = 0; 152 let end = 0; 153 let segment = null; 154 const visited = Object.create(null); 155 const stack = [[startSegment, 0]]; 156 let skippedSegment = null; 157 let broken = false; 158 const controller = { 159 skip() { 160 if (stack.length <= 1) { 161 broken = true; 162 } else { 163 skippedSegment = stack[stack.length - 2][0]; 164 } 165 }, 166 break() { 167 broken = true; 168 } 169 }; 170 171 /** 172 * Checks a given previous segment has been visited. 173 * @param {CodePathSegment} prevSegment A previous segment to check. 174 * @returns {boolean} `true` if the segment has been visited. 175 */ 176 function isVisited(prevSegment) { 177 return ( 178 visited[prevSegment.id] || 179 segment.isLoopedPrevSegment(prevSegment) 180 ); 181 } 182 183 while (stack.length > 0) { 184 item = stack[stack.length - 1]; 185 segment = item[0]; 186 index = item[1]; 187 188 if (index === 0) { 189 190 // Skip if this segment has been visited already. 191 if (visited[segment.id]) { 192 stack.pop(); 193 continue; 194 } 195 196 // Skip if all previous segments have not been visited. 197 if (segment !== startSegment && 198 segment.prevSegments.length > 0 && 199 !segment.prevSegments.every(isVisited) 200 ) { 201 stack.pop(); 202 continue; 203 } 204 205 // Reset the flag of skipping if all branches have been skipped. 206 if (skippedSegment && segment.prevSegments.indexOf(skippedSegment) !== -1) { 207 skippedSegment = null; 208 } 209 visited[segment.id] = true; 210 211 // Call the callback when the first time. 212 if (!skippedSegment) { 213 resolvedCallback.call(this, segment, controller); 214 if (segment === lastSegment) { 215 controller.skip(); 216 } 217 if (broken) { 218 break; 219 } 220 } 221 } 222 223 // Update the stack. 224 end = segment.nextSegments.length - 1; 225 if (index < end) { 226 item[1] += 1; 227 stack.push([segment.nextSegments[index], 0]); 228 } else if (index === end) { 229 item[0] = segment.nextSegments[index]; 230 item[1] = 0; 231 } else { 232 stack.pop(); 233 } 234 } 235 } 236} 237 238module.exports = CodePath; 239