UNPKG

7.07 kBJavaScriptView Raw
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;