main
js 239 lines 6.35 KB
Raw
1 'use strict';
2
3 //------------------------------------------------------------------------------
4 // Requirements
5 //------------------------------------------------------------------------------
6
7 // eslint-disable-next-line
8 const CodePathState = require('./code-path-state');
9 // eslint-disable-next-line
10 const IdGenerator = require('./id-generator');
11
12 //------------------------------------------------------------------------------
13 // Public Interface
14 //------------------------------------------------------------------------------
15
16 /**
17 * A code path.
18 */
19 class CodePath {
20 /**
21 * Creates a new instance.
22 * @param {Object} options Options for the function (see below).
23 * @param {string} options.id An identifier.
24 * @param {string} options.origin The type of code path origin.
25 * @param {CodePath|null} options.upper The code path of the upper function scope.
26 * @param {Function} options.onLooped A callback function to notify looping.
27 */
28 constructor({id, origin, upper, onLooped}) {
29 /**
30 * The identifier of this code path.
31 * Rules use it to store additional information of each rule.
32 * @type {string}
33 */
34 this.id = id;
35
36 /**
37 * The reason that this code path was started. May be "program",
38 * "function", "class-field-initializer", or "class-static-block".
39 * @type {string}
40 */
41 this.origin = origin;
42
43 /**
44 * The code path of the upper function scope.
45 * @type {CodePath|null}
46 */
47 this.upper = upper;
48
49 /**
50 * The code paths of nested function scopes.
51 * @type {CodePath[]}
52 */
53 this.childCodePaths = [];
54
55 // Initializes internal state.
56 Object.defineProperty(this, 'internal', {
57 value: new CodePathState(new IdGenerator(`${id}_`), onLooped),
58 });
59
60 // Adds this into `childCodePaths` of `upper`.
61 if (upper) {
62 upper.childCodePaths.push(this);
63 }
64 }
65
66 /**
67 * Gets the state of a given code path.
68 * @param {CodePath} codePath A code path to get.
69 * @returns {CodePathState} The state of the code path.
70 */
71 static getState(codePath) {
72 return codePath.internal;
73 }
74
75 /**
76 * The initial code path segment.
77 * @type {CodePathSegment}
78 */
79 get initialSegment() {
80 return this.internal.initialSegment;
81 }
82
83 /**
84 * Final code path segments.
85 * This array is a mix of `returnedSegments` and `thrownSegments`.
86 * @type {CodePathSegment[]}
87 */
88 get finalSegments() {
89 return this.internal.finalSegments;
90 }
91
92 /**
93 * Final code path segments which is with `return` statements.
94 * This array contains the last path segment if it's reachable.
95 * Since the reachable last path returns `undefined`.
96 * @type {CodePathSegment[]}
97 */
98 get returnedSegments() {
99 return this.internal.returnedForkContext;
100 }
101
102 /**
103 * Final code path segments which is with `throw` statements.
104 * @type {CodePathSegment[]}
105 */
106 get thrownSegments() {
107 return this.internal.thrownForkContext;
108 }
109
110 /**
111 * Current code path segments.
112 * @type {CodePathSegment[]}
113 */
114 get currentSegments() {
115 return this.internal.currentSegments;
116 }
117
118 /**
119 * Traverses all segments in this code path.
120 *
121 * codePath.traverseSegments(function(segment, controller) {
122 * // do something.
123 * });
124 *
125 * This method enumerates segments in order from the head.
126 *
127 * The `controller` object has two methods.
128 *
129 * - `controller.skip()` - Skip the following segments in this branch.
130 * - `controller.break()` - Skip all following segments.
131 * @param {Object} [options] Omittable.
132 * @param {CodePathSegment} [options.first] The first segment to traverse.
133 * @param {CodePathSegment} [options.last] The last segment to traverse.
134 * @param {Function} callback A callback function.
135 * @returns {void}
136 */
137 traverseSegments(options, callback) {
138 let resolvedOptions;
139 let resolvedCallback;
140
141 if (typeof options === 'function') {
142 resolvedCallback = options;
143 resolvedOptions = {};
144 } else {
145 resolvedOptions = options || {};
146 resolvedCallback = callback;
147 }
148
149 const startSegment = resolvedOptions.first || this.internal.initialSegment;
150 const lastSegment = resolvedOptions.last;
151
152 let item = null;
153 let index = 0;
154 let end = 0;
155 let segment = null;
156 const visited = Object.create(null);
157 const stack = [[startSegment, 0]];
158 let skippedSegment = null;
159 let broken = false;
160 const controller = {
161 skip() {
162 if (stack.length <= 1) {
163 broken = true;
164 } else {
165 skippedSegment = stack[stack.length - 2][0];
166 }
167 },
168 break() {
169 broken = true;
170 },
171 };
172
173 /**
174 * Checks a given previous segment has been visited.
175 * @param {CodePathSegment} prevSegment A previous segment to check.
176 * @returns {boolean} `true` if the segment has been visited.
177 */
178 function isVisited(prevSegment) {
179 return (
180 visited[prevSegment.id] || 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 // 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 (
198 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.includes(skippedSegment)) {
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
239 module.exports = CodePath;