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 | /**
|
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 |
|
239 | module.exports = CodePath;
|