eslint 8.50.0 → 8.52.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.
@@ -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 includes the given segments, adds to the `all`
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-context for a `continue` statement.
44
- * @param {CodePathState} state A state to get.
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 A state to get.
69
- * @param {string} label The label of a `break` statement.
70
- * @returns {LoopContext|SwitchContext} A context for a `break` statement.
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
- * @param {CodePathState} state A state to get.
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
- * @param {CodePathState} state A state to get.
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 element from a given array.
126
- * @param {any[]} xs An array to remove the specific element.
127
- * @param {any} x An element to be removed.
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 remove(xs, x) {
131
- xs.splice(xs.indexOf(x), 1);
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 removeConnection(prevSegments, nextSegments) {
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
- remove(prevSegment.nextSegments, nextSegment);
150
- remove(prevSegment.allNextSegments, nextSegment);
151
- remove(nextSegment.prevSegments, prevSegment);
152
- remove(nextSegment.allPrevSegments, prevSegment);
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
- * @param {CodePathState} state The instance.
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.qqForkContext.add(head);
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
- // returnedSegments and thrownSegments push elements into finalSegments also.
242
- const final = this.finalSegments = [];
243
- const returned = this.returnedForkContext = [];
244
- const thrown = this.thrownForkContext = [];
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
- * The head segments.
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 A flag that shows that goes different
345
- * paths between `true` and `false`.
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 context = this.choiceContext;
367
-
368
- this.choiceContext = context.upper;
369
-
1103
+ const poppedChoiceContext = this.choiceContext;
370
1104
  const forkContext = this.forkContext;
371
- const headSegments = forkContext.head;
1105
+ const head = forkContext.head;
1106
+
1107
+ this.choiceContext = poppedChoiceContext.upper;
372
1108
 
373
- switch (context.kind) {
1109
+ switch (poppedChoiceContext.kind) {
374
1110
  case "&&":
375
1111
  case "||":
376
1112
  case "??":
377
1113
 
378
1114
  /*
379
- * If any result were not transferred from child contexts,
380
- * this sets the head segments to both cases.
381
- * The head segments are the path of the right-hand operand.
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 (!context.processed) {
384
- context.trueForkContext.add(headSegments);
385
- context.falseForkContext.add(headSegments);
386
- context.qqForkContext.add(headSegments);
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
- * Transfers results to upper context if this context is in
391
- * test chunk.
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 (context.isForkingAsResult) {
1131
+ if (poppedChoiceContext.isForkingAsResult) {
394
1132
  const parentContext = this.choiceContext;
395
1133
 
396
- parentContext.trueForkContext.addAll(context.trueForkContext);
397
- parentContext.falseForkContext.addAll(context.falseForkContext);
398
- parentContext.qqForkContext.addAll(context.qqForkContext);
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
- return context;
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 (!context.processed) {
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
- context.trueForkContext.clear();
414
- context.trueForkContext.add(headSegments);
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
- context.falseForkContext.clear();
423
- context.falseForkContext.add(headSegments);
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
- * This is called from popLoopContext().
1170
+ * Loops are addressed in `popLoopContext()` so just return
1171
+ * the context without modification.
433
1172
  */
434
- return context;
1173
+ return poppedChoiceContext;
435
1174
 
436
1175
  /* c8 ignore next */
437
1176
  default:
438
1177
  throw new Error("unreachable");
439
1178
  }
440
1179
 
441
- // Merges all paths.
442
- const prevForkContext = context.trueForkContext;
1180
+ /*
1181
+ * Merge the true path with the false path to create a single path.
1182
+ */
1183
+ const combinedForkContext = poppedChoiceContext.trueForkContext;
443
1184
 
444
- prevForkContext.addAll(context.falseForkContext);
445
- forkContext.replaceHead(prevForkContext.makeNext(0, -1));
1185
+ combinedForkContext.addAll(poppedChoiceContext.falseForkContext);
1186
+ forkContext.replaceHead(combinedForkContext.makeNext(0, -1));
446
1187
 
447
- return context;
1188
+ return poppedChoiceContext;
448
1189
  }
449
1190
 
450
1191
  /**
451
- * Makes a code path segment of the right-hand operand of a logical
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 context = this.choiceContext;
1199
+ const currentChoiceContext = this.choiceContext;
458
1200
  const forkContext = this.forkContext;
459
1201
 
460
- if (context.processed) {
1202
+ if (currentChoiceContext.processed) {
461
1203
 
462
1204
  /*
463
- * This got segments already from the child choice context.
464
- * Creates the next path from own true/false fork context.
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 (context.kind) {
1212
+ switch (currentChoiceContext.kind) {
469
1213
  case "&&": // if true then go to the right-hand side.
470
- prevForkContext = context.trueForkContext;
1214
+ prevForkContext = currentChoiceContext.trueForkContext;
471
1215
  break;
472
1216
  case "||": // if false then go to the right-hand side.
473
- prevForkContext = context.falseForkContext;
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 qqForkContext.
476
- prevForkContext = context.qqForkContext;
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
- context.processed = false;
1241
+ currentChoiceContext.processed = false;
1242
+
485
1243
  } else {
486
1244
 
487
1245
  /*
488
- * This did not get segments from the child choice context.
489
- * So addresses the head segments.
490
- * The head segments are the path of the left-hand operand.
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 (context.kind) {
493
- case "&&": // the false path can short-circuit.
494
- context.falseForkContext.add(forkContext.head);
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
- context.trueForkContext.add(forkContext.head);
1279
+ currentChoiceContext.trueForkContext.add(forkContext.head);
498
1280
  break;
499
1281
  case "??": // both can short-circuit.
500
- context.trueForkContext.add(forkContext.head);
501
- context.falseForkContext.add(forkContext.head);
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.qqForkContext.add(forkContext.head);
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
- * Push a new `ChainExpression` context to the stack.
566
- * This method is called on entering to each `ChainExpression` node.
567
- * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
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
- * This method is called on exiting from each `ChainExpression` node.
580
- * This merges all forks of the last optional chaining.
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.countChoiceContexts; i > 0; --i) {
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.countChoiceContexts += 1;
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.countForks === 0) {
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
- * A path which is failed in all case test should be connected to path
688
- * of `default` chunk.
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
- * Remove a link from `default` label to its chunk.
695
- * It's false route.
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
- * It handles the last case body as broken if `default` chunk
703
- * does not exist.
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
- // Pops the segment context stack until the entry segment.
710
- for (let i = 0; i < context.countForks; ++i) {
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} isEmpty `true` if the body is empty.
724
- * @param {boolean} isDefault `true` if the body is the default case.
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(isEmpty, isDefault) {
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
- * Save `default` chunk info.
746
- * If the `default` label is not at the last, we must make a path from
747
- * the last `case` to the `default` chunk.
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 (isDefault) {
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
- if (isEmpty) {
752
- context.foundDefault = true;
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
- if (!isEmpty && context.foundDefault) {
758
- context.foundDefault = false;
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
- context.lastIsDefault = isDefault;
764
- context.countForks += 1;
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 the `finally`
1610
+ * The following process is executed only when there is a `finally`
811
1611
  * block.
812
1612
  */
813
1613
 
814
- const returned = context.returnedForkContext;
815
- const thrown = context.thrownForkContext;
1614
+ const originalReturnedForkContext = context.returnedForkContext;
1615
+ const originalThrownForkContext = context.thrownForkContext;
816
1616
 
817
- if (returned.empty && thrown.empty) {
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 (!returned.empty) {
1636
+ if (!originalReturnedForkContext.empty) {
830
1637
  getReturnContext(this).returnedForkContext.add(leavingSegments);
831
1638
  }
832
- if (!thrown.empty) {
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 thrown = context.thrownForkContext;
1662
+ const originalThrownForkContext = context.thrownForkContext;
856
1663
 
857
- // Update state.
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
- thrown.add(forkContext.head);
864
- const thrownSegments = thrown.makeNext(0, -1);
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 returned = context.returnedForkContext;
884
- const thrown = context.thrownForkContext;
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
- if (returned.empty && thrown.empty) {
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 < returned.segmentsList.length; ++j) {
916
- prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]);
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 < thrown.segmentsList.length; ++j) {
919
- prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]);
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
- // `true` paths go to looping.
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
- // Go next.
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
- // Update state.
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
- // Creates paths of `continue` statements.
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
- // Update state.
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
- // Update state.
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
- // Update state.
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
- // `update` -> `test`
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 for BreakStatement.
1340
- * @param {boolean} breakable The flag to indicate it can break by
1341
- * an unlabeled BreakStatement.
1342
- * @param {string|null} label The label of this context.
1343
- * @returns {Object} The new context.
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 effects a break also.
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
  ) {