1 | /**
|
2 | * @fileoverview A class of the code path.
|
3 | * @author Toru Nagashima
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const CodePathState = require("./code-path-state");
|
13 | const IdGenerator = require("./id-generator");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Public Interface
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | /**
|
20 | * A code path.
|
21 | */
|
22 | class 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 |
|
238 | module.exports = CodePath;
|