UNPKG

7.04 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 /**
25 * @param {string} id - An identifier.
26 * @param {CodePath|null} upper - The code path of the upper function scope.
27 * @param {Function} onLooped - A callback function to notify looping.
28 */
29 constructor(id, upper, onLooped) {
30
31 /**
32 * The identifier of this code path.
33 * Rules use it to store additional information of each rule.
34 * @type {string}
35 */
36 this.id = id;
37
38 /**
39 * The code path of the upper function scope.
40 * @type {CodePath|null}
41 */
42 this.upper = upper;
43
44 /**
45 * The code paths of nested function scopes.
46 * @type {CodePath[]}
47 */
48 this.childCodePaths = [];
49
50 // Initializes internal state.
51 Object.defineProperty(
52 this,
53 "internal",
54 { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) }
55 );
56
57 // Adds this into `childCodePaths` of `upper`.
58 if (upper) {
59 upper.childCodePaths.push(this);
60 }
61 }
62
63 /**
64 * Gets the state of a given code path.
65 *
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 *
130 * @param {Object} [options] - Omittable.
131 * @param {CodePathSegment} [options.first] - The first segment to traverse.
132 * @param {CodePathSegment} [options.last] - The last segment to traverse.
133 * @param {Function} callback - A callback function.
134 * @returns {void}
135 */
136 traverseSegments(options, callback) {
137 let resolvedOptions;
138 let resolvedCallback;
139
140 if (typeof options === "function") {
141 resolvedCallback = options;
142 resolvedOptions = {};
143 } else {
144 resolvedOptions = options || {};
145 resolvedCallback = callback;
146 }
147
148 const startSegment = resolvedOptions.first || this.internal.initialSegment;
149 const lastSegment = resolvedOptions.last;
150
151 let item = null;
152 let index = 0;
153 let end = 0;
154 let segment = null;
155 const visited = Object.create(null);
156 const stack = [[startSegment, 0]];
157 let skippedSegment = null;
158 let broken = false;
159 const controller = {
160 skip() {
161 if (stack.length <= 1) {
162 broken = true;
163 } else {
164 skippedSegment = stack[stack.length - 2][0];
165 }
166 },
167 break() {
168 broken = true;
169 }
170 };
171
172 /**
173 * Checks a given previous segment has been visited.
174 * @param {CodePathSegment} prevSegment - A previous segment to check.
175 * @returns {boolean} `true` if the segment has been visited.
176 */
177 function isVisited(prevSegment) {
178 return (
179 visited[prevSegment.id] ||
180 segment.isLoopedPrevSegment(prevSegment)
181 );
182 }
183
184 while (stack.length > 0) {
185 item = stack[stack.length - 1];
186 segment = item[0];
187 index = item[1];
188
189 if (index === 0) {
190
191 // Skip if this segment has been visited already.
192 if (visited[segment.id]) {
193 stack.pop();
194 continue;
195 }
196
197 // Skip if all previous segments have not been visited.
198 if (segment !== startSegment &&
199 segment.prevSegments.length > 0 &&
200 !segment.prevSegments.every(isVisited)
201 ) {
202 stack.pop();
203 continue;
204 }
205
206 // Reset the flag of skipping if all branches have been skipped.
207 if (skippedSegment && segment.prevSegments.indexOf(skippedSegment) !== -1) {
208 skippedSegment = null;
209 }
210 visited[segment.id] = true;
211
212 // Call the callback when the first time.
213 if (!skippedSegment) {
214 resolvedCallback.call(this, segment, controller);
215 if (segment === lastSegment) {
216 controller.skip();
217 }
218 if (broken) {
219 break;
220 }
221 }
222 }
223
224 // Update the stack.
225 end = segment.nextSegments.length - 1;
226 if (index < end) {
227 item[1] += 1;
228 stack.push([segment.nextSegments[index], 0]);
229 } else if (index === end) {
230 item[0] = segment.nextSegments[index];
231 item[1] = 0;
232 } else {
233 stack.pop();
234 }
235 }
236 }
237}
238
239module.exports = CodePath;