Skip to content

Commit 24e1f14

Browse files
authored
chore: Refactor and document CodePath (#17558)
* chore: Refactor and document CodePath * Clarify docs
1 parent 299bfae commit 24e1f14

File tree

1 file changed

+127
-33
lines changed

1 file changed

+127
-33
lines changed

lib/linter/code-path-analysis/code-path.js

Lines changed: 127 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -80,42 +80,58 @@ class CodePath {
8080
}
8181

8282
/**
83-
* The initial code path segment.
83+
* The initial code path segment. This is the segment that is at the head
84+
* of the code path.
85+
* This is a passthrough to the underlying `CodePathState`.
8486
* @type {CodePathSegment}
8587
*/
8688
get initialSegment() {
8789
return this.internal.initialSegment;
8890
}
8991

9092
/**
91-
* Final code path segments.
92-
* This array is a mix of `returnedSegments` and `thrownSegments`.
93+
* Final code path segments. These are the terminal (tail) segments in the
94+
* code path, which is the combination of `returnedSegments` and `thrownSegments`.
95+
* All segments in this array are reachable.
96+
* This is a passthrough to the underlying `CodePathState`.
9397
* @type {CodePathSegment[]}
9498
*/
9599
get finalSegments() {
96100
return this.internal.finalSegments;
97101
}
98102

99103
/**
100-
* Final code path segments which is with `return` statements.
101-
* This array contains the last path segment if it's reachable.
102-
* Since the reachable last path returns `undefined`.
104+
* Final code path segments that represent normal completion of the code path.
105+
* For functions, this means both explicit `return` statements and implicit returns,
106+
* such as the last reachable segment in a function that does not have an
107+
* explicit `return` as this implicitly returns `undefined`. For scripts,
108+
* modules, class field initializers, and class static blocks, this means
109+
* all lines of code have been executed.
110+
* These segments are also present in `finalSegments`.
111+
* This is a passthrough to the underlying `CodePathState`.
103112
* @type {CodePathSegment[]}
104113
*/
105114
get returnedSegments() {
106115
return this.internal.returnedForkContext;
107116
}
108117

109118
/**
110-
* Final code path segments which is with `throw` statements.
119+
* Final code path segments that represent `throw` statements.
120+
* This is a passthrough to the underlying `CodePathState`.
121+
* These segments are also present in `finalSegments`.
111122
* @type {CodePathSegment[]}
112123
*/
113124
get thrownSegments() {
114125
return this.internal.thrownForkContext;
115126
}
116127

117128
/**
118-
* Current code path segments.
129+
* Tracks the traversal of the code path through each segment. This array
130+
* starts empty and segments are added or removed as the code path is
131+
* traversed. This array always ends up empty at the end of a code path
132+
* traversal. The `CodePathState` uses this to track its progress through
133+
* the code path.
134+
* This is a passthrough to the underlying `CodePathState`.
119135
* @type {CodePathSegment[]}
120136
* @deprecated
121137
*/
@@ -126,79 +142,123 @@ class CodePath {
126142
/**
127143
* Traverses all segments in this code path.
128144
*
129-
* codePath.traverseSegments(function(segment, controller) {
145+
* codePath.traverseSegments((segment, controller) => {
130146
* // do something.
131147
* });
132148
*
133149
* This method enumerates segments in order from the head.
134150
*
135-
* The `controller` object has two methods.
151+
* The `controller` argument has two methods:
136152
*
137-
* - `controller.skip()` - Skip the following segments in this branch.
138-
* - `controller.break()` - Skip all following segments.
139-
* @param {Object} [options] Omittable.
140-
* @param {CodePathSegment} [options.first] The first segment to traverse.
141-
* @param {CodePathSegment} [options.last] The last segment to traverse.
153+
* - `skip()` - skips the following segments in this branch
154+
* - `break()` - skips all following segments in the traversal
155+
*
156+
* A note on the parameters: the `options` argument is optional. This means
157+
* the first argument might be an options object or the callback function.
158+
* @param {Object} [optionsOrCallback] Optional first and last segments to traverse.
159+
* @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse.
160+
* @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse.
142161
* @param {Function} callback A callback function.
143162
* @returns {void}
144163
*/
145-
traverseSegments(options, callback) {
164+
traverseSegments(optionsOrCallback, callback) {
165+
166+
// normalize the arguments into a callback and options
146167
let resolvedOptions;
147168
let resolvedCallback;
148169

149-
if (typeof options === "function") {
150-
resolvedCallback = options;
170+
if (typeof optionsOrCallback === "function") {
171+
resolvedCallback = optionsOrCallback;
151172
resolvedOptions = {};
152173
} else {
153-
resolvedOptions = options || {};
174+
resolvedOptions = optionsOrCallback || {};
154175
resolvedCallback = callback;
155176
}
156177

178+
// determine where to start traversing from based on the options
157179
const startSegment = resolvedOptions.first || this.internal.initialSegment;
158180
const lastSegment = resolvedOptions.last;
159181

160-
let item = null;
182+
// set up initial location information
183+
let record = null;
161184
let index = 0;
162185
let end = 0;
163186
let segment = null;
164-
const visited = Object.create(null);
187+
188+
// segments that have already been visited during traversal
189+
const visited = new Set();
190+
191+
// tracks the traversal steps
165192
const stack = [[startSegment, 0]];
193+
194+
// tracks the last skipped segment during traversal
166195
let skippedSegment = null;
196+
197+
// indicates if we exited early from the traversal
167198
let broken = false;
199+
200+
/**
201+
* Maintains traversal state.
202+
*/
168203
const controller = {
204+
205+
/**
206+
* Skip the following segments in this branch.
207+
* @returns {void}
208+
*/
169209
skip() {
170210
if (stack.length <= 1) {
171211
broken = true;
172212
} else {
173213
skippedSegment = stack[stack.length - 2][0];
174214
}
175215
},
216+
217+
/**
218+
* Stop traversal completely - do not traverse to any
219+
* other segments.
220+
* @returns {void}
221+
*/
176222
break() {
177223
broken = true;
178224
}
179225
};
180226

181227
/**
182-
* Checks a given previous segment has been visited.
228+
* Checks if a given previous segment has been visited.
183229
* @param {CodePathSegment} prevSegment A previous segment to check.
184230
* @returns {boolean} `true` if the segment has been visited.
185231
*/
186232
function isVisited(prevSegment) {
187233
return (
188-
visited[prevSegment.id] ||
234+
visited.has(prevSegment) ||
189235
segment.isLoopedPrevSegment(prevSegment)
190236
);
191237
}
192238

239+
// the traversal
193240
while (stack.length > 0) {
194-
item = stack[stack.length - 1];
195-
segment = item[0];
196-
index = item[1];
241+
242+
/*
243+
* This isn't a pure stack. We use the top record all the time
244+
* but don't always pop it off. The record is popped only if
245+
* one of the following is true:
246+
*
247+
* 1) We have already visited the segment.
248+
* 2) We have not visited *all* of the previous segments.
249+
* 3) We have traversed past the available next segments.
250+
*
251+
* Otherwise, we just read the value and sometimes modify the
252+
* record as we traverse.
253+
*/
254+
record = stack[stack.length - 1];
255+
segment = record[0];
256+
index = record[1];
197257

198258
if (index === 0) {
199259

200260
// Skip if this segment has been visited already.
201-
if (visited[segment.id]) {
261+
if (visited.has(segment)) {
202262
stack.pop();
203263
continue;
204264
}
@@ -212,18 +272,29 @@ class CodePath {
212272
continue;
213273
}
214274

215-
// Reset the flag of skipping if all branches have been skipped.
275+
// Reset the skipping flag if all branches have been skipped.
216276
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
217277
skippedSegment = null;
218278
}
219-
visited[segment.id] = true;
279+
visited.add(segment);
220280

221-
// Call the callback when the first time.
281+
/*
282+
* If the most recent segment hasn't been skipped, then we call
283+
* the callback, passing in the segment and the controller.
284+
*/
222285
if (!skippedSegment) {
223286
resolvedCallback.call(this, segment, controller);
287+
288+
// exit if we're at the last segment
224289
if (segment === lastSegment) {
225290
controller.skip();
226291
}
292+
293+
/*
294+
* If the previous statement was executed, or if the callback
295+
* called a method on the controller, we might need to exit the
296+
* loop, so check for that and break accordingly.
297+
*/
227298
if (broken) {
228299
break;
229300
}
@@ -233,12 +304,35 @@ class CodePath {
233304
// Update the stack.
234305
end = segment.nextSegments.length - 1;
235306
if (index < end) {
236-
item[1] += 1;
307+
308+
/*
309+
* If we haven't yet visited all of the next segments, update
310+
* the current top record on the stack to the next index to visit
311+
* and then push a record for the current segment on top.
312+
*
313+
* Setting the current top record's index lets us know how many
314+
* times we've been here and ensures that the segment won't be
315+
* reprocessed (because we only process segments with an index
316+
* of 0).
317+
*/
318+
record[1] += 1;
237319
stack.push([segment.nextSegments[index], 0]);
238320
} else if (index === end) {
239-
item[0] = segment.nextSegments[index];
240-
item[1] = 0;
321+
322+
/*
323+
* If we are at the last next segment, then reset the top record
324+
* in the stack to next segment and set its index to 0 so it will
325+
* be processed next.
326+
*/
327+
record[0] = segment.nextSegments[index];
328+
record[1] = 0;
241329
} else {
330+
331+
/*
332+
* If index > end, that means we have no more segments that need
333+
* processing. So, we pop that record off of the stack in order to
334+
* continue traversing at the next level up.
335+
*/
242336
stack.pop();
243337
}
244338
}

0 commit comments

Comments
 (0)