eslint 8.50.0 → 8.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/eslint.js +15 -4
- package/lib/cli.js +6 -2
- package/lib/config/flat-config-schema.js +1 -3
- package/lib/eslint/eslint-helpers.js +8 -3
- package/lib/eslint/flat-eslint.js +15 -6
- package/lib/linter/apply-disable-directives.js +1 -1
- package/lib/linter/code-path-analysis/code-path-state.js +1108 -243
- package/lib/linter/code-path-analysis/code-path.js +127 -33
- package/lib/linter/code-path-analysis/fork-context.js +173 -72
- package/lib/linter/config-comment-parser.js +1 -1
- package/lib/options.js +13 -0
- package/lib/rules/logical-assignment-operators.js +31 -3
- package/package.json +2 -2
@@ -12,13 +12,622 @@
|
|
12
12
|
const CodePathSegment = require("./code-path-segment"),
|
13
13
|
ForkContext = require("./fork-context");
|
14
14
|
|
15
|
+
//-----------------------------------------------------------------------------
|
16
|
+
// Contexts
|
17
|
+
//-----------------------------------------------------------------------------
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Represents the context in which a `break` statement can be used.
|
21
|
+
*
|
22
|
+
* A `break` statement without a label is only valid in a few places in
|
23
|
+
* JavaScript: any type of loop or a `switch` statement. Otherwise, `break`
|
24
|
+
* without a label causes a syntax error. For these contexts, `breakable` is
|
25
|
+
* set to `true` to indicate that a `break` without a label is valid.
|
26
|
+
*
|
27
|
+
* However, a `break` statement with a label is also valid inside of a labeled
|
28
|
+
* statement. For example, this is valid:
|
29
|
+
*
|
30
|
+
* a : {
|
31
|
+
* break a;
|
32
|
+
* }
|
33
|
+
*
|
34
|
+
* The `breakable` property is set false for labeled statements to indicate
|
35
|
+
* that `break` without a label is invalid.
|
36
|
+
*/
|
37
|
+
class BreakContext {
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Creates a new instance.
|
41
|
+
* @param {BreakContext} upperContext The previous `BreakContext`.
|
42
|
+
* @param {boolean} breakable Indicates if we are inside a statement where
|
43
|
+
* `break` without a label will exit the statement.
|
44
|
+
* @param {string|null} label The label for the statement.
|
45
|
+
* @param {ForkContext} forkContext The current fork context.
|
46
|
+
*/
|
47
|
+
constructor(upperContext, breakable, label, forkContext) {
|
48
|
+
|
49
|
+
/**
|
50
|
+
* The previous `BreakContext`
|
51
|
+
* @type {BreakContext}
|
52
|
+
*/
|
53
|
+
this.upper = upperContext;
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Indicates if we are inside a statement where `break` without a label
|
57
|
+
* will exit the statement.
|
58
|
+
* @type {boolean}
|
59
|
+
*/
|
60
|
+
this.breakable = breakable;
|
61
|
+
|
62
|
+
/**
|
63
|
+
* The label associated with the statement.
|
64
|
+
* @type {string|null}
|
65
|
+
*/
|
66
|
+
this.label = label;
|
67
|
+
|
68
|
+
/**
|
69
|
+
* The fork context for the `break`.
|
70
|
+
* @type {ForkContext}
|
71
|
+
*/
|
72
|
+
this.brokenForkContext = ForkContext.newEmpty(forkContext);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Represents the context for `ChainExpression` nodes.
|
78
|
+
*/
|
79
|
+
class ChainContext {
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Creates a new instance.
|
83
|
+
* @param {ChainContext} upperContext The previous `ChainContext`.
|
84
|
+
*/
|
85
|
+
constructor(upperContext) {
|
86
|
+
|
87
|
+
/**
|
88
|
+
* The previous `ChainContext`
|
89
|
+
* @type {ChainContext}
|
90
|
+
*/
|
91
|
+
this.upper = upperContext;
|
92
|
+
|
93
|
+
/**
|
94
|
+
* The number of choice contexts inside of the `ChainContext`.
|
95
|
+
* @type {number}
|
96
|
+
*/
|
97
|
+
this.choiceContextCount = 0;
|
98
|
+
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Represents a choice in the code path.
|
104
|
+
*
|
105
|
+
* Choices are created by logical operators such as `&&`, loops, conditionals,
|
106
|
+
* and `if` statements. This is the point at which the code path has a choice of
|
107
|
+
* which direction to go.
|
108
|
+
*
|
109
|
+
* The result of a choice might be in the left (test) expression of another choice,
|
110
|
+
* and in that case, may create a new fork. For example, `a || b` is a choice
|
111
|
+
* but does not create a new fork because the result of the expression is
|
112
|
+
* not used as the test expression in another expression. In this case,
|
113
|
+
* `isForkingAsResult` is false. In the expression `a || b || c`, the `a || b`
|
114
|
+
* expression appears as the test expression for `|| c`, so the
|
115
|
+
* result of `a || b` creates a fork because execution may or may not
|
116
|
+
* continue to `|| c`. `isForkingAsResult` for `a || b` in this case is true
|
117
|
+
* while `isForkingAsResult` for `|| c` is false. (`isForkingAsResult` is always
|
118
|
+
* false for `if` statements, conditional expressions, and loops.)
|
119
|
+
*
|
120
|
+
* All of the choices except one (`??`) operate on a true/false fork, meaning if
|
121
|
+
* true go one way and if false go the other (tracked by `trueForkContext` and
|
122
|
+
* `falseForkContext`). The `??` operator doesn't operate on true/false because
|
123
|
+
* the left expression is evaluated to be nullish or not, so only if nullish do
|
124
|
+
* we fork to the right expression (tracked by `nullishForkContext`).
|
125
|
+
*/
|
126
|
+
class ChoiceContext {
|
127
|
+
|
128
|
+
/**
|
129
|
+
* Creates a new instance.
|
130
|
+
* @param {ChoiceContext} upperContext The previous `ChoiceContext`.
|
131
|
+
* @param {string} kind The kind of choice. If it's a logical or assignment expression, this
|
132
|
+
* is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or
|
133
|
+
* conditional expression, this is `"test"`; otherwise, this is `"loop"`.
|
134
|
+
* @param {boolean} isForkingAsResult Indicates if the result of the choice
|
135
|
+
* creates a fork.
|
136
|
+
* @param {ForkContext} forkContext The containing `ForkContext`.
|
137
|
+
*/
|
138
|
+
constructor(upperContext, kind, isForkingAsResult, forkContext) {
|
139
|
+
|
140
|
+
/**
|
141
|
+
* The previous `ChoiceContext`
|
142
|
+
* @type {ChoiceContext}
|
143
|
+
*/
|
144
|
+
this.upper = upperContext;
|
145
|
+
|
146
|
+
/**
|
147
|
+
* The kind of choice. If it's a logical or assignment expression, this
|
148
|
+
* is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or
|
149
|
+
* conditional expression, this is `"test"`; otherwise, this is `"loop"`.
|
150
|
+
* @type {string}
|
151
|
+
*/
|
152
|
+
this.kind = kind;
|
153
|
+
|
154
|
+
/**
|
155
|
+
* Indicates if the result of the choice forks the code path.
|
156
|
+
* @type {boolean}
|
157
|
+
*/
|
158
|
+
this.isForkingAsResult = isForkingAsResult;
|
159
|
+
|
160
|
+
/**
|
161
|
+
* The fork context for the `true` path of the choice.
|
162
|
+
* @type {ForkContext}
|
163
|
+
*/
|
164
|
+
this.trueForkContext = ForkContext.newEmpty(forkContext);
|
165
|
+
|
166
|
+
/**
|
167
|
+
* The fork context for the `false` path of the choice.
|
168
|
+
* @type {ForkContext}
|
169
|
+
*/
|
170
|
+
this.falseForkContext = ForkContext.newEmpty(forkContext);
|
171
|
+
|
172
|
+
/**
|
173
|
+
* The fork context for when the choice result is `null` or `undefined`.
|
174
|
+
* @type {ForkContext}
|
175
|
+
*/
|
176
|
+
this.nullishForkContext = ForkContext.newEmpty(forkContext);
|
177
|
+
|
178
|
+
/**
|
179
|
+
* Indicates if any of `trueForkContext`, `falseForkContext`, or
|
180
|
+
* `nullishForkContext` have been updated with segments from a child context.
|
181
|
+
* @type {boolean}
|
182
|
+
*/
|
183
|
+
this.processed = false;
|
184
|
+
}
|
185
|
+
|
186
|
+
}
|
187
|
+
|
188
|
+
/**
|
189
|
+
* Base class for all loop contexts.
|
190
|
+
*/
|
191
|
+
class LoopContextBase {
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Creates a new instance.
|
195
|
+
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
196
|
+
* @param {string} type The AST node's `type` for the loop.
|
197
|
+
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
198
|
+
* @param {BreakContext} breakContext The context for breaking the loop.
|
199
|
+
*/
|
200
|
+
constructor(upperContext, type, label, breakContext) {
|
201
|
+
|
202
|
+
/**
|
203
|
+
* The previous `LoopContext`.
|
204
|
+
* @type {LoopContext}
|
205
|
+
*/
|
206
|
+
this.upper = upperContext;
|
207
|
+
|
208
|
+
/**
|
209
|
+
* The AST node's `type` for the loop.
|
210
|
+
* @type {string}
|
211
|
+
*/
|
212
|
+
this.type = type;
|
213
|
+
|
214
|
+
/**
|
215
|
+
* The label for the loop from an enclosing `LabeledStatement`.
|
216
|
+
* @type {string|null}
|
217
|
+
*/
|
218
|
+
this.label = label;
|
219
|
+
|
220
|
+
/**
|
221
|
+
* The fork context for when `break` is encountered.
|
222
|
+
* @type {ForkContext}
|
223
|
+
*/
|
224
|
+
this.brokenForkContext = breakContext.brokenForkContext;
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
228
|
+
/**
|
229
|
+
* Represents the context for a `while` loop.
|
230
|
+
*/
|
231
|
+
class WhileLoopContext extends LoopContextBase {
|
232
|
+
|
233
|
+
/**
|
234
|
+
* Creates a new instance.
|
235
|
+
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
236
|
+
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
237
|
+
* @param {BreakContext} breakContext The context for breaking the loop.
|
238
|
+
*/
|
239
|
+
constructor(upperContext, label, breakContext) {
|
240
|
+
super(upperContext, "WhileStatement", label, breakContext);
|
241
|
+
|
242
|
+
/**
|
243
|
+
* The hardcoded literal boolean test condition for
|
244
|
+
* the loop. Used to catch infinite or skipped loops.
|
245
|
+
* @type {boolean|undefined}
|
246
|
+
*/
|
247
|
+
this.test = void 0;
|
248
|
+
|
249
|
+
/**
|
250
|
+
* The segments representing the test condition where `continue` will
|
251
|
+
* jump to. The test condition will typically have just one segment but
|
252
|
+
* it's possible for there to be more than one.
|
253
|
+
* @type {Array<CodePathSegment>|null}
|
254
|
+
*/
|
255
|
+
this.continueDestSegments = null;
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
/**
|
260
|
+
* Represents the context for a `do-while` loop.
|
261
|
+
*/
|
262
|
+
class DoWhileLoopContext extends LoopContextBase {
|
263
|
+
|
264
|
+
/**
|
265
|
+
* Creates a new instance.
|
266
|
+
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
267
|
+
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
268
|
+
* @param {BreakContext} breakContext The context for breaking the loop.
|
269
|
+
* @param {ForkContext} forkContext The enclosing fork context.
|
270
|
+
*/
|
271
|
+
constructor(upperContext, label, breakContext, forkContext) {
|
272
|
+
super(upperContext, "DoWhileStatement", label, breakContext);
|
273
|
+
|
274
|
+
/**
|
275
|
+
* The hardcoded literal boolean test condition for
|
276
|
+
* the loop. Used to catch infinite or skipped loops.
|
277
|
+
* @type {boolean|undefined}
|
278
|
+
*/
|
279
|
+
this.test = void 0;
|
280
|
+
|
281
|
+
/**
|
282
|
+
* The segments at the start of the loop body. This is the only loop
|
283
|
+
* where the test comes at the end, so the first iteration always
|
284
|
+
* happens and we need a reference to the first statements.
|
285
|
+
* @type {Array<CodePathSegment>|null}
|
286
|
+
*/
|
287
|
+
this.entrySegments = null;
|
288
|
+
|
289
|
+
/**
|
290
|
+
* The fork context to follow when a `continue` is found.
|
291
|
+
* @type {ForkContext}
|
292
|
+
*/
|
293
|
+
this.continueForkContext = ForkContext.newEmpty(forkContext);
|
294
|
+
}
|
295
|
+
}
|
296
|
+
|
297
|
+
/**
|
298
|
+
* Represents the context for a `for` loop.
|
299
|
+
*/
|
300
|
+
class ForLoopContext extends LoopContextBase {
|
301
|
+
|
302
|
+
/**
|
303
|
+
* Creates a new instance.
|
304
|
+
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
305
|
+
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
306
|
+
* @param {BreakContext} breakContext The context for breaking the loop.
|
307
|
+
*/
|
308
|
+
constructor(upperContext, label, breakContext) {
|
309
|
+
super(upperContext, "ForStatement", label, breakContext);
|
310
|
+
|
311
|
+
/**
|
312
|
+
* The hardcoded literal boolean test condition for
|
313
|
+
* the loop. Used to catch infinite or skipped loops.
|
314
|
+
* @type {boolean|undefined}
|
315
|
+
*/
|
316
|
+
this.test = void 0;
|
317
|
+
|
318
|
+
/**
|
319
|
+
* The end of the init expression. This may change during the lifetime
|
320
|
+
* of the instance as we traverse the loop because some loops don't have
|
321
|
+
* an init expression.
|
322
|
+
* @type {Array<CodePathSegment>|null}
|
323
|
+
*/
|
324
|
+
this.endOfInitSegments = null;
|
325
|
+
|
326
|
+
/**
|
327
|
+
* The start of the test expression. This may change during the lifetime
|
328
|
+
* of the instance as we traverse the loop because some loops don't have
|
329
|
+
* a test expression.
|
330
|
+
* @type {Array<CodePathSegment>|null}
|
331
|
+
*/
|
332
|
+
this.testSegments = null;
|
333
|
+
|
334
|
+
/**
|
335
|
+
* The end of the test expression. This may change during the lifetime
|
336
|
+
* of the instance as we traverse the loop because some loops don't have
|
337
|
+
* a test expression.
|
338
|
+
* @type {Array<CodePathSegment>|null}
|
339
|
+
*/
|
340
|
+
this.endOfTestSegments = null;
|
341
|
+
|
342
|
+
/**
|
343
|
+
* The start of the update expression. This may change during the lifetime
|
344
|
+
* of the instance as we traverse the loop because some loops don't have
|
345
|
+
* an update expression.
|
346
|
+
* @type {Array<CodePathSegment>|null}
|
347
|
+
*/
|
348
|
+
this.updateSegments = null;
|
349
|
+
|
350
|
+
/**
|
351
|
+
* The end of the update expresion. This may change during the lifetime
|
352
|
+
* of the instance as we traverse the loop because some loops don't have
|
353
|
+
* an update expression.
|
354
|
+
* @type {Array<CodePathSegment>|null}
|
355
|
+
*/
|
356
|
+
this.endOfUpdateSegments = null;
|
357
|
+
|
358
|
+
/**
|
359
|
+
* The segments representing the test condition where `continue` will
|
360
|
+
* jump to. The test condition will typically have just one segment but
|
361
|
+
* it's possible for there to be more than one. This may change during the
|
362
|
+
* lifetime of the instance as we traverse the loop because some loops
|
363
|
+
* don't have an update expression. When there is an update expression, this
|
364
|
+
* will end up pointing to that expression; otherwise it will end up pointing
|
365
|
+
* to the test expression.
|
366
|
+
* @type {Array<CodePathSegment>|null}
|
367
|
+
*/
|
368
|
+
this.continueDestSegments = null;
|
369
|
+
}
|
370
|
+
}
|
371
|
+
|
372
|
+
/**
|
373
|
+
* Represents the context for a `for-in` loop.
|
374
|
+
*
|
375
|
+
* Terminology:
|
376
|
+
* - "left" means the part of the loop to the left of the `in` keyword. For
|
377
|
+
* example, in `for (var x in y)`, the left is `var x`.
|
378
|
+
* - "right" means the part of the loop to the right of the `in` keyword. For
|
379
|
+
* example, in `for (var x in y)`, the right is `y`.
|
380
|
+
*/
|
381
|
+
class ForInLoopContext extends LoopContextBase {
|
382
|
+
|
383
|
+
/**
|
384
|
+
* Creates a new instance.
|
385
|
+
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
386
|
+
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
387
|
+
* @param {BreakContext} breakContext The context for breaking the loop.
|
388
|
+
*/
|
389
|
+
constructor(upperContext, label, breakContext) {
|
390
|
+
super(upperContext, "ForInStatement", label, breakContext);
|
391
|
+
|
392
|
+
/**
|
393
|
+
* The segments that came immediately before the start of the loop.
|
394
|
+
* This allows you to traverse backwards out of the loop into the
|
395
|
+
* surrounding code. This is necessary to evaluate the right expression
|
396
|
+
* correctly, as it must be evaluated in the same way as the left
|
397
|
+
* expression, but the pointer to these segments would otherwise be
|
398
|
+
* lost if not stored on the instance. Once the right expression has
|
399
|
+
* been evaluated, this property is no longer used.
|
400
|
+
* @type {Array<CodePathSegment>|null}
|
401
|
+
*/
|
402
|
+
this.prevSegments = null;
|
403
|
+
|
404
|
+
/**
|
405
|
+
* Segments representing the start of everything to the left of the
|
406
|
+
* `in` keyword. This can be used to move forward towards
|
407
|
+
* `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are
|
408
|
+
* effectively the head and tail of a doubly-linked list.
|
409
|
+
* @type {Array<CodePathSegment>|null}
|
410
|
+
*/
|
411
|
+
this.leftSegments = null;
|
412
|
+
|
413
|
+
/**
|
414
|
+
* Segments representing the end of everything to the left of the
|
415
|
+
* `in` keyword. This can be used to move backward towards `leftSegments`.
|
416
|
+
* `leftSegments` and `endOfLeftSegments` are effectively the head
|
417
|
+
* and tail of a doubly-linked list.
|
418
|
+
* @type {Array<CodePathSegment>|null}
|
419
|
+
*/
|
420
|
+
this.endOfLeftSegments = null;
|
421
|
+
|
422
|
+
/**
|
423
|
+
* The segments representing the left expression where `continue` will
|
424
|
+
* jump to. In `for-in` loops, `continue` must always re-execute the
|
425
|
+
* left expression each time through the loop. This contains the same
|
426
|
+
* segments as `leftSegments`, but is duplicated here so each loop
|
427
|
+
* context has the same property pointing to where `continue` should
|
428
|
+
* end up.
|
429
|
+
* @type {Array<CodePathSegment>|null}
|
430
|
+
*/
|
431
|
+
this.continueDestSegments = null;
|
432
|
+
}
|
433
|
+
}
|
434
|
+
|
435
|
+
/**
|
436
|
+
* Represents the context for a `for-of` loop.
|
437
|
+
*/
|
438
|
+
class ForOfLoopContext extends LoopContextBase {
|
439
|
+
|
440
|
+
/**
|
441
|
+
* Creates a new instance.
|
442
|
+
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
443
|
+
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
444
|
+
* @param {BreakContext} breakContext The context for breaking the loop.
|
445
|
+
*/
|
446
|
+
constructor(upperContext, label, breakContext) {
|
447
|
+
super(upperContext, "ForOfStatement", label, breakContext);
|
448
|
+
|
449
|
+
/**
|
450
|
+
* The segments that came immediately before the start of the loop.
|
451
|
+
* This allows you to traverse backwards out of the loop into the
|
452
|
+
* surrounding code. This is necessary to evaluate the right expression
|
453
|
+
* correctly, as it must be evaluated in the same way as the left
|
454
|
+
* expression, but the pointer to these segments would otherwise be
|
455
|
+
* lost if not stored on the instance. Once the right expression has
|
456
|
+
* been evaluated, this property is no longer used.
|
457
|
+
* @type {Array<CodePathSegment>|null}
|
458
|
+
*/
|
459
|
+
this.prevSegments = null;
|
460
|
+
|
461
|
+
/**
|
462
|
+
* Segments representing the start of everything to the left of the
|
463
|
+
* `of` keyword. This can be used to move forward towards
|
464
|
+
* `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are
|
465
|
+
* effectively the head and tail of a doubly-linked list.
|
466
|
+
* @type {Array<CodePathSegment>|null}
|
467
|
+
*/
|
468
|
+
this.leftSegments = null;
|
469
|
+
|
470
|
+
/**
|
471
|
+
* Segments representing the end of everything to the left of the
|
472
|
+
* `of` keyword. This can be used to move backward towards `leftSegments`.
|
473
|
+
* `leftSegments` and `endOfLeftSegments` are effectively the head
|
474
|
+
* and tail of a doubly-linked list.
|
475
|
+
* @type {Array<CodePathSegment>|null}
|
476
|
+
*/
|
477
|
+
this.endOfLeftSegments = null;
|
478
|
+
|
479
|
+
/**
|
480
|
+
* The segments representing the left expression where `continue` will
|
481
|
+
* jump to. In `for-in` loops, `continue` must always re-execute the
|
482
|
+
* left expression each time through the loop. This contains the same
|
483
|
+
* segments as `leftSegments`, but is duplicated here so each loop
|
484
|
+
* context has the same property pointing to where `continue` should
|
485
|
+
* end up.
|
486
|
+
* @type {Array<CodePathSegment>|null}
|
487
|
+
*/
|
488
|
+
this.continueDestSegments = null;
|
489
|
+
}
|
490
|
+
}
|
491
|
+
|
492
|
+
/**
|
493
|
+
* Represents the context for any loop.
|
494
|
+
* @typedef {WhileLoopContext|DoWhileLoopContext|ForLoopContext|ForInLoopContext|ForOfLoopContext} LoopContext
|
495
|
+
*/
|
496
|
+
|
497
|
+
/**
|
498
|
+
* Represents the context for a `switch` statement.
|
499
|
+
*/
|
500
|
+
class SwitchContext {
|
501
|
+
|
502
|
+
/**
|
503
|
+
* Creates a new instance.
|
504
|
+
* @param {SwitchContext} upperContext The previous context.
|
505
|
+
* @param {boolean} hasCase Indicates if there is at least one `case` statement.
|
506
|
+
* `default` doesn't count.
|
507
|
+
*/
|
508
|
+
constructor(upperContext, hasCase) {
|
509
|
+
|
510
|
+
/**
|
511
|
+
* The previous context.
|
512
|
+
* @type {SwitchContext}
|
513
|
+
*/
|
514
|
+
this.upper = upperContext;
|
515
|
+
|
516
|
+
/**
|
517
|
+
* Indicates if there is at least one `case` statement. `default` doesn't count.
|
518
|
+
* @type {boolean}
|
519
|
+
*/
|
520
|
+
this.hasCase = hasCase;
|
521
|
+
|
522
|
+
/**
|
523
|
+
* The `default` keyword.
|
524
|
+
* @type {Array<CodePathSegment>|null}
|
525
|
+
*/
|
526
|
+
this.defaultSegments = null;
|
527
|
+
|
528
|
+
/**
|
529
|
+
* The default case body starting segments.
|
530
|
+
* @type {Array<CodePathSegment>|null}
|
531
|
+
*/
|
532
|
+
this.defaultBodySegments = null;
|
533
|
+
|
534
|
+
/**
|
535
|
+
* Indicates if a `default` case and is empty exists.
|
536
|
+
* @type {boolean}
|
537
|
+
*/
|
538
|
+
this.foundEmptyDefault = false;
|
539
|
+
|
540
|
+
/**
|
541
|
+
* Indicates that a `default` exists and is the last case.
|
542
|
+
* @type {boolean}
|
543
|
+
*/
|
544
|
+
this.lastIsDefault = false;
|
545
|
+
|
546
|
+
/**
|
547
|
+
* The number of fork contexts created. This is equivalent to the
|
548
|
+
* number of `case` statements plus a `default` statement (if present).
|
549
|
+
* @type {number}
|
550
|
+
*/
|
551
|
+
this.forkCount = 0;
|
552
|
+
}
|
553
|
+
}
|
554
|
+
|
555
|
+
/**
|
556
|
+
* Represents the context for a `try` statement.
|
557
|
+
*/
|
558
|
+
class TryContext {
|
559
|
+
|
560
|
+
/**
|
561
|
+
* Creates a new instance.
|
562
|
+
* @param {TryContext} upperContext The previous context.
|
563
|
+
* @param {boolean} hasFinalizer Indicates if the `try` statement has a
|
564
|
+
* `finally` block.
|
565
|
+
* @param {ForkContext} forkContext The enclosing fork context.
|
566
|
+
*/
|
567
|
+
constructor(upperContext, hasFinalizer, forkContext) {
|
568
|
+
|
569
|
+
/**
|
570
|
+
* The previous context.
|
571
|
+
* @type {TryContext}
|
572
|
+
*/
|
573
|
+
this.upper = upperContext;
|
574
|
+
|
575
|
+
/**
|
576
|
+
* Indicates if the `try` statement has a `finally` block.
|
577
|
+
* @type {boolean}
|
578
|
+
*/
|
579
|
+
this.hasFinalizer = hasFinalizer;
|
580
|
+
|
581
|
+
/**
|
582
|
+
* Tracks the traversal position inside of the `try` statement. This is
|
583
|
+
* used to help determine the context necessary to create paths because
|
584
|
+
* a `try` statement may or may not have `catch` or `finally` blocks,
|
585
|
+
* and code paths behave differently in those blocks.
|
586
|
+
* @type {"try"|"catch"|"finally"}
|
587
|
+
*/
|
588
|
+
this.position = "try";
|
589
|
+
|
590
|
+
/**
|
591
|
+
* If the `try` statement has a `finally` block, this affects how a
|
592
|
+
* `return` statement behaves in the `try` block. Without `finally`,
|
593
|
+
* `return` behaves as usual and doesn't require a fork; with `finally`,
|
594
|
+
* `return` forks into the `finally` block, so we need a fork context
|
595
|
+
* to track it.
|
596
|
+
* @type {ForkContext|null}
|
597
|
+
*/
|
598
|
+
this.returnedForkContext = hasFinalizer
|
599
|
+
? ForkContext.newEmpty(forkContext)
|
600
|
+
: null;
|
601
|
+
|
602
|
+
/**
|
603
|
+
* When a `throw` occurs inside of a `try` block, the code path forks
|
604
|
+
* into the `catch` or `finally` blocks, and this fork context tracks
|
605
|
+
* that path.
|
606
|
+
* @type {ForkContext}
|
607
|
+
*/
|
608
|
+
this.thrownForkContext = ForkContext.newEmpty(forkContext);
|
609
|
+
|
610
|
+
/**
|
611
|
+
* Indicates if the last segment in the `try` block is reachable.
|
612
|
+
* @type {boolean}
|
613
|
+
*/
|
614
|
+
this.lastOfTryIsReachable = false;
|
615
|
+
|
616
|
+
/**
|
617
|
+
* Indicates if the last segment in the `catch` block is reachable.
|
618
|
+
* @type {boolean}
|
619
|
+
*/
|
620
|
+
this.lastOfCatchIsReachable = false;
|
621
|
+
}
|
622
|
+
}
|
623
|
+
|
15
624
|
//------------------------------------------------------------------------------
|
16
625
|
// Helpers
|
17
626
|
//------------------------------------------------------------------------------
|
18
627
|
|
19
628
|
/**
|
20
629
|
* Adds given segments into the `dest` array.
|
21
|
-
* If the `others` array does not
|
630
|
+
* If the `others` array does not include the given segments, adds to the `all`
|
22
631
|
* array as well.
|
23
632
|
*
|
24
633
|
* This adds only reachable and used segments.
|
@@ -40,9 +649,9 @@ function addToReturnedOrThrown(dest, others, all, segments) {
|
|
40
649
|
}
|
41
650
|
|
42
651
|
/**
|
43
|
-
* Gets a loop
|
44
|
-
* @param {CodePathState} state
|
45
|
-
* @param {string} label The label of a `continue` statement.
|
652
|
+
* Gets a loop context for a `continue` statement based on a given label.
|
653
|
+
* @param {CodePathState} state The state to search within.
|
654
|
+
* @param {string|null} label The label of a `continue` statement.
|
46
655
|
* @returns {LoopContext} A loop-context for a `continue` statement.
|
47
656
|
*/
|
48
657
|
function getContinueContext(state, label) {
|
@@ -65,9 +674,9 @@ function getContinueContext(state, label) {
|
|
65
674
|
|
66
675
|
/**
|
67
676
|
* Gets a context for a `break` statement.
|
68
|
-
* @param {CodePathState} state
|
69
|
-
* @param {string} label The label of a `break` statement.
|
70
|
-
* @returns {
|
677
|
+
* @param {CodePathState} state The state to search within.
|
678
|
+
* @param {string|null} label The label of a `break` statement.
|
679
|
+
* @returns {BreakContext} A context for a `break` statement.
|
71
680
|
*/
|
72
681
|
function getBreakContext(state, label) {
|
73
682
|
let context = state.breakContext;
|
@@ -84,8 +693,10 @@ function getBreakContext(state, label) {
|
|
84
693
|
}
|
85
694
|
|
86
695
|
/**
|
87
|
-
* Gets a context for a `return` statement.
|
88
|
-
*
|
696
|
+
* Gets a context for a `return` statement. There is just one special case:
|
697
|
+
* if there is a `try` statement with a `finally` block, because that alters
|
698
|
+
* how `return` behaves; otherwise, this just passes through the given state.
|
699
|
+
* @param {CodePathState} state The state to search within
|
89
700
|
* @returns {TryContext|CodePathState} A context for a `return` statement.
|
90
701
|
*/
|
91
702
|
function getReturnContext(state) {
|
@@ -102,8 +713,11 @@ function getReturnContext(state) {
|
|
102
713
|
}
|
103
714
|
|
104
715
|
/**
|
105
|
-
* Gets a context for a `throw` statement.
|
106
|
-
*
|
716
|
+
* Gets a context for a `throw` statement. There is just one special case:
|
717
|
+
* if there is a `try` statement with a `finally` block and we are inside of
|
718
|
+
* a `catch` because that changes how `throw` behaves; otherwise, this just
|
719
|
+
* passes through the given state.
|
720
|
+
* @param {CodePathState} state The state to search within.
|
107
721
|
* @returns {TryContext|CodePathState} A context for a `throw` statement.
|
108
722
|
*/
|
109
723
|
function getThrowContext(state) {
|
@@ -122,13 +736,13 @@ function getThrowContext(state) {
|
|
122
736
|
}
|
123
737
|
|
124
738
|
/**
|
125
|
-
* Removes a given
|
126
|
-
* @param {any[]}
|
127
|
-
* @param {any}
|
739
|
+
* Removes a given value from a given array.
|
740
|
+
* @param {any[]} elements An array to remove the specific element.
|
741
|
+
* @param {any} value The value to be removed.
|
128
742
|
* @returns {void}
|
129
743
|
*/
|
130
|
-
function
|
131
|
-
|
744
|
+
function removeFromArray(elements, value) {
|
745
|
+
elements.splice(elements.indexOf(value), 1);
|
132
746
|
}
|
133
747
|
|
134
748
|
/**
|
@@ -141,48 +755,77 @@ function remove(xs, x) {
|
|
141
755
|
* @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
|
142
756
|
* @returns {void}
|
143
757
|
*/
|
144
|
-
function
|
758
|
+
function disconnectSegments(prevSegments, nextSegments) {
|
145
759
|
for (let i = 0; i < prevSegments.length; ++i) {
|
146
760
|
const prevSegment = prevSegments[i];
|
147
761
|
const nextSegment = nextSegments[i];
|
148
762
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
763
|
+
removeFromArray(prevSegment.nextSegments, nextSegment);
|
764
|
+
removeFromArray(prevSegment.allNextSegments, nextSegment);
|
765
|
+
removeFromArray(nextSegment.prevSegments, prevSegment);
|
766
|
+
removeFromArray(nextSegment.allPrevSegments, prevSegment);
|
153
767
|
}
|
154
768
|
}
|
155
769
|
|
156
770
|
/**
|
157
|
-
* Creates looping path
|
158
|
-
*
|
771
|
+
* Creates looping path between two arrays of segments, ensuring that there are
|
772
|
+
* paths going between matching segments in the arrays.
|
773
|
+
* @param {CodePathState} state The state to operate on.
|
159
774
|
* @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
|
160
775
|
* @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
|
161
776
|
* @returns {void}
|
162
777
|
*/
|
163
778
|
function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
|
779
|
+
|
164
780
|
const fromSegments = CodePathSegment.flattenUnusedSegments(unflattenedFromSegments);
|
165
781
|
const toSegments = CodePathSegment.flattenUnusedSegments(unflattenedToSegments);
|
166
|
-
|
167
782
|
const end = Math.min(fromSegments.length, toSegments.length);
|
168
783
|
|
784
|
+
/*
|
785
|
+
* This loop effectively updates a doubly-linked list between two collections
|
786
|
+
* of segments making sure that segments in the same array indices are
|
787
|
+
* combined to create a path.
|
788
|
+
*/
|
169
789
|
for (let i = 0; i < end; ++i) {
|
790
|
+
|
791
|
+
// get the segments in matching array indices
|
170
792
|
const fromSegment = fromSegments[i];
|
171
793
|
const toSegment = toSegments[i];
|
172
794
|
|
795
|
+
/*
|
796
|
+
* If the destination segment is reachable, then create a path from the
|
797
|
+
* source segment to the destination segment.
|
798
|
+
*/
|
173
799
|
if (toSegment.reachable) {
|
174
800
|
fromSegment.nextSegments.push(toSegment);
|
175
801
|
}
|
802
|
+
|
803
|
+
/*
|
804
|
+
* If the source segment is reachable, then create a path from the
|
805
|
+
* destination segment back to the source segment.
|
806
|
+
*/
|
176
807
|
if (fromSegment.reachable) {
|
177
808
|
toSegment.prevSegments.push(fromSegment);
|
178
809
|
}
|
810
|
+
|
811
|
+
/*
|
812
|
+
* Also update the arrays that don't care if the segments are reachable
|
813
|
+
* or not. This should always happen regardless of anything else.
|
814
|
+
*/
|
179
815
|
fromSegment.allNextSegments.push(toSegment);
|
180
816
|
toSegment.allPrevSegments.push(fromSegment);
|
181
817
|
|
818
|
+
/*
|
819
|
+
* If the destination segment has at least two previous segments in its
|
820
|
+
* path then that means there was one previous segment before this iteration
|
821
|
+
* of the loop was executed. So, we need to mark the source segment as
|
822
|
+
* looped.
|
823
|
+
*/
|
182
824
|
if (toSegment.allPrevSegments.length >= 2) {
|
183
825
|
CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
|
184
826
|
}
|
185
827
|
|
828
|
+
// let the code path analyzer know that there's been a loop created
|
186
829
|
state.notifyLooped(fromSegment, toSegment);
|
187
830
|
}
|
188
831
|
}
|
@@ -198,15 +841,27 @@ function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
|
|
198
841
|
* @returns {void}
|
199
842
|
*/
|
200
843
|
function finalizeTestSegmentsOfFor(context, choiceContext, head) {
|
844
|
+
|
845
|
+
/*
|
846
|
+
* If this choice context doesn't already contain paths from a
|
847
|
+
* child context, then add the current head to each potential path.
|
848
|
+
*/
|
201
849
|
if (!choiceContext.processed) {
|
202
850
|
choiceContext.trueForkContext.add(head);
|
203
851
|
choiceContext.falseForkContext.add(head);
|
204
|
-
choiceContext.
|
852
|
+
choiceContext.nullishForkContext.add(head);
|
205
853
|
}
|
206
854
|
|
855
|
+
/*
|
856
|
+
* If the test condition isn't a hardcoded truthy value, then `break`
|
857
|
+
* must follow the same path as if the test condition is false. To represent
|
858
|
+
* that, we append the path for when the loop test is false (represented by
|
859
|
+
* `falseForkContext`) to the `brokenForkContext`.
|
860
|
+
*/
|
207
861
|
if (context.test !== true) {
|
208
862
|
context.brokenForkContext.addAll(choiceContext.falseForkContext);
|
209
863
|
}
|
864
|
+
|
210
865
|
context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
|
211
866
|
}
|
212
867
|
|
@@ -220,35 +875,124 @@ function finalizeTestSegmentsOfFor(context, choiceContext, head) {
|
|
220
875
|
class CodePathState {
|
221
876
|
|
222
877
|
/**
|
878
|
+
* Creates a new instance.
|
223
879
|
* @param {IdGenerator} idGenerator An id generator to generate id for code
|
224
880
|
* path segments.
|
225
881
|
* @param {Function} onLooped A callback function to notify looping.
|
226
882
|
*/
|
227
883
|
constructor(idGenerator, onLooped) {
|
884
|
+
|
885
|
+
/**
|
886
|
+
* The ID generator to use when creating new segments.
|
887
|
+
* @type {IdGenerator}
|
888
|
+
*/
|
228
889
|
this.idGenerator = idGenerator;
|
890
|
+
|
891
|
+
/**
|
892
|
+
* A callback function to call when there is a loop.
|
893
|
+
* @type {Function}
|
894
|
+
*/
|
229
895
|
this.notifyLooped = onLooped;
|
896
|
+
|
897
|
+
/**
|
898
|
+
* The root fork context for this state.
|
899
|
+
* @type {ForkContext}
|
900
|
+
*/
|
230
901
|
this.forkContext = ForkContext.newRoot(idGenerator);
|
902
|
+
|
903
|
+
/**
|
904
|
+
* Context for logical expressions, conditional expressions, `if` statements,
|
905
|
+
* and loops.
|
906
|
+
* @type {ChoiceContext}
|
907
|
+
*/
|
231
908
|
this.choiceContext = null;
|
909
|
+
|
910
|
+
/**
|
911
|
+
* Context for `switch` statements.
|
912
|
+
* @type {SwitchContext}
|
913
|
+
*/
|
232
914
|
this.switchContext = null;
|
915
|
+
|
916
|
+
/**
|
917
|
+
* Context for `try` statements.
|
918
|
+
* @type {TryContext}
|
919
|
+
*/
|
233
920
|
this.tryContext = null;
|
921
|
+
|
922
|
+
/**
|
923
|
+
* Context for loop statements.
|
924
|
+
* @type {LoopContext}
|
925
|
+
*/
|
234
926
|
this.loopContext = null;
|
927
|
+
|
928
|
+
/**
|
929
|
+
* Context for `break` statements.
|
930
|
+
* @type {BreakContext}
|
931
|
+
*/
|
235
932
|
this.breakContext = null;
|
933
|
+
|
934
|
+
/**
|
935
|
+
* Context for `ChainExpression` nodes.
|
936
|
+
* @type {ChainContext}
|
937
|
+
*/
|
236
938
|
this.chainContext = null;
|
237
939
|
|
940
|
+
/**
|
941
|
+
* An array that tracks the current segments in the state. The array
|
942
|
+
* starts empty and segments are added with each `onCodePathSegmentStart`
|
943
|
+
* event and removed with each `onCodePathSegmentEnd` event. Effectively,
|
944
|
+
* this is tracking the code path segment traversal as the state is
|
945
|
+
* modified.
|
946
|
+
* @type {Array<CodePathSegment>}
|
947
|
+
*/
|
238
948
|
this.currentSegments = [];
|
949
|
+
|
950
|
+
/**
|
951
|
+
* Tracks the starting segment for this path. This value never changes.
|
952
|
+
* @type {CodePathSegment}
|
953
|
+
*/
|
239
954
|
this.initialSegment = this.forkContext.head[0];
|
240
955
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
956
|
+
/**
|
957
|
+
* The final segments of the code path which are either `return` or `throw`.
|
958
|
+
* This is a union of the segments in `returnedForkContext` and `thrownForkContext`.
|
959
|
+
* @type {Array<CodePathSegment>}
|
960
|
+
*/
|
961
|
+
this.finalSegments = [];
|
962
|
+
|
963
|
+
/**
|
964
|
+
* The final segments of the code path which are `return`. These
|
965
|
+
* segments are also contained in `finalSegments`.
|
966
|
+
* @type {Array<CodePathSegment>}
|
967
|
+
*/
|
968
|
+
this.returnedForkContext = [];
|
969
|
+
|
970
|
+
/**
|
971
|
+
* The final segments of the code path which are `throw`. These
|
972
|
+
* segments are also contained in `finalSegments`.
|
973
|
+
* @type {Array<CodePathSegment>}
|
974
|
+
*/
|
975
|
+
this.thrownForkContext = [];
|
976
|
+
|
977
|
+
/*
|
978
|
+
* We add an `add` method so that these look more like fork contexts and
|
979
|
+
* can be used interchangeably when a fork context is needed to add more
|
980
|
+
* segments to a path.
|
981
|
+
*
|
982
|
+
* Ultimately, we want anything added to `returned` or `thrown` to also
|
983
|
+
* be added to `final`. We only add reachable and used segments to these
|
984
|
+
* arrays.
|
985
|
+
*/
|
986
|
+
const final = this.finalSegments;
|
987
|
+
const returned = this.returnedForkContext;
|
988
|
+
const thrown = this.thrownForkContext;
|
245
989
|
|
246
990
|
returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
|
247
991
|
thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
|
248
992
|
}
|
249
993
|
|
250
994
|
/**
|
251
|
-
*
|
995
|
+
* A passthrough property exposing the current pointer as part of the API.
|
252
996
|
* @type {CodePathSegment[]}
|
253
997
|
*/
|
254
998
|
get headSegments() {
|
@@ -341,77 +1085,72 @@ class CodePathState {
|
|
341
1085
|
* If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
|
342
1086
|
* If it's IfStatement's or ConditionalExpression's, this is `"test"`.
|
343
1087
|
* Otherwise, this is `"loop"`.
|
344
|
-
* @param {boolean} isForkingAsResult
|
345
|
-
*
|
1088
|
+
* @param {boolean} isForkingAsResult Indicates if the result of the choice
|
1089
|
+
* creates a fork.
|
346
1090
|
* @returns {void}
|
347
1091
|
*/
|
348
1092
|
pushChoiceContext(kind, isForkingAsResult) {
|
349
|
-
this.choiceContext =
|
350
|
-
upper: this.choiceContext,
|
351
|
-
kind,
|
352
|
-
isForkingAsResult,
|
353
|
-
trueForkContext: ForkContext.newEmpty(this.forkContext),
|
354
|
-
falseForkContext: ForkContext.newEmpty(this.forkContext),
|
355
|
-
qqForkContext: ForkContext.newEmpty(this.forkContext),
|
356
|
-
processed: false
|
357
|
-
};
|
1093
|
+
this.choiceContext = new ChoiceContext(this.choiceContext, kind, isForkingAsResult, this.forkContext);
|
358
1094
|
}
|
359
1095
|
|
360
1096
|
/**
|
361
1097
|
* Pops the last choice context and finalizes it.
|
1098
|
+
* This is called upon leaving a node that represents a choice.
|
362
1099
|
* @throws {Error} (Unreachable.)
|
363
1100
|
* @returns {ChoiceContext} The popped context.
|
364
1101
|
*/
|
365
1102
|
popChoiceContext() {
|
366
|
-
const
|
367
|
-
|
368
|
-
this.choiceContext = context.upper;
|
369
|
-
|
1103
|
+
const poppedChoiceContext = this.choiceContext;
|
370
1104
|
const forkContext = this.forkContext;
|
371
|
-
const
|
1105
|
+
const head = forkContext.head;
|
1106
|
+
|
1107
|
+
this.choiceContext = poppedChoiceContext.upper;
|
372
1108
|
|
373
|
-
switch (
|
1109
|
+
switch (poppedChoiceContext.kind) {
|
374
1110
|
case "&&":
|
375
1111
|
case "||":
|
376
1112
|
case "??":
|
377
1113
|
|
378
1114
|
/*
|
379
|
-
*
|
380
|
-
*
|
381
|
-
*
|
1115
|
+
* The `head` are the path of the right-hand operand.
|
1116
|
+
* If we haven't previously added segments from child contexts,
|
1117
|
+
* then we add these segments to all possible forks.
|
382
1118
|
*/
|
383
|
-
if (!
|
384
|
-
|
385
|
-
|
386
|
-
|
1119
|
+
if (!poppedChoiceContext.processed) {
|
1120
|
+
poppedChoiceContext.trueForkContext.add(head);
|
1121
|
+
poppedChoiceContext.falseForkContext.add(head);
|
1122
|
+
poppedChoiceContext.nullishForkContext.add(head);
|
387
1123
|
}
|
388
1124
|
|
389
1125
|
/*
|
390
|
-
*
|
391
|
-
*
|
1126
|
+
* If this context is the left (test) expression for another choice
|
1127
|
+
* context, such as `a || b` in the expression `a || b || c`,
|
1128
|
+
* then we take the segments for this context and move them up
|
1129
|
+
* to the parent context.
|
392
1130
|
*/
|
393
|
-
if (
|
1131
|
+
if (poppedChoiceContext.isForkingAsResult) {
|
394
1132
|
const parentContext = this.choiceContext;
|
395
1133
|
|
396
|
-
parentContext.trueForkContext.addAll(
|
397
|
-
parentContext.falseForkContext.addAll(
|
398
|
-
parentContext.
|
1134
|
+
parentContext.trueForkContext.addAll(poppedChoiceContext.trueForkContext);
|
1135
|
+
parentContext.falseForkContext.addAll(poppedChoiceContext.falseForkContext);
|
1136
|
+
parentContext.nullishForkContext.addAll(poppedChoiceContext.nullishForkContext);
|
399
1137
|
parentContext.processed = true;
|
400
1138
|
|
401
|
-
|
1139
|
+
// Exit early so we don't collapse all paths into one.
|
1140
|
+
return poppedChoiceContext;
|
402
1141
|
}
|
403
1142
|
|
404
1143
|
break;
|
405
1144
|
|
406
1145
|
case "test":
|
407
|
-
if (!
|
1146
|
+
if (!poppedChoiceContext.processed) {
|
408
1147
|
|
409
1148
|
/*
|
410
1149
|
* The head segments are the path of the `if` block here.
|
411
1150
|
* Updates the `true` path with the end of the `if` block.
|
412
1151
|
*/
|
413
|
-
|
414
|
-
|
1152
|
+
poppedChoiceContext.trueForkContext.clear();
|
1153
|
+
poppedChoiceContext.trueForkContext.add(head);
|
415
1154
|
} else {
|
416
1155
|
|
417
1156
|
/*
|
@@ -419,8 +1158,8 @@ class CodePathState {
|
|
419
1158
|
* Updates the `false` path with the end of the `else`
|
420
1159
|
* block.
|
421
1160
|
*/
|
422
|
-
|
423
|
-
|
1161
|
+
poppedChoiceContext.falseForkContext.clear();
|
1162
|
+
poppedChoiceContext.falseForkContext.add(head);
|
424
1163
|
}
|
425
1164
|
|
426
1165
|
break;
|
@@ -428,82 +1167,129 @@ class CodePathState {
|
|
428
1167
|
case "loop":
|
429
1168
|
|
430
1169
|
/*
|
431
|
-
* Loops are addressed in popLoopContext()
|
432
|
-
*
|
1170
|
+
* Loops are addressed in `popLoopContext()` so just return
|
1171
|
+
* the context without modification.
|
433
1172
|
*/
|
434
|
-
return
|
1173
|
+
return poppedChoiceContext;
|
435
1174
|
|
436
1175
|
/* c8 ignore next */
|
437
1176
|
default:
|
438
1177
|
throw new Error("unreachable");
|
439
1178
|
}
|
440
1179
|
|
441
|
-
|
442
|
-
|
1180
|
+
/*
|
1181
|
+
* Merge the true path with the false path to create a single path.
|
1182
|
+
*/
|
1183
|
+
const combinedForkContext = poppedChoiceContext.trueForkContext;
|
443
1184
|
|
444
|
-
|
445
|
-
forkContext.replaceHead(
|
1185
|
+
combinedForkContext.addAll(poppedChoiceContext.falseForkContext);
|
1186
|
+
forkContext.replaceHead(combinedForkContext.makeNext(0, -1));
|
446
1187
|
|
447
|
-
return
|
1188
|
+
return poppedChoiceContext;
|
448
1189
|
}
|
449
1190
|
|
450
1191
|
/**
|
451
|
-
*
|
1192
|
+
* Creates a code path segment to represent right-hand operand of a logical
|
452
1193
|
* expression.
|
1194
|
+
* This is called in the preprocessing phase when entering a node.
|
453
1195
|
* @throws {Error} (Unreachable.)
|
454
1196
|
* @returns {void}
|
455
1197
|
*/
|
456
1198
|
makeLogicalRight() {
|
457
|
-
const
|
1199
|
+
const currentChoiceContext = this.choiceContext;
|
458
1200
|
const forkContext = this.forkContext;
|
459
1201
|
|
460
|
-
if (
|
1202
|
+
if (currentChoiceContext.processed) {
|
461
1203
|
|
462
1204
|
/*
|
463
|
-
* This
|
464
|
-
*
|
1205
|
+
* This context was already assigned segments from a child
|
1206
|
+
* choice context. In this case, we are concerned only about
|
1207
|
+
* the path that does not short-circuit and so ends up on the
|
1208
|
+
* right-hand operand of the logical expression.
|
465
1209
|
*/
|
466
1210
|
let prevForkContext;
|
467
1211
|
|
468
|
-
switch (
|
1212
|
+
switch (currentChoiceContext.kind) {
|
469
1213
|
case "&&": // if true then go to the right-hand side.
|
470
|
-
prevForkContext =
|
1214
|
+
prevForkContext = currentChoiceContext.trueForkContext;
|
471
1215
|
break;
|
472
1216
|
case "||": // if false then go to the right-hand side.
|
473
|
-
prevForkContext =
|
1217
|
+
prevForkContext = currentChoiceContext.falseForkContext;
|
474
1218
|
break;
|
475
|
-
case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's
|
476
|
-
prevForkContext =
|
1219
|
+
case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's nullishForkContext.
|
1220
|
+
prevForkContext = currentChoiceContext.nullishForkContext;
|
477
1221
|
break;
|
478
1222
|
default:
|
479
1223
|
throw new Error("unreachable");
|
480
1224
|
}
|
481
1225
|
|
1226
|
+
/*
|
1227
|
+
* Create the segment for the right-hand operand of the logical expression
|
1228
|
+
* and adjust the fork context pointer to point there. The right-hand segment
|
1229
|
+
* is added at the end of all segments in `prevForkContext`.
|
1230
|
+
*/
|
482
1231
|
forkContext.replaceHead(prevForkContext.makeNext(0, -1));
|
1232
|
+
|
1233
|
+
/*
|
1234
|
+
* We no longer need this list of segments.
|
1235
|
+
*
|
1236
|
+
* Reset `processed` because we've removed the segments from the child
|
1237
|
+
* choice context. This allows `popChoiceContext()` to continue adding
|
1238
|
+
* segments later.
|
1239
|
+
*/
|
483
1240
|
prevForkContext.clear();
|
484
|
-
|
1241
|
+
currentChoiceContext.processed = false;
|
1242
|
+
|
485
1243
|
} else {
|
486
1244
|
|
487
1245
|
/*
|
488
|
-
* This
|
489
|
-
*
|
490
|
-
*
|
1246
|
+
* This choice context was not assigned segments from a child
|
1247
|
+
* choice context, which means that it's a terminal logical
|
1248
|
+
* expression.
|
1249
|
+
*
|
1250
|
+
* `head` is the segments for the left-hand operand of the
|
1251
|
+
* logical expression.
|
1252
|
+
*
|
1253
|
+
* Each of the fork contexts below are empty at this point. We choose
|
1254
|
+
* the path(s) that will short-circuit and add the segment for the
|
1255
|
+
* left-hand operand to it. Ultimately, this will be the only segment
|
1256
|
+
* in that path due to the short-circuting, so we are just seeding
|
1257
|
+
* these paths to start.
|
491
1258
|
*/
|
492
|
-
switch (
|
493
|
-
case "&&":
|
494
|
-
|
1259
|
+
switch (currentChoiceContext.kind) {
|
1260
|
+
case "&&":
|
1261
|
+
|
1262
|
+
/*
|
1263
|
+
* In most contexts, when a && expression evaluates to false,
|
1264
|
+
* it short circuits, so we need to account for that by setting
|
1265
|
+
* the `falseForkContext` to the left operand.
|
1266
|
+
*
|
1267
|
+
* When a && expression is the left-hand operand for a ??
|
1268
|
+
* expression, such as `(a && b) ?? c`, a nullish value will
|
1269
|
+
* also short-circuit in a different way than a false value,
|
1270
|
+
* so we also set the `nullishForkContext` to the left operand.
|
1271
|
+
* This path is only used with a ?? expression and is thrown
|
1272
|
+
* away for any other type of logical expression, so it's safe
|
1273
|
+
* to always add.
|
1274
|
+
*/
|
1275
|
+
currentChoiceContext.falseForkContext.add(forkContext.head);
|
1276
|
+
currentChoiceContext.nullishForkContext.add(forkContext.head);
|
495
1277
|
break;
|
496
1278
|
case "||": // the true path can short-circuit.
|
497
|
-
|
1279
|
+
currentChoiceContext.trueForkContext.add(forkContext.head);
|
498
1280
|
break;
|
499
1281
|
case "??": // both can short-circuit.
|
500
|
-
|
501
|
-
|
1282
|
+
currentChoiceContext.trueForkContext.add(forkContext.head);
|
1283
|
+
currentChoiceContext.falseForkContext.add(forkContext.head);
|
502
1284
|
break;
|
503
1285
|
default:
|
504
1286
|
throw new Error("unreachable");
|
505
1287
|
}
|
506
1288
|
|
1289
|
+
/*
|
1290
|
+
* Create the segment for the right-hand operand of the logical expression
|
1291
|
+
* and adjust the fork context pointer to point there.
|
1292
|
+
*/
|
507
1293
|
forkContext.replaceHead(forkContext.makeNext(-1, -1));
|
508
1294
|
}
|
509
1295
|
}
|
@@ -524,7 +1310,7 @@ class CodePathState {
|
|
524
1310
|
if (!context.processed) {
|
525
1311
|
context.trueForkContext.add(forkContext.head);
|
526
1312
|
context.falseForkContext.add(forkContext.head);
|
527
|
-
context.
|
1313
|
+
context.nullishForkContext.add(forkContext.head);
|
528
1314
|
}
|
529
1315
|
|
530
1316
|
context.processed = false;
|
@@ -562,22 +1348,20 @@ class CodePathState {
|
|
562
1348
|
//--------------------------------------------------------------------------
|
563
1349
|
|
564
1350
|
/**
|
565
|
-
*
|
566
|
-
*
|
567
|
-
*
|
1351
|
+
* Pushes a new `ChainExpression` context to the stack. This method is
|
1352
|
+
* called when entering a `ChainExpression` node. A chain context is used to
|
1353
|
+
* count forking in the optional chain then merge them on the exiting from the
|
1354
|
+
* `ChainExpression` node.
|
568
1355
|
* @returns {void}
|
569
1356
|
*/
|
570
1357
|
pushChainContext() {
|
571
|
-
this.chainContext =
|
572
|
-
upper: this.chainContext,
|
573
|
-
countChoiceContexts: 0
|
574
|
-
};
|
1358
|
+
this.chainContext = new ChainContext(this.chainContext);
|
575
1359
|
}
|
576
1360
|
|
577
1361
|
/**
|
578
|
-
* Pop a `ChainExpression` context from the stack.
|
579
|
-
*
|
580
|
-
*
|
1362
|
+
* Pop a `ChainExpression` context from the stack. This method is called on
|
1363
|
+
* exiting from each `ChainExpression` node. This merges all forks of the
|
1364
|
+
* last optional chaining.
|
581
1365
|
* @returns {void}
|
582
1366
|
*/
|
583
1367
|
popChainContext() {
|
@@ -586,7 +1370,7 @@ class CodePathState {
|
|
586
1370
|
this.chainContext = context.upper;
|
587
1371
|
|
588
1372
|
// pop all choice contexts of this.
|
589
|
-
for (let i = context.
|
1373
|
+
for (let i = context.choiceContextCount; i > 0; --i) {
|
590
1374
|
this.popChoiceContext();
|
591
1375
|
}
|
592
1376
|
}
|
@@ -599,7 +1383,7 @@ class CodePathState {
|
|
599
1383
|
*/
|
600
1384
|
makeOptionalNode() {
|
601
1385
|
if (this.chainContext) {
|
602
|
-
this.chainContext.
|
1386
|
+
this.chainContext.choiceContextCount += 1;
|
603
1387
|
this.pushChoiceContext("??", false);
|
604
1388
|
}
|
605
1389
|
}
|
@@ -627,16 +1411,7 @@ class CodePathState {
|
|
627
1411
|
* @returns {void}
|
628
1412
|
*/
|
629
1413
|
pushSwitchContext(hasCase, label) {
|
630
|
-
this.switchContext =
|
631
|
-
upper: this.switchContext,
|
632
|
-
hasCase,
|
633
|
-
defaultSegments: null,
|
634
|
-
defaultBodySegments: null,
|
635
|
-
foundDefault: false,
|
636
|
-
lastIsDefault: false,
|
637
|
-
countForks: 0
|
638
|
-
};
|
639
|
-
|
1414
|
+
this.switchContext = new SwitchContext(this.switchContext, hasCase);
|
640
1415
|
this.pushBreakContext(true, label);
|
641
1416
|
}
|
642
1417
|
|
@@ -657,7 +1432,7 @@ class CodePathState {
|
|
657
1432
|
const forkContext = this.forkContext;
|
658
1433
|
const brokenForkContext = this.popBreakContext().brokenForkContext;
|
659
1434
|
|
660
|
-
if (context.
|
1435
|
+
if (context.forkCount === 0) {
|
661
1436
|
|
662
1437
|
/*
|
663
1438
|
* When there is only one `default` chunk and there is one or more
|
@@ -684,47 +1459,54 @@ class CodePathState {
|
|
684
1459
|
brokenForkContext.add(lastSegments);
|
685
1460
|
|
686
1461
|
/*
|
687
|
-
*
|
688
|
-
*
|
1462
|
+
* Any value that doesn't match a `case` test should flow to the default
|
1463
|
+
* case. That happens normally when the default case is last in the `switch`,
|
1464
|
+
* but if it's not, we need to rewire some of the paths to be correct.
|
689
1465
|
*/
|
690
1466
|
if (!context.lastIsDefault) {
|
691
1467
|
if (context.defaultBodySegments) {
|
692
1468
|
|
693
1469
|
/*
|
694
|
-
*
|
695
|
-
*
|
1470
|
+
* There is a non-empty default case, so remove the path from the `default`
|
1471
|
+
* label to its body for an accurate representation.
|
1472
|
+
*/
|
1473
|
+
disconnectSegments(context.defaultSegments, context.defaultBodySegments);
|
1474
|
+
|
1475
|
+
/*
|
1476
|
+
* Connect the path from the last non-default case to the body of the
|
1477
|
+
* default case.
|
696
1478
|
*/
|
697
|
-
removeConnection(context.defaultSegments, context.defaultBodySegments);
|
698
1479
|
makeLooped(this, lastCaseSegments, context.defaultBodySegments);
|
1480
|
+
|
699
1481
|
} else {
|
700
1482
|
|
701
1483
|
/*
|
702
|
-
*
|
703
|
-
*
|
1484
|
+
* There is no default case, so we treat this as if the last case
|
1485
|
+
* had a `break` in it.
|
704
1486
|
*/
|
705
1487
|
brokenForkContext.add(lastCaseSegments);
|
706
1488
|
}
|
707
1489
|
}
|
708
1490
|
|
709
|
-
//
|
710
|
-
for (let i = 0; i < context.
|
1491
|
+
// Traverse up to the original fork context for the `switch` statement
|
1492
|
+
for (let i = 0; i < context.forkCount; ++i) {
|
711
1493
|
this.forkContext = this.forkContext.upper;
|
712
1494
|
}
|
713
1495
|
|
714
1496
|
/*
|
715
|
-
* Creates a path from all brokenForkContext paths.
|
716
|
-
* This is a path after switch statement.
|
1497
|
+
* Creates a path from all `brokenForkContext` paths.
|
1498
|
+
* This is a path after `switch` statement.
|
717
1499
|
*/
|
718
1500
|
this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
|
719
1501
|
}
|
720
1502
|
|
721
1503
|
/**
|
722
1504
|
* Makes a code path segment for a `SwitchCase` node.
|
723
|
-
* @param {boolean}
|
724
|
-
* @param {boolean}
|
1505
|
+
* @param {boolean} isCaseBodyEmpty `true` if the body is empty.
|
1506
|
+
* @param {boolean} isDefaultCase `true` if the body is the default case.
|
725
1507
|
* @returns {void}
|
726
1508
|
*/
|
727
|
-
makeSwitchCaseBody(
|
1509
|
+
makeSwitchCaseBody(isCaseBodyEmpty, isDefaultCase) {
|
728
1510
|
const context = this.switchContext;
|
729
1511
|
|
730
1512
|
if (!context.hasCase) {
|
@@ -734,7 +1516,7 @@ class CodePathState {
|
|
734
1516
|
/*
|
735
1517
|
* Merge forks.
|
736
1518
|
* The parent fork context has two segments.
|
737
|
-
* Those are from the current case and the body of the previous case.
|
1519
|
+
* Those are from the current `case` and the body of the previous case.
|
738
1520
|
*/
|
739
1521
|
const parentForkContext = this.forkContext;
|
740
1522
|
const forkContext = this.pushForkContext();
|
@@ -742,26 +1524,53 @@ class CodePathState {
|
|
742
1524
|
forkContext.add(parentForkContext.makeNext(0, -1));
|
743
1525
|
|
744
1526
|
/*
|
745
|
-
*
|
746
|
-
*
|
747
|
-
*
|
1527
|
+
* Add information about the default case.
|
1528
|
+
*
|
1529
|
+
* The purpose of this is to identify the starting segments for the
|
1530
|
+
* default case to make sure there is a path there.
|
748
1531
|
*/
|
749
|
-
if (
|
1532
|
+
if (isDefaultCase) {
|
1533
|
+
|
1534
|
+
/*
|
1535
|
+
* This is the default case in the `switch`.
|
1536
|
+
*
|
1537
|
+
* We first save the current pointer as `defaultSegments` to point
|
1538
|
+
* to the `default` keyword.
|
1539
|
+
*/
|
750
1540
|
context.defaultSegments = parentForkContext.head;
|
751
|
-
|
752
|
-
|
1541
|
+
|
1542
|
+
/*
|
1543
|
+
* If the body of the case is empty then we just set
|
1544
|
+
* `foundEmptyDefault` to true; otherwise, we save a reference
|
1545
|
+
* to the current pointer as `defaultBodySegments`.
|
1546
|
+
*/
|
1547
|
+
if (isCaseBodyEmpty) {
|
1548
|
+
context.foundEmptyDefault = true;
|
753
1549
|
} else {
|
754
1550
|
context.defaultBodySegments = forkContext.head;
|
755
1551
|
}
|
1552
|
+
|
756
1553
|
} else {
|
757
|
-
|
758
|
-
|
1554
|
+
|
1555
|
+
/*
|
1556
|
+
* This is not the default case in the `switch`.
|
1557
|
+
*
|
1558
|
+
* If it's not empty and there is already an empty default case found,
|
1559
|
+
* that means the default case actually comes before this case,
|
1560
|
+
* and that it will fall through to this case. So, we can now
|
1561
|
+
* ignore the previous default case (reset `foundEmptyDefault` to false)
|
1562
|
+
* and set `defaultBodySegments` to the current segments because this is
|
1563
|
+
* effectively the new default case.
|
1564
|
+
*/
|
1565
|
+
if (!isCaseBodyEmpty && context.foundEmptyDefault) {
|
1566
|
+
context.foundEmptyDefault = false;
|
759
1567
|
context.defaultBodySegments = forkContext.head;
|
760
1568
|
}
|
761
1569
|
}
|
762
1570
|
|
763
|
-
|
764
|
-
context.
|
1571
|
+
// keep track if the default case ends up last
|
1572
|
+
context.lastIsDefault = isDefaultCase;
|
1573
|
+
context.forkCount += 1;
|
765
1574
|
}
|
766
1575
|
|
767
1576
|
//--------------------------------------------------------------------------
|
@@ -775,19 +1584,7 @@ class CodePathState {
|
|
775
1584
|
* @returns {void}
|
776
1585
|
*/
|
777
1586
|
pushTryContext(hasFinalizer) {
|
778
|
-
this.tryContext =
|
779
|
-
upper: this.tryContext,
|
780
|
-
position: "try",
|
781
|
-
hasFinalizer,
|
782
|
-
|
783
|
-
returnedForkContext: hasFinalizer
|
784
|
-
? ForkContext.newEmpty(this.forkContext)
|
785
|
-
: null,
|
786
|
-
|
787
|
-
thrownForkContext: ForkContext.newEmpty(this.forkContext),
|
788
|
-
lastOfTryIsReachable: false,
|
789
|
-
lastOfCatchIsReachable: false
|
790
|
-
};
|
1587
|
+
this.tryContext = new TryContext(this.tryContext, hasFinalizer, this.forkContext);
|
791
1588
|
}
|
792
1589
|
|
793
1590
|
/**
|
@@ -799,25 +1596,35 @@ class CodePathState {
|
|
799
1596
|
|
800
1597
|
this.tryContext = context.upper;
|
801
1598
|
|
1599
|
+
/*
|
1600
|
+
* If we're inside the `catch` block, that means there is no `finally`,
|
1601
|
+
* so we can process the `try` and `catch` blocks the simple way and
|
1602
|
+
* merge their two paths.
|
1603
|
+
*/
|
802
1604
|
if (context.position === "catch") {
|
803
|
-
|
804
|
-
// Merges two paths from the `try` block and `catch` block merely.
|
805
1605
|
this.popForkContext();
|
806
1606
|
return;
|
807
1607
|
}
|
808
1608
|
|
809
1609
|
/*
|
810
|
-
* The following process is executed only when there is
|
1610
|
+
* The following process is executed only when there is a `finally`
|
811
1611
|
* block.
|
812
1612
|
*/
|
813
1613
|
|
814
|
-
const
|
815
|
-
const
|
1614
|
+
const originalReturnedForkContext = context.returnedForkContext;
|
1615
|
+
const originalThrownForkContext = context.thrownForkContext;
|
816
1616
|
|
817
|
-
|
1617
|
+
// no `return` or `throw` in `try` or `catch` so there's nothing left to do
|
1618
|
+
if (originalReturnedForkContext.empty && originalThrownForkContext.empty) {
|
818
1619
|
return;
|
819
1620
|
}
|
820
1621
|
|
1622
|
+
/*
|
1623
|
+
* The following process is executed only when there is a `finally`
|
1624
|
+
* block and there was a `return` or `throw` in the `try` or `catch`
|
1625
|
+
* blocks.
|
1626
|
+
*/
|
1627
|
+
|
821
1628
|
// Separate head to normal paths and leaving paths.
|
822
1629
|
const headSegments = this.forkContext.head;
|
823
1630
|
|
@@ -826,10 +1633,10 @@ class CodePathState {
|
|
826
1633
|
const leavingSegments = headSegments.slice(headSegments.length / 2 | 0);
|
827
1634
|
|
828
1635
|
// Forwards the leaving path to upper contexts.
|
829
|
-
if (!
|
1636
|
+
if (!originalReturnedForkContext.empty) {
|
830
1637
|
getReturnContext(this).returnedForkContext.add(leavingSegments);
|
831
1638
|
}
|
832
|
-
if (!
|
1639
|
+
if (!originalThrownForkContext.empty) {
|
833
1640
|
getThrowContext(this).thrownForkContext.add(leavingSegments);
|
834
1641
|
}
|
835
1642
|
|
@@ -852,16 +1659,20 @@ class CodePathState {
|
|
852
1659
|
makeCatchBlock() {
|
853
1660
|
const context = this.tryContext;
|
854
1661
|
const forkContext = this.forkContext;
|
855
|
-
const
|
1662
|
+
const originalThrownForkContext = context.thrownForkContext;
|
856
1663
|
|
857
|
-
|
1664
|
+
/*
|
1665
|
+
* We are now in a catch block so we need to update the context
|
1666
|
+
* with that information. This includes creating a new fork
|
1667
|
+
* context in case we encounter any `throw` statements here.
|
1668
|
+
*/
|
858
1669
|
context.position = "catch";
|
859
1670
|
context.thrownForkContext = ForkContext.newEmpty(forkContext);
|
860
1671
|
context.lastOfTryIsReachable = forkContext.reachable;
|
861
1672
|
|
862
|
-
// Merge thrown paths
|
863
|
-
|
864
|
-
const thrownSegments =
|
1673
|
+
// Merge the thrown paths from the `try` and `catch` blocks
|
1674
|
+
originalThrownForkContext.add(forkContext.head);
|
1675
|
+
const thrownSegments = originalThrownForkContext.makeNext(0, -1);
|
865
1676
|
|
866
1677
|
// Fork to a bypass and the merged thrown path.
|
867
1678
|
this.pushForkContext();
|
@@ -880,8 +1691,8 @@ class CodePathState {
|
|
880
1691
|
makeFinallyBlock() {
|
881
1692
|
const context = this.tryContext;
|
882
1693
|
let forkContext = this.forkContext;
|
883
|
-
const
|
884
|
-
const
|
1694
|
+
const originalReturnedForkContext = context.returnedForkContext;
|
1695
|
+
const originalThrownForContext = context.thrownForkContext;
|
885
1696
|
const headOfLeavingSegments = forkContext.head;
|
886
1697
|
|
887
1698
|
// Update state.
|
@@ -895,9 +1706,15 @@ class CodePathState {
|
|
895
1706
|
} else {
|
896
1707
|
context.lastOfTryIsReachable = forkContext.reachable;
|
897
1708
|
}
|
1709
|
+
|
1710
|
+
|
898
1711
|
context.position = "finally";
|
899
1712
|
|
900
|
-
|
1713
|
+
/*
|
1714
|
+
* If there was no `return` or `throw` in either the `try` or `catch`
|
1715
|
+
* blocks, then there's no further code paths to create for `finally`.
|
1716
|
+
*/
|
1717
|
+
if (originalReturnedForkContext.empty && originalThrownForContext.empty) {
|
901
1718
|
|
902
1719
|
// This path does not leave.
|
903
1720
|
return;
|
@@ -905,18 +1722,18 @@ class CodePathState {
|
|
905
1722
|
|
906
1723
|
/*
|
907
1724
|
* Create a parallel segment from merging returned and thrown.
|
908
|
-
* This segment will leave at the end of this finally block.
|
1725
|
+
* This segment will leave at the end of this `finally` block.
|
909
1726
|
*/
|
910
1727
|
const segments = forkContext.makeNext(-1, -1);
|
911
1728
|
|
912
1729
|
for (let i = 0; i < forkContext.count; ++i) {
|
913
1730
|
const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];
|
914
1731
|
|
915
|
-
for (let j = 0; j <
|
916
|
-
prevSegsOfLeavingSegment.push(
|
1732
|
+
for (let j = 0; j < originalReturnedForkContext.segmentsList.length; ++j) {
|
1733
|
+
prevSegsOfLeavingSegment.push(originalReturnedForkContext.segmentsList[j][i]);
|
917
1734
|
}
|
918
|
-
for (let j = 0; j <
|
919
|
-
prevSegsOfLeavingSegment.push(
|
1735
|
+
for (let j = 0; j < originalThrownForContext.segmentsList.length; ++j) {
|
1736
|
+
prevSegsOfLeavingSegment.push(originalThrownForContext.segmentsList[j][i]);
|
920
1737
|
}
|
921
1738
|
|
922
1739
|
segments.push(
|
@@ -971,63 +1788,32 @@ class CodePathState {
|
|
971
1788
|
*/
|
972
1789
|
pushLoopContext(type, label) {
|
973
1790
|
const forkContext = this.forkContext;
|
1791
|
+
|
1792
|
+
// All loops need a path to account for `break` statements
|
974
1793
|
const breakContext = this.pushBreakContext(true, label);
|
975
1794
|
|
976
1795
|
switch (type) {
|
977
1796
|
case "WhileStatement":
|
978
1797
|
this.pushChoiceContext("loop", false);
|
979
|
-
this.loopContext =
|
980
|
-
upper: this.loopContext,
|
981
|
-
type,
|
982
|
-
label,
|
983
|
-
test: void 0,
|
984
|
-
continueDestSegments: null,
|
985
|
-
brokenForkContext: breakContext.brokenForkContext
|
986
|
-
};
|
1798
|
+
this.loopContext = new WhileLoopContext(this.loopContext, label, breakContext);
|
987
1799
|
break;
|
988
1800
|
|
989
1801
|
case "DoWhileStatement":
|
990
1802
|
this.pushChoiceContext("loop", false);
|
991
|
-
this.loopContext =
|
992
|
-
upper: this.loopContext,
|
993
|
-
type,
|
994
|
-
label,
|
995
|
-
test: void 0,
|
996
|
-
entrySegments: null,
|
997
|
-
continueForkContext: ForkContext.newEmpty(forkContext),
|
998
|
-
brokenForkContext: breakContext.brokenForkContext
|
999
|
-
};
|
1803
|
+
this.loopContext = new DoWhileLoopContext(this.loopContext, label, breakContext, forkContext);
|
1000
1804
|
break;
|
1001
1805
|
|
1002
1806
|
case "ForStatement":
|
1003
1807
|
this.pushChoiceContext("loop", false);
|
1004
|
-
this.loopContext =
|
1005
|
-
upper: this.loopContext,
|
1006
|
-
type,
|
1007
|
-
label,
|
1008
|
-
test: void 0,
|
1009
|
-
endOfInitSegments: null,
|
1010
|
-
testSegments: null,
|
1011
|
-
endOfTestSegments: null,
|
1012
|
-
updateSegments: null,
|
1013
|
-
endOfUpdateSegments: null,
|
1014
|
-
continueDestSegments: null,
|
1015
|
-
brokenForkContext: breakContext.brokenForkContext
|
1016
|
-
};
|
1808
|
+
this.loopContext = new ForLoopContext(this.loopContext, label, breakContext);
|
1017
1809
|
break;
|
1018
1810
|
|
1019
1811
|
case "ForInStatement":
|
1812
|
+
this.loopContext = new ForInLoopContext(this.loopContext, label, breakContext);
|
1813
|
+
break;
|
1814
|
+
|
1020
1815
|
case "ForOfStatement":
|
1021
|
-
this.loopContext =
|
1022
|
-
upper: this.loopContext,
|
1023
|
-
type,
|
1024
|
-
label,
|
1025
|
-
prevSegments: null,
|
1026
|
-
leftSegments: null,
|
1027
|
-
endOfLeftSegments: null,
|
1028
|
-
continueDestSegments: null,
|
1029
|
-
brokenForkContext: breakContext.brokenForkContext
|
1030
|
-
};
|
1816
|
+
this.loopContext = new ForOfLoopContext(this.loopContext, label, breakContext);
|
1031
1817
|
break;
|
1032
1818
|
|
1033
1819
|
/* c8 ignore next */
|
@@ -1054,6 +1840,11 @@ class CodePathState {
|
|
1054
1840
|
case "WhileStatement":
|
1055
1841
|
case "ForStatement":
|
1056
1842
|
this.popChoiceContext();
|
1843
|
+
|
1844
|
+
/*
|
1845
|
+
* Creates the path from the end of the loop body up to the
|
1846
|
+
* location where `continue` would jump to.
|
1847
|
+
*/
|
1057
1848
|
makeLooped(
|
1058
1849
|
this,
|
1059
1850
|
forkContext.head,
|
@@ -1068,11 +1859,21 @@ class CodePathState {
|
|
1068
1859
|
choiceContext.trueForkContext.add(forkContext.head);
|
1069
1860
|
choiceContext.falseForkContext.add(forkContext.head);
|
1070
1861
|
}
|
1862
|
+
|
1863
|
+
/*
|
1864
|
+
* If this isn't a hardcoded `true` condition, then `break`
|
1865
|
+
* should continue down the path as if the condition evaluated
|
1866
|
+
* to false.
|
1867
|
+
*/
|
1071
1868
|
if (context.test !== true) {
|
1072
1869
|
brokenForkContext.addAll(choiceContext.falseForkContext);
|
1073
1870
|
}
|
1074
1871
|
|
1075
|
-
|
1872
|
+
/*
|
1873
|
+
* When the condition is true, the loop continues back to the top,
|
1874
|
+
* so create a path from each possible true condition back to the
|
1875
|
+
* top of the loop.
|
1876
|
+
*/
|
1076
1877
|
const segmentsList = choiceContext.trueForkContext.segmentsList;
|
1077
1878
|
|
1078
1879
|
for (let i = 0; i < segmentsList.length; ++i) {
|
@@ -1088,6 +1889,11 @@ class CodePathState {
|
|
1088
1889
|
case "ForInStatement":
|
1089
1890
|
case "ForOfStatement":
|
1090
1891
|
brokenForkContext.add(forkContext.head);
|
1892
|
+
|
1893
|
+
/*
|
1894
|
+
* Creates the path from the end of the loop body up to the
|
1895
|
+
* left expression (left of `in` or `of`) of the loop.
|
1896
|
+
*/
|
1091
1897
|
makeLooped(
|
1092
1898
|
this,
|
1093
1899
|
forkContext.head,
|
@@ -1100,7 +1906,14 @@ class CodePathState {
|
|
1100
1906
|
throw new Error("unreachable");
|
1101
1907
|
}
|
1102
1908
|
|
1103
|
-
|
1909
|
+
/*
|
1910
|
+
* If there wasn't a `break` statement in the loop, then we're at
|
1911
|
+
* the end of the loop's path, so we make an unreachable segment
|
1912
|
+
* to mark that.
|
1913
|
+
*
|
1914
|
+
* If there was a `break` statement, then we continue on into the
|
1915
|
+
* `brokenForkContext`.
|
1916
|
+
*/
|
1104
1917
|
if (brokenForkContext.empty) {
|
1105
1918
|
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
|
1106
1919
|
} else {
|
@@ -1138,7 +1951,11 @@ class CodePathState {
|
|
1138
1951
|
choiceContext.falseForkContext.add(forkContext.head);
|
1139
1952
|
}
|
1140
1953
|
|
1141
|
-
|
1954
|
+
/*
|
1955
|
+
* If this isn't a hardcoded `true` condition, then `break`
|
1956
|
+
* should continue down the path as if the condition evaluated
|
1957
|
+
* to false.
|
1958
|
+
*/
|
1142
1959
|
if (context.test !== true) {
|
1143
1960
|
context.brokenForkContext.addAll(choiceContext.falseForkContext);
|
1144
1961
|
}
|
@@ -1170,7 +1987,11 @@ class CodePathState {
|
|
1170
1987
|
|
1171
1988
|
context.test = test;
|
1172
1989
|
|
1173
|
-
|
1990
|
+
/*
|
1991
|
+
* If there is a `continue` statement in the loop then `continueForkContext`
|
1992
|
+
* won't be empty. We wire up the path from `continue` to the loop
|
1993
|
+
* test condition and then continue the traversal in the root fork context.
|
1994
|
+
*/
|
1174
1995
|
if (!context.continueForkContext.empty) {
|
1175
1996
|
context.continueForkContext.add(forkContext.head);
|
1176
1997
|
const testSegments = context.continueForkContext.makeNext(0, -1);
|
@@ -1190,7 +2011,14 @@ class CodePathState {
|
|
1190
2011
|
const endOfInitSegments = forkContext.head;
|
1191
2012
|
const testSegments = forkContext.makeNext(-1, -1);
|
1192
2013
|
|
1193
|
-
|
2014
|
+
/*
|
2015
|
+
* Update the state.
|
2016
|
+
*
|
2017
|
+
* The `continueDestSegments` are set to `testSegments` because we
|
2018
|
+
* don't yet know if there is an update expression in this loop. So,
|
2019
|
+
* from what we already know at this point, a `continue` statement
|
2020
|
+
* will jump back to the test expression.
|
2021
|
+
*/
|
1194
2022
|
context.test = test;
|
1195
2023
|
context.endOfInitSegments = endOfInitSegments;
|
1196
2024
|
context.continueDestSegments = context.testSegments = testSegments;
|
@@ -1217,7 +2045,14 @@ class CodePathState {
|
|
1217
2045
|
context.endOfInitSegments = forkContext.head;
|
1218
2046
|
}
|
1219
2047
|
|
1220
|
-
|
2048
|
+
/*
|
2049
|
+
* Update the state.
|
2050
|
+
*
|
2051
|
+
* The `continueDestSegments` are now set to `updateSegments` because we
|
2052
|
+
* know there is an update expression in this loop. So, a `continue` statement
|
2053
|
+
* in the loop will jump to the update expression first, and then to any
|
2054
|
+
* test expression the loop might have.
|
2055
|
+
*/
|
1221
2056
|
const updateSegments = forkContext.makeDisconnected(-1, -1);
|
1222
2057
|
|
1223
2058
|
context.continueDestSegments = context.updateSegments = updateSegments;
|
@@ -1233,11 +2068,30 @@ class CodePathState {
|
|
1233
2068
|
const choiceContext = this.choiceContext;
|
1234
2069
|
const forkContext = this.forkContext;
|
1235
2070
|
|
1236
|
-
|
2071
|
+
/*
|
2072
|
+
* Determine what to do based on which part of the `for` loop are present.
|
2073
|
+
* 1. If there is an update expression, then `updateSegments` is not null and
|
2074
|
+
* we need to assign `endOfUpdateSegments`, and if there is a test
|
2075
|
+
* expression, we then need to create the looped path to get back to
|
2076
|
+
* the test condition.
|
2077
|
+
* 2. If there is no update expression but there is a test expression,
|
2078
|
+
* then we only need to update the test segment information.
|
2079
|
+
* 3. If there is no update expression and no test expression, then we
|
2080
|
+
* just save `endOfInitSegments`.
|
2081
|
+
*/
|
1237
2082
|
if (context.updateSegments) {
|
1238
2083
|
context.endOfUpdateSegments = forkContext.head;
|
1239
2084
|
|
1240
|
-
|
2085
|
+
/*
|
2086
|
+
* In a `for` loop that has both an update expression and a test
|
2087
|
+
* condition, execution flows from the test expression into the
|
2088
|
+
* loop body, to the update expression, and then back to the test
|
2089
|
+
* expression to determine if the loop should continue.
|
2090
|
+
*
|
2091
|
+
* To account for that, we need to make a path from the end of the
|
2092
|
+
* update expression to the start of the test expression. This is
|
2093
|
+
* effectively what creates the loop in the code path.
|
2094
|
+
*/
|
1241
2095
|
if (context.testSegments) {
|
1242
2096
|
makeLooped(
|
1243
2097
|
this,
|
@@ -1257,12 +2111,18 @@ class CodePathState {
|
|
1257
2111
|
|
1258
2112
|
let bodySegments = context.endOfTestSegments;
|
1259
2113
|
|
2114
|
+
/*
|
2115
|
+
* If there is a test condition, then there `endOfTestSegments` is also
|
2116
|
+
* the start of the loop body. If there isn't a test condition then
|
2117
|
+
* `bodySegments` will be null and we need to look elsewhere to find
|
2118
|
+
* the start of the body.
|
2119
|
+
*
|
2120
|
+
* The body starts at the end of the init expression and ends at the end
|
2121
|
+
* of the update expression, so we use those locations to determine the
|
2122
|
+
* body segments.
|
2123
|
+
*/
|
1260
2124
|
if (!bodySegments) {
|
1261
2125
|
|
1262
|
-
/*
|
1263
|
-
* If there is not the `test` part, the `body` path comes from the
|
1264
|
-
* `init` part and the `update` part.
|
1265
|
-
*/
|
1266
2126
|
const prevForkContext = ForkContext.newEmpty(forkContext);
|
1267
2127
|
|
1268
2128
|
prevForkContext.add(context.endOfInitSegments);
|
@@ -1272,7 +2132,16 @@ class CodePathState {
|
|
1272
2132
|
|
1273
2133
|
bodySegments = prevForkContext.makeNext(0, -1);
|
1274
2134
|
}
|
2135
|
+
|
2136
|
+
/*
|
2137
|
+
* If there was no test condition and no update expression, then
|
2138
|
+
* `continueDestSegments` will be null. In that case, a
|
2139
|
+
* `continue` should skip directly to the body of the loop.
|
2140
|
+
* Otherwise, we want to keep the current `continueDestSegments`.
|
2141
|
+
*/
|
1275
2142
|
context.continueDestSegments = context.continueDestSegments || bodySegments;
|
2143
|
+
|
2144
|
+
// move pointer to the body
|
1276
2145
|
forkContext.replaceHead(bodySegments);
|
1277
2146
|
}
|
1278
2147
|
|
@@ -1336,19 +2205,15 @@ class CodePathState {
|
|
1336
2205
|
//--------------------------------------------------------------------------
|
1337
2206
|
|
1338
2207
|
/**
|
1339
|
-
* Creates new context
|
1340
|
-
*
|
1341
|
-
*
|
1342
|
-
*
|
1343
|
-
* @
|
2208
|
+
* Creates new context in which a `break` statement can be used. This occurs inside of a loop,
|
2209
|
+
* labeled statement, or switch statement.
|
2210
|
+
* @param {boolean} breakable Indicates if we are inside a statement where
|
2211
|
+
* `break` without a label will exit the statement.
|
2212
|
+
* @param {string|null} label The label associated with the statement.
|
2213
|
+
* @returns {BreakContext} The new context.
|
1344
2214
|
*/
|
1345
2215
|
pushBreakContext(breakable, label) {
|
1346
|
-
this.breakContext =
|
1347
|
-
upper: this.breakContext,
|
1348
|
-
breakable,
|
1349
|
-
label,
|
1350
|
-
brokenForkContext: ForkContext.newEmpty(this.forkContext)
|
1351
|
-
};
|
2216
|
+
this.breakContext = new BreakContext(this.breakContext, breakable, label, this.forkContext);
|
1352
2217
|
return this.breakContext;
|
1353
2218
|
}
|
1354
2219
|
|
@@ -1380,7 +2245,7 @@ class CodePathState {
|
|
1380
2245
|
*
|
1381
2246
|
* It registers the head segment to a context of `break`.
|
1382
2247
|
* It makes new unreachable segment, then it set the head with the segment.
|
1383
|
-
* @param {string} label A label of the break statement.
|
2248
|
+
* @param {string|null} label A label of the break statement.
|
1384
2249
|
* @returns {void}
|
1385
2250
|
*/
|
1386
2251
|
makeBreak(label) {
|
@@ -1406,7 +2271,7 @@ class CodePathState {
|
|
1406
2271
|
*
|
1407
2272
|
* It makes a looping path.
|
1408
2273
|
* It makes new unreachable segment, then it set the head with the segment.
|
1409
|
-
* @param {string} label A label of the continue statement.
|
2274
|
+
* @param {string|null} label A label of the continue statement.
|
1410
2275
|
* @returns {void}
|
1411
2276
|
*/
|
1412
2277
|
makeContinue(label) {
|
@@ -1422,7 +2287,7 @@ class CodePathState {
|
|
1422
2287
|
if (context.continueDestSegments) {
|
1423
2288
|
makeLooped(this, forkContext.head, context.continueDestSegments);
|
1424
2289
|
|
1425
|
-
// If the context is a for-in/of loop, this
|
2290
|
+
// If the context is a for-in/of loop, this affects a break also.
|
1426
2291
|
if (context.type === "ForInStatement" ||
|
1427
2292
|
context.type === "ForOfStatement"
|
1428
2293
|
) {
|