main
js 1,441 lines 41.2 KB
Raw
1 'use strict';
2
3 //------------------------------------------------------------------------------
4 // Requirements
5 //------------------------------------------------------------------------------
6
7 // eslint-disable-next-line
8 const CodePathSegment = require('./code-path-segment');
9 // eslint-disable-next-line
10 const ForkContext = require('./fork-context');
11
12 //------------------------------------------------------------------------------
13 // Helpers
14 //------------------------------------------------------------------------------
15
16 /**
17 * Adds given segments into the `dest` array.
18 * If the `others` array does not includes the given segments, adds to the `all`
19 * array as well.
20 *
21 * This adds only reachable and used segments.
22 * @param {CodePathSegment[]} dest A destination array (`returnedSegments` or `thrownSegments`).
23 * @param {CodePathSegment[]} others Another destination array (`returnedSegments` or `thrownSegments`).
24 * @param {CodePathSegment[]} all The unified destination array (`finalSegments`).
25 * @param {CodePathSegment[]} segments Segments to add.
26 * @returns {void}
27 */
28 function addToReturnedOrThrown(dest, others, all, segments) {
29 for (let i = 0; i < segments.length; ++i) {
30 const segment = segments[i];
31
32 dest.push(segment);
33 if (!others.includes(segment)) {
34 all.push(segment);
35 }
36 }
37 }
38
39 /**
40 * Gets a loop-context for a `continue` statement.
41 * @param {CodePathState} state A state to get.
42 * @param {string} label The label of a `continue` statement.
43 * @returns {LoopContext} A loop-context for a `continue` statement.
44 */
45 function getContinueContext(state, label) {
46 if (!label) {
47 return state.loopContext;
48 }
49
50 let context = state.loopContext;
51
52 while (context) {
53 if (context.label === label) {
54 return context;
55 }
56 context = context.upper;
57 }
58
59 /* c8 ignore next */
60 return null;
61 }
62
63 /**
64 * Gets a context for a `break` statement.
65 * @param {CodePathState} state A state to get.
66 * @param {string} label The label of a `break` statement.
67 * @returns {LoopContext|SwitchContext} A context for a `break` statement.
68 */
69 function getBreakContext(state, label) {
70 let context = state.breakContext;
71
72 while (context) {
73 if (label ? context.label === label : context.breakable) {
74 return context;
75 }
76 context = context.upper;
77 }
78
79 /* c8 ignore next */
80 return null;
81 }
82
83 /**
84 * Gets a context for a `return` statement.
85 * @param {CodePathState} state A state to get.
86 * @returns {TryContext|CodePathState} A context for a `return` statement.
87 */
88 function getReturnContext(state) {
89 let context = state.tryContext;
90
91 while (context) {
92 if (context.hasFinalizer && context.position !== 'finally') {
93 return context;
94 }
95 context = context.upper;
96 }
97
98 return state;
99 }
100
101 /**
102 * Gets a context for a `throw` statement.
103 * @param {CodePathState} state A state to get.
104 * @returns {TryContext|CodePathState} A context for a `throw` statement.
105 */
106 function getThrowContext(state) {
107 let context = state.tryContext;
108
109 while (context) {
110 if (
111 context.position === 'try' ||
112 (context.hasFinalizer && context.position === 'catch')
113 ) {
114 return context;
115 }
116 context = context.upper;
117 }
118
119 return state;
120 }
121
122 /**
123 * Removes a given element from a given array.
124 * @param {any[]} xs An array to remove the specific element.
125 * @param {any} x An element to be removed.
126 * @returns {void}
127 */
128 function remove(xs, x) {
129 xs.splice(xs.indexOf(x), 1);
130 }
131
132 /**
133 * Disconnect given segments.
134 *
135 * This is used in a process for switch statements.
136 * If there is the "default" chunk before other cases, the order is different
137 * between node's and running's.
138 * @param {CodePathSegment[]} prevSegments Forward segments to disconnect.
139 * @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
140 * @returns {void}
141 */
142 function removeConnection(prevSegments, nextSegments) {
143 for (let i = 0; i < prevSegments.length; ++i) {
144 const prevSegment = prevSegments[i];
145 const nextSegment = nextSegments[i];
146
147 remove(prevSegment.nextSegments, nextSegment);
148 remove(prevSegment.allNextSegments, nextSegment);
149 remove(nextSegment.prevSegments, prevSegment);
150 remove(nextSegment.allPrevSegments, prevSegment);
151 }
152 }
153
154 /**
155 * Creates looping path.
156 * @param {CodePathState} state The instance.
157 * @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
158 * @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
159 * @returns {void}
160 */
161 function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
162 const fromSegments = CodePathSegment.flattenUnusedSegments(
163 unflattenedFromSegments,
164 );
165 const toSegments = CodePathSegment.flattenUnusedSegments(
166 unflattenedToSegments,
167 );
168
169 const end = Math.min(fromSegments.length, toSegments.length);
170
171 for (let i = 0; i < end; ++i) {
172 const fromSegment = fromSegments[i];
173 const toSegment = toSegments[i];
174
175 if (toSegment.reachable) {
176 fromSegment.nextSegments.push(toSegment);
177 }
178 if (fromSegment.reachable) {
179 toSegment.prevSegments.push(fromSegment);
180 }
181 fromSegment.allNextSegments.push(toSegment);
182 toSegment.allPrevSegments.push(fromSegment);
183
184 if (toSegment.allPrevSegments.length >= 2) {
185 CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
186 }
187
188 state.notifyLooped(fromSegment, toSegment);
189 }
190 }
191
192 /**
193 * Finalizes segments of `test` chunk of a ForStatement.
194 *
195 * - Adds `false` paths to paths which are leaving from the loop.
196 * - Sets `true` paths to paths which go to the body.
197 * @param {LoopContext} context A loop context to modify.
198 * @param {ChoiceContext} choiceContext A choice context of this loop.
199 * @param {CodePathSegment[]} head The current head paths.
200 * @returns {void}
201 */
202 function finalizeTestSegmentsOfFor(context, choiceContext, head) {
203 if (!choiceContext.processed) {
204 choiceContext.trueForkContext.add(head);
205 choiceContext.falseForkContext.add(head);
206 choiceContext.qqForkContext.add(head);
207 }
208
209 if (context.test !== true) {
210 context.brokenForkContext.addAll(choiceContext.falseForkContext);
211 }
212 context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
213 }
214
215 //------------------------------------------------------------------------------
216 // Public Interface
217 //------------------------------------------------------------------------------
218
219 /**
220 * A class which manages state to analyze code paths.
221 */
222 class CodePathState {
223 /**
224 * @param {IdGenerator} idGenerator An id generator to generate id for code
225 * path segments.
226 * @param {Function} onLooped A callback function to notify looping.
227 */
228 constructor(idGenerator, onLooped) {
229 this.idGenerator = idGenerator;
230 this.notifyLooped = onLooped;
231 this.forkContext = ForkContext.newRoot(idGenerator);
232 this.choiceContext = null;
233 this.switchContext = null;
234 this.tryContext = null;
235 this.loopContext = null;
236 this.breakContext = null;
237 this.chainContext = null;
238
239 this.currentSegments = [];
240 this.initialSegment = this.forkContext.head[0];
241
242 // returnedSegments and thrownSegments push elements into finalSegments also.
243 const final = (this.finalSegments = []);
244 const returned = (this.returnedForkContext = []);
245 const thrown = (this.thrownForkContext = []);
246
247 returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
248 thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
249 }
250
251 /**
252 * The head segments.
253 * @type {CodePathSegment[]}
254 */
255 get headSegments() {
256 return this.forkContext.head;
257 }
258
259 /**
260 * The parent forking context.
261 * This is used for the root of new forks.
262 * @type {ForkContext}
263 */
264 get parentForkContext() {
265 const current = this.forkContext;
266
267 return current && current.upper;
268 }
269
270 /**
271 * Creates and stacks new forking context.
272 * @param {boolean} forkLeavingPath A flag which shows being in a
273 * "finally" block.
274 * @returns {ForkContext} The created context.
275 */
276 pushForkContext(forkLeavingPath) {
277 this.forkContext = ForkContext.newEmpty(this.forkContext, forkLeavingPath);
278
279 return this.forkContext;
280 }
281
282 /**
283 * Pops and merges the last forking context.
284 * @returns {ForkContext} The last context.
285 */
286 popForkContext() {
287 const lastContext = this.forkContext;
288
289 this.forkContext = lastContext.upper;
290 this.forkContext.replaceHead(lastContext.makeNext(0, -1));
291
292 return lastContext;
293 }
294
295 /**
296 * Creates a new path.
297 * @returns {void}
298 */
299 forkPath() {
300 this.forkContext.add(this.parentForkContext.makeNext(-1, -1));
301 }
302
303 /**
304 * Creates a bypass path.
305 * This is used for such as IfStatement which does not have "else" chunk.
306 * @returns {void}
307 */
308 forkBypassPath() {
309 this.forkContext.add(this.parentForkContext.head);
310 }
311
312 //--------------------------------------------------------------------------
313 // ConditionalExpression, LogicalExpression, IfStatement
314 //--------------------------------------------------------------------------
315
316 /**
317 * Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
318 * IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
319 *
320 * LogicalExpressions have cases that it goes different paths between the
321 * `true` case and the `false` case.
322 *
323 * For Example:
324 *
325 * if (a || b) {
326 * foo();
327 * } else {
328 * bar();
329 * }
330 *
331 * In this case, `b` is evaluated always in the code path of the `else`
332 * block, but it's not so in the code path of the `if` block.
333 * So there are 3 paths.
334 *
335 * a -> foo();
336 * a -> b -> foo();
337 * a -> b -> bar();
338 * @param {string} kind A kind string.
339 * If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
340 * If it's IfStatement's or ConditionalExpression's, this is `"test"`.
341 * Otherwise, this is `"loop"`.
342 * @param {boolean} isForkingAsResult A flag that shows that goes different
343 * paths between `true` and `false`.
344 * @returns {void}
345 */
346 pushChoiceContext(kind, isForkingAsResult) {
347 this.choiceContext = {
348 upper: this.choiceContext,
349 kind,
350 isForkingAsResult,
351 trueForkContext: ForkContext.newEmpty(this.forkContext),
352 falseForkContext: ForkContext.newEmpty(this.forkContext),
353 qqForkContext: ForkContext.newEmpty(this.forkContext),
354 processed: false,
355 };
356 }
357
358 /**
359 * Pops the last choice context and finalizes it.
360 * @throws {Error} (Unreachable.)
361 * @returns {ChoiceContext} The popped context.
362 */
363 popChoiceContext() {
364 const context = this.choiceContext;
365
366 this.choiceContext = context.upper;
367
368 const forkContext = this.forkContext;
369 const headSegments = forkContext.head;
370
371 switch (context.kind) {
372 case '&&':
373 case '||':
374 case '??':
375 /*
376 * If any result were not transferred from child contexts,
377 * this sets the head segments to both cases.
378 * The head segments are the path of the right-hand operand.
379 */
380 if (!context.processed) {
381 context.trueForkContext.add(headSegments);
382 context.falseForkContext.add(headSegments);
383 context.qqForkContext.add(headSegments);
384 }
385
386 /*
387 * Transfers results to upper context if this context is in
388 * test chunk.
389 */
390 if (context.isForkingAsResult) {
391 const parentContext = this.choiceContext;
392
393 parentContext.trueForkContext.addAll(context.trueForkContext);
394 parentContext.falseForkContext.addAll(context.falseForkContext);
395 parentContext.qqForkContext.addAll(context.qqForkContext);
396 parentContext.processed = true;
397
398 return context;
399 }
400
401 break;
402
403 case 'test':
404 if (!context.processed) {
405 /*
406 * The head segments are the path of the `if` block here.
407 * Updates the `true` path with the end of the `if` block.
408 */
409 context.trueForkContext.clear();
410 context.trueForkContext.add(headSegments);
411 } else {
412 /*
413 * The head segments are the path of the `else` block here.
414 * Updates the `false` path with the end of the `else`
415 * block.
416 */
417 context.falseForkContext.clear();
418 context.falseForkContext.add(headSegments);
419 }
420
421 break;
422
423 case 'loop':
424 /*
425 * Loops are addressed in popLoopContext().
426 * This is called from popLoopContext().
427 */
428 return context;
429
430 /* c8 ignore next */
431 default:
432 throw new Error('unreachable');
433 }
434
435 // Merges all paths.
436 const prevForkContext = context.trueForkContext;
437
438 prevForkContext.addAll(context.falseForkContext);
439 forkContext.replaceHead(prevForkContext.makeNext(0, -1));
440
441 return context;
442 }
443
444 /**
445 * Makes a code path segment of the right-hand operand of a logical
446 * expression.
447 * @throws {Error} (Unreachable.)
448 * @returns {void}
449 */
450 makeLogicalRight() {
451 const context = this.choiceContext;
452 const forkContext = this.forkContext;
453
454 if (context.processed) {
455 /*
456 * This got segments already from the child choice context.
457 * Creates the next path from own true/false fork context.
458 */
459 let prevForkContext;
460
461 switch (context.kind) {
462 case '&&': // if true then go to the right-hand side.
463 prevForkContext = context.trueForkContext;
464 break;
465 case '||': // if false then go to the right-hand side.
466 prevForkContext = context.falseForkContext;
467 break;
468 case '??': // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext.
469 prevForkContext = context.qqForkContext;
470 break;
471 default:
472 throw new Error('unreachable');
473 }
474
475 forkContext.replaceHead(prevForkContext.makeNext(0, -1));
476 prevForkContext.clear();
477 context.processed = false;
478 } else {
479 /*
480 * This did not get segments from the child choice context.
481 * So addresses the head segments.
482 * The head segments are the path of the left-hand operand.
483 */
484 switch (context.kind) {
485 case '&&': // the false path can short-circuit.
486 context.falseForkContext.add(forkContext.head);
487 break;
488 case '||': // the true path can short-circuit.
489 context.trueForkContext.add(forkContext.head);
490 break;
491 case '??': // both can short-circuit.
492 context.trueForkContext.add(forkContext.head);
493 context.falseForkContext.add(forkContext.head);
494 break;
495 default:
496 throw new Error('unreachable');
497 }
498
499 forkContext.replaceHead(forkContext.makeNext(-1, -1));
500 }
501 }
502
503 /**
504 * Makes a code path segment of the `if` block.
505 * @returns {void}
506 */
507 makeIfConsequent() {
508 const context = this.choiceContext;
509 const forkContext = this.forkContext;
510
511 /*
512 * If any result were not transferred from child contexts,
513 * this sets the head segments to both cases.
514 * The head segments are the path of the test expression.
515 */
516 if (!context.processed) {
517 context.trueForkContext.add(forkContext.head);
518 context.falseForkContext.add(forkContext.head);
519 context.qqForkContext.add(forkContext.head);
520 }
521
522 context.processed = false;
523
524 // Creates new path from the `true` case.
525 forkContext.replaceHead(context.trueForkContext.makeNext(0, -1));
526 }
527
528 /**
529 * Makes a code path segment of the `else` block.
530 * @returns {void}
531 */
532 makeIfAlternate() {
533 const context = this.choiceContext;
534 const forkContext = this.forkContext;
535
536 /*
537 * The head segments are the path of the `if` block.
538 * Updates the `true` path with the end of the `if` block.
539 */
540 context.trueForkContext.clear();
541 context.trueForkContext.add(forkContext.head);
542 context.processed = true;
543
544 // Creates new path from the `false` case.
545 forkContext.replaceHead(context.falseForkContext.makeNext(0, -1));
546 }
547
548 //--------------------------------------------------------------------------
549 // ChainExpression
550 //--------------------------------------------------------------------------
551
552 /**
553 * Push a new `ChainExpression` context to the stack.
554 * This method is called on entering to each `ChainExpression` node.
555 * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
556 * @returns {void}
557 */
558 pushChainContext() {
559 this.chainContext = {
560 upper: this.chainContext,
561 countChoiceContexts: 0,
562 };
563 }
564
565 /**
566 * Pop a `ChainExpression` context from the stack.
567 * This method is called on exiting from each `ChainExpression` node.
568 * This merges all forks of the last optional chaining.
569 * @returns {void}
570 */
571 popChainContext() {
572 const context = this.chainContext;
573
574 this.chainContext = context.upper;
575
576 // pop all choice contexts of this.
577 for (let i = context.countChoiceContexts; i > 0; --i) {
578 this.popChoiceContext();
579 }
580 }
581
582 /**
583 * Create a choice context for optional access.
584 * This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
585 * This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
586 * @returns {void}
587 */
588 makeOptionalNode() {
589 if (this.chainContext) {
590 this.chainContext.countChoiceContexts += 1;
591 this.pushChoiceContext('??', false);
592 }
593 }
594
595 /**
596 * Create a fork.
597 * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
598 * @returns {void}
599 */
600 makeOptionalRight() {
601 if (this.chainContext) {
602 this.makeLogicalRight();
603 }
604 }
605
606 //--------------------------------------------------------------------------
607 // SwitchStatement
608 //--------------------------------------------------------------------------
609
610 /**
611 * Creates a context object of SwitchStatement and stacks it.
612 * @param {boolean} hasCase `true` if the switch statement has one or more
613 * case parts.
614 * @param {string|null} label The label text.
615 * @returns {void}
616 */
617 pushSwitchContext(hasCase, label) {
618 this.switchContext = {
619 upper: this.switchContext,
620 hasCase,
621 defaultSegments: null,
622 defaultBodySegments: null,
623 foundDefault: false,
624 lastIsDefault: false,
625 countForks: 0,
626 };
627
628 this.pushBreakContext(true, label);
629 }
630
631 /**
632 * Pops the last context of SwitchStatement and finalizes it.
633 *
634 * - Disposes all forking stack for `case` and `default`.
635 * - Creates the next code path segment from `context.brokenForkContext`.
636 * - If the last `SwitchCase` node is not a `default` part, creates a path
637 * to the `default` body.
638 * @returns {void}
639 */
640 popSwitchContext() {
641 const context = this.switchContext;
642
643 this.switchContext = context.upper;
644
645 const forkContext = this.forkContext;
646 const brokenForkContext = this.popBreakContext().brokenForkContext;
647
648 if (context.countForks === 0) {
649 /*
650 * When there is only one `default` chunk and there is one or more
651 * `break` statements, even if forks are nothing, it needs to merge
652 * those.
653 */
654 if (!brokenForkContext.empty) {
655 brokenForkContext.add(forkContext.makeNext(-1, -1));
656 forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
657 }
658
659 return;
660 }
661
662 const lastSegments = forkContext.head;
663
664 this.forkBypassPath();
665 const lastCaseSegments = forkContext.head;
666
667 /*
668 * `brokenForkContext` is used to make the next segment.
669 * It must add the last segment into `brokenForkContext`.
670 */
671 brokenForkContext.add(lastSegments);
672
673 /*
674 * A path which is failed in all case test should be connected to path
675 * of `default` chunk.
676 */
677 if (!context.lastIsDefault) {
678 if (context.defaultBodySegments) {
679 /*
680 * Remove a link from `default` label to its chunk.
681 * It's false route.
682 */
683 removeConnection(context.defaultSegments, context.defaultBodySegments);
684 makeLooped(this, lastCaseSegments, context.defaultBodySegments);
685 } else {
686 /*
687 * It handles the last case body as broken if `default` chunk
688 * does not exist.
689 */
690 brokenForkContext.add(lastCaseSegments);
691 }
692 }
693
694 // Pops the segment context stack until the entry segment.
695 for (let i = 0; i < context.countForks; ++i) {
696 this.forkContext = this.forkContext.upper;
697 }
698
699 /*
700 * Creates a path from all brokenForkContext paths.
701 * This is a path after switch statement.
702 */
703 this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
704 }
705
706 /**
707 * Makes a code path segment for a `SwitchCase` node.
708 * @param {boolean} isEmpty `true` if the body is empty.
709 * @param {boolean} isDefault `true` if the body is the default case.
710 * @returns {void}
711 */
712 makeSwitchCaseBody(isEmpty, isDefault) {
713 const context = this.switchContext;
714
715 if (!context.hasCase) {
716 return;
717 }
718
719 /*
720 * Merge forks.
721 * The parent fork context has two segments.
722 * Those are from the current case and the body of the previous case.
723 */
724 const parentForkContext = this.forkContext;
725 const forkContext = this.pushForkContext();
726
727 forkContext.add(parentForkContext.makeNext(0, -1));
728
729 /*
730 * Save `default` chunk info.
731 * If the `default` label is not at the last, we must make a path from
732 * the last `case` to the `default` chunk.
733 */
734 if (isDefault) {
735 context.defaultSegments = parentForkContext.head;
736 if (isEmpty) {
737 context.foundDefault = true;
738 } else {
739 context.defaultBodySegments = forkContext.head;
740 }
741 } else {
742 if (!isEmpty && context.foundDefault) {
743 context.foundDefault = false;
744 context.defaultBodySegments = forkContext.head;
745 }
746 }
747
748 context.lastIsDefault = isDefault;
749 context.countForks += 1;
750 }
751
752 //--------------------------------------------------------------------------
753 // TryStatement
754 //--------------------------------------------------------------------------
755
756 /**
757 * Creates a context object of TryStatement and stacks it.
758 * @param {boolean} hasFinalizer `true` if the try statement has a
759 * `finally` block.
760 * @returns {void}
761 */
762 pushTryContext(hasFinalizer) {
763 this.tryContext = {
764 upper: this.tryContext,
765 position: 'try',
766 hasFinalizer,
767
768 returnedForkContext: hasFinalizer
769 ? ForkContext.newEmpty(this.forkContext)
770 : null,
771
772 thrownForkContext: ForkContext.newEmpty(this.forkContext),
773 lastOfTryIsReachable: false,
774 lastOfCatchIsReachable: false,
775 };
776 }
777
778 /**
779 * Pops the last context of TryStatement and finalizes it.
780 * @returns {void}
781 */
782 popTryContext() {
783 const context = this.tryContext;
784
785 this.tryContext = context.upper;
786
787 if (context.position === 'catch') {
788 // Merges two paths from the `try` block and `catch` block merely.
789 this.popForkContext();
790 return;
791 }
792
793 /*
794 * The following process is executed only when there is the `finally`
795 * block.
796 */
797
798 const returned = context.returnedForkContext;
799 const thrown = context.thrownForkContext;
800
801 if (returned.empty && thrown.empty) {
802 return;
803 }
804
805 // Separate head to normal paths and leaving paths.
806 const headSegments = this.forkContext.head;
807
808 this.forkContext = this.forkContext.upper;
809 const normalSegments = headSegments.slice(0, (headSegments.length / 2) | 0);
810 const leavingSegments = headSegments.slice((headSegments.length / 2) | 0);
811
812 // Forwards the leaving path to upper contexts.
813 if (!returned.empty) {
814 getReturnContext(this).returnedForkContext.add(leavingSegments);
815 }
816 if (!thrown.empty) {
817 getThrowContext(this).thrownForkContext.add(leavingSegments);
818 }
819
820 // Sets the normal path as the next.
821 this.forkContext.replaceHead(normalSegments);
822
823 /*
824 * If both paths of the `try` block and the `catch` block are
825 * unreachable, the next path becomes unreachable as well.
826 */
827 if (!context.lastOfTryIsReachable && !context.lastOfCatchIsReachable) {
828 this.forkContext.makeUnreachable();
829 }
830 }
831
832 /**
833 * Makes a code path segment for a `catch` block.
834 * @returns {void}
835 */
836 makeCatchBlock() {
837 const context = this.tryContext;
838 const forkContext = this.forkContext;
839 const thrown = context.thrownForkContext;
840
841 // Update state.
842 context.position = 'catch';
843 context.thrownForkContext = ForkContext.newEmpty(forkContext);
844 context.lastOfTryIsReachable = forkContext.reachable;
845
846 // Merge thrown paths.
847 thrown.add(forkContext.head);
848 const thrownSegments = thrown.makeNext(0, -1);
849
850 // Fork to a bypass and the merged thrown path.
851 this.pushForkContext();
852 this.forkBypassPath();
853 this.forkContext.add(thrownSegments);
854 }
855
856 /**
857 * Makes a code path segment for a `finally` block.
858 *
859 * In the `finally` block, parallel paths are created. The parallel paths
860 * are used as leaving-paths. The leaving-paths are paths from `return`
861 * statements and `throw` statements in a `try` block or a `catch` block.
862 * @returns {void}
863 */
864 makeFinallyBlock() {
865 const context = this.tryContext;
866 let forkContext = this.forkContext;
867 const returned = context.returnedForkContext;
868 const thrown = context.thrownForkContext;
869 const headOfLeavingSegments = forkContext.head;
870
871 // Update state.
872 if (context.position === 'catch') {
873 // Merges two paths from the `try` block and `catch` block.
874 this.popForkContext();
875 forkContext = this.forkContext;
876
877 context.lastOfCatchIsReachable = forkContext.reachable;
878 } else {
879 context.lastOfTryIsReachable = forkContext.reachable;
880 }
881 context.position = 'finally';
882
883 if (returned.empty && thrown.empty) {
884 // This path does not leave.
885 return;
886 }
887
888 /*
889 * Create a parallel segment from merging returned and thrown.
890 * This segment will leave at the end of this finally block.
891 */
892 const segments = forkContext.makeNext(-1, -1);
893
894 for (let i = 0; i < forkContext.count; ++i) {
895 const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];
896
897 for (let j = 0; j < returned.segmentsList.length; ++j) {
898 prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]);
899 }
900 for (let j = 0; j < thrown.segmentsList.length; ++j) {
901 prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]);
902 }
903
904 segments.push(
905 CodePathSegment.newNext(
906 this.idGenerator.next(),
907 prevSegsOfLeavingSegment,
908 ),
909 );
910 }
911
912 this.pushForkContext(true);
913 this.forkContext.add(segments);
914 }
915
916 /**
917 * Makes a code path segment from the first throwable node to the `catch`
918 * block or the `finally` block.
919 * @returns {void}
920 */
921 makeFirstThrowablePathInTryBlock() {
922 const forkContext = this.forkContext;
923
924 if (!forkContext.reachable) {
925 return;
926 }
927
928 const context = getThrowContext(this);
929
930 if (
931 context === this ||
932 context.position !== 'try' ||
933 !context.thrownForkContext.empty
934 ) {
935 return;
936 }
937
938 context.thrownForkContext.add(forkContext.head);
939 forkContext.replaceHead(forkContext.makeNext(-1, -1));
940 }
941
942 //--------------------------------------------------------------------------
943 // Loop Statements
944 //--------------------------------------------------------------------------
945
946 /**
947 * Creates a context object of a loop statement and stacks it.
948 * @param {string} type The type of the node which was triggered. One of
949 * `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`,
950 * and `ForStatement`.
951 * @param {string|null} label A label of the node which was triggered.
952 * @throws {Error} (Unreachable - unknown type.)
953 * @returns {void}
954 */
955 pushLoopContext(type, label) {
956 const forkContext = this.forkContext;
957 const breakContext = this.pushBreakContext(true, label);
958
959 switch (type) {
960 case 'WhileStatement':
961 this.pushChoiceContext('loop', false);
962 this.loopContext = {
963 upper: this.loopContext,
964 type,
965 label,
966 test: void 0,
967 continueDestSegments: null,
968 brokenForkContext: breakContext.brokenForkContext,
969 };
970 break;
971
972 case 'DoWhileStatement':
973 this.pushChoiceContext('loop', false);
974 this.loopContext = {
975 upper: this.loopContext,
976 type,
977 label,
978 test: void 0,
979 entrySegments: null,
980 continueForkContext: ForkContext.newEmpty(forkContext),
981 brokenForkContext: breakContext.brokenForkContext,
982 };
983 break;
984
985 case 'ForStatement':
986 this.pushChoiceContext('loop', false);
987 this.loopContext = {
988 upper: this.loopContext,
989 type,
990 label,
991 test: void 0,
992 endOfInitSegments: null,
993 testSegments: null,
994 endOfTestSegments: null,
995 updateSegments: null,
996 endOfUpdateSegments: null,
997 continueDestSegments: null,
998 brokenForkContext: breakContext.brokenForkContext,
999 };
1000 break;
1001
1002 case 'ForInStatement':
1003 case 'ForOfStatement':
1004 this.loopContext = {
1005 upper: this.loopContext,
1006 type,
1007 label,
1008 prevSegments: null,
1009 leftSegments: null,
1010 endOfLeftSegments: null,
1011 continueDestSegments: null,
1012 brokenForkContext: breakContext.brokenForkContext,
1013 };
1014 break;
1015
1016 /* c8 ignore next */
1017 default:
1018 throw new Error(`unknown type: "${type}"`);
1019 }
1020 }
1021
1022 /**
1023 * Pops the last context of a loop statement and finalizes it.
1024 * @throws {Error} (Unreachable - unknown type.)
1025 * @returns {void}
1026 */
1027 popLoopContext() {
1028 const context = this.loopContext;
1029
1030 this.loopContext = context.upper;
1031
1032 const forkContext = this.forkContext;
1033 const brokenForkContext = this.popBreakContext().brokenForkContext;
1034
1035 // Creates a looped path.
1036 switch (context.type) {
1037 case 'WhileStatement':
1038 case 'ForStatement':
1039 this.popChoiceContext();
1040 makeLooped(this, forkContext.head, context.continueDestSegments);
1041 break;
1042
1043 case 'DoWhileStatement': {
1044 const choiceContext = this.popChoiceContext();
1045
1046 if (!choiceContext.processed) {
1047 choiceContext.trueForkContext.add(forkContext.head);
1048 choiceContext.falseForkContext.add(forkContext.head);
1049 }
1050 if (context.test !== true) {
1051 brokenForkContext.addAll(choiceContext.falseForkContext);
1052 }
1053
1054 // `true` paths go to looping.
1055 const segmentsList = choiceContext.trueForkContext.segmentsList;
1056
1057 for (let i = 0; i < segmentsList.length; ++i) {
1058 makeLooped(this, segmentsList[i], context.entrySegments);
1059 }
1060 break;
1061 }
1062
1063 case 'ForInStatement':
1064 case 'ForOfStatement':
1065 brokenForkContext.add(forkContext.head);
1066 makeLooped(this, forkContext.head, context.leftSegments);
1067 break;
1068
1069 /* c8 ignore next */
1070 default:
1071 throw new Error('unreachable');
1072 }
1073
1074 // Go next.
1075 if (brokenForkContext.empty) {
1076 forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1077 } else {
1078 forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
1079 }
1080 }
1081
1082 /**
1083 * Makes a code path segment for the test part of a WhileStatement.
1084 * @param {boolean|undefined} test The test value (only when constant).
1085 * @returns {void}
1086 */
1087 makeWhileTest(test) {
1088 const context = this.loopContext;
1089 const forkContext = this.forkContext;
1090 const testSegments = forkContext.makeNext(0, -1);
1091
1092 // Update state.
1093 context.test = test;
1094 context.continueDestSegments = testSegments;
1095 forkContext.replaceHead(testSegments);
1096 }
1097
1098 /**
1099 * Makes a code path segment for the body part of a WhileStatement.
1100 * @returns {void}
1101 */
1102 makeWhileBody() {
1103 const context = this.loopContext;
1104 const choiceContext = this.choiceContext;
1105 const forkContext = this.forkContext;
1106
1107 if (!choiceContext.processed) {
1108 choiceContext.trueForkContext.add(forkContext.head);
1109 choiceContext.falseForkContext.add(forkContext.head);
1110 }
1111
1112 // Update state.
1113 if (context.test !== true) {
1114 context.brokenForkContext.addAll(choiceContext.falseForkContext);
1115 }
1116 forkContext.replaceHead(choiceContext.trueForkContext.makeNext(0, -1));
1117 }
1118
1119 /**
1120 * Makes a code path segment for the body part of a DoWhileStatement.
1121 * @returns {void}
1122 */
1123 makeDoWhileBody() {
1124 const context = this.loopContext;
1125 const forkContext = this.forkContext;
1126 const bodySegments = forkContext.makeNext(-1, -1);
1127
1128 // Update state.
1129 context.entrySegments = bodySegments;
1130 forkContext.replaceHead(bodySegments);
1131 }
1132
1133 /**
1134 * Makes a code path segment for the test part of a DoWhileStatement.
1135 * @param {boolean|undefined} test The test value (only when constant).
1136 * @returns {void}
1137 */
1138 makeDoWhileTest(test) {
1139 const context = this.loopContext;
1140 const forkContext = this.forkContext;
1141
1142 context.test = test;
1143
1144 // Creates paths of `continue` statements.
1145 if (!context.continueForkContext.empty) {
1146 context.continueForkContext.add(forkContext.head);
1147 const testSegments = context.continueForkContext.makeNext(0, -1);
1148
1149 forkContext.replaceHead(testSegments);
1150 }
1151 }
1152
1153 /**
1154 * Makes a code path segment for the test part of a ForStatement.
1155 * @param {boolean|undefined} test The test value (only when constant).
1156 * @returns {void}
1157 */
1158 makeForTest(test) {
1159 const context = this.loopContext;
1160 const forkContext = this.forkContext;
1161 const endOfInitSegments = forkContext.head;
1162 const testSegments = forkContext.makeNext(-1, -1);
1163
1164 // Update state.
1165 context.test = test;
1166 context.endOfInitSegments = endOfInitSegments;
1167 context.continueDestSegments = context.testSegments = testSegments;
1168 forkContext.replaceHead(testSegments);
1169 }
1170
1171 /**
1172 * Makes a code path segment for the update part of a ForStatement.
1173 * @returns {void}
1174 */
1175 makeForUpdate() {
1176 const context = this.loopContext;
1177 const choiceContext = this.choiceContext;
1178 const forkContext = this.forkContext;
1179
1180 // Make the next paths of the test.
1181 if (context.testSegments) {
1182 finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
1183 } else {
1184 context.endOfInitSegments = forkContext.head;
1185 }
1186
1187 // Update state.
1188 const updateSegments = forkContext.makeDisconnected(-1, -1);
1189
1190 context.continueDestSegments = context.updateSegments = updateSegments;
1191 forkContext.replaceHead(updateSegments);
1192 }
1193
1194 /**
1195 * Makes a code path segment for the body part of a ForStatement.
1196 * @returns {void}
1197 */
1198 makeForBody() {
1199 const context = this.loopContext;
1200 const choiceContext = this.choiceContext;
1201 const forkContext = this.forkContext;
1202
1203 // Update state.
1204 if (context.updateSegments) {
1205 context.endOfUpdateSegments = forkContext.head;
1206
1207 // `update` -> `test`
1208 if (context.testSegments) {
1209 makeLooped(this, context.endOfUpdateSegments, context.testSegments);
1210 }
1211 } else if (context.testSegments) {
1212 finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
1213 } else {
1214 context.endOfInitSegments = forkContext.head;
1215 }
1216
1217 let bodySegments = context.endOfTestSegments;
1218
1219 if (!bodySegments) {
1220 /*
1221 * If there is not the `test` part, the `body` path comes from the
1222 * `init` part and the `update` part.
1223 */
1224 const prevForkContext = ForkContext.newEmpty(forkContext);
1225
1226 prevForkContext.add(context.endOfInitSegments);
1227 if (context.endOfUpdateSegments) {
1228 prevForkContext.add(context.endOfUpdateSegments);
1229 }
1230
1231 bodySegments = prevForkContext.makeNext(0, -1);
1232 }
1233 context.continueDestSegments = context.continueDestSegments || bodySegments;
1234 forkContext.replaceHead(bodySegments);
1235 }
1236
1237 /**
1238 * Makes a code path segment for the left part of a ForInStatement and a
1239 * ForOfStatement.
1240 * @returns {void}
1241 */
1242 makeForInOfLeft() {
1243 const context = this.loopContext;
1244 const forkContext = this.forkContext;
1245 const leftSegments = forkContext.makeDisconnected(-1, -1);
1246
1247 // Update state.
1248 context.prevSegments = forkContext.head;
1249 context.leftSegments = context.continueDestSegments = leftSegments;
1250 forkContext.replaceHead(leftSegments);
1251 }
1252
1253 /**
1254 * Makes a code path segment for the right part of a ForInStatement and a
1255 * ForOfStatement.
1256 * @returns {void}
1257 */
1258 makeForInOfRight() {
1259 const context = this.loopContext;
1260 const forkContext = this.forkContext;
1261 const temp = ForkContext.newEmpty(forkContext);
1262
1263 temp.add(context.prevSegments);
1264 const rightSegments = temp.makeNext(-1, -1);
1265
1266 // Update state.
1267 context.endOfLeftSegments = forkContext.head;
1268 forkContext.replaceHead(rightSegments);
1269 }
1270
1271 /**
1272 * Makes a code path segment for the body part of a ForInStatement and a
1273 * ForOfStatement.
1274 * @returns {void}
1275 */
1276 makeForInOfBody() {
1277 const context = this.loopContext;
1278 const forkContext = this.forkContext;
1279 const temp = ForkContext.newEmpty(forkContext);
1280
1281 temp.add(context.endOfLeftSegments);
1282 const bodySegments = temp.makeNext(-1, -1);
1283
1284 // Make a path: `right` -> `left`.
1285 makeLooped(this, forkContext.head, context.leftSegments);
1286
1287 // Update state.
1288 context.brokenForkContext.add(forkContext.head);
1289 forkContext.replaceHead(bodySegments);
1290 }
1291
1292 //--------------------------------------------------------------------------
1293 // Control Statements
1294 //--------------------------------------------------------------------------
1295
1296 /**
1297 * Creates new context for BreakStatement.
1298 * @param {boolean} breakable The flag to indicate it can break by
1299 * an unlabeled BreakStatement.
1300 * @param {string|null} label The label of this context.
1301 * @returns {Object} The new context.
1302 */
1303 pushBreakContext(breakable, label) {
1304 this.breakContext = {
1305 upper: this.breakContext,
1306 breakable,
1307 label,
1308 brokenForkContext: ForkContext.newEmpty(this.forkContext),
1309 };
1310 return this.breakContext;
1311 }
1312
1313 /**
1314 * Removes the top item of the break context stack.
1315 * @returns {Object} The removed context.
1316 */
1317 popBreakContext() {
1318 const context = this.breakContext;
1319 const forkContext = this.forkContext;
1320
1321 this.breakContext = context.upper;
1322
1323 // Process this context here for other than switches and loops.
1324 if (!context.breakable) {
1325 const brokenForkContext = context.brokenForkContext;
1326
1327 if (!brokenForkContext.empty) {
1328 brokenForkContext.add(forkContext.head);
1329 forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
1330 }
1331 }
1332
1333 return context;
1334 }
1335
1336 /**
1337 * Makes a path for a `break` statement.
1338 *
1339 * It registers the head segment to a context of `break`.
1340 * It makes new unreachable segment, then it set the head with the segment.
1341 * @param {string} label A label of the break statement.
1342 * @returns {void}
1343 */
1344 makeBreak(label) {
1345 const forkContext = this.forkContext;
1346
1347 if (!forkContext.reachable) {
1348 return;
1349 }
1350
1351 const context = getBreakContext(this, label);
1352
1353 if (context) {
1354 context.brokenForkContext.add(forkContext.head);
1355 }
1356
1357 /* c8 ignore next */
1358 forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1359 }
1360
1361 /**
1362 * Makes a path for a `continue` statement.
1363 *
1364 * It makes a looping path.
1365 * It makes new unreachable segment, then it set the head with the segment.
1366 * @param {string} label A label of the continue statement.
1367 * @returns {void}
1368 */
1369 makeContinue(label) {
1370 const forkContext = this.forkContext;
1371
1372 if (!forkContext.reachable) {
1373 return;
1374 }
1375
1376 const context = getContinueContext(this, label);
1377
1378 if (context) {
1379 if (context.continueDestSegments) {
1380 makeLooped(this, forkContext.head, context.continueDestSegments);
1381
1382 // If the context is a for-in/of loop, this effects a break also.
1383 if (
1384 context.type === 'ForInStatement' ||
1385 context.type === 'ForOfStatement'
1386 ) {
1387 context.brokenForkContext.add(forkContext.head);
1388 }
1389 } else {
1390 context.continueForkContext.add(forkContext.head);
1391 }
1392 }
1393 forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1394 }
1395
1396 /**
1397 * Makes a path for a `return` statement.
1398 *
1399 * It registers the head segment to a context of `return`.
1400 * It makes new unreachable segment, then it set the head with the segment.
1401 * @returns {void}
1402 */
1403 makeReturn() {
1404 const forkContext = this.forkContext;
1405
1406 if (forkContext.reachable) {
1407 getReturnContext(this).returnedForkContext.add(forkContext.head);
1408 forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1409 }
1410 }
1411
1412 /**
1413 * Makes a path for a `throw` statement.
1414 *
1415 * It registers the head segment to a context of `throw`.
1416 * It makes new unreachable segment, then it set the head with the segment.
1417 * @returns {void}
1418 */
1419 makeThrow() {
1420 const forkContext = this.forkContext;
1421
1422 if (forkContext.reachable) {
1423 getThrowContext(this).thrownForkContext.add(forkContext.head);
1424 forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1425 }
1426 }
1427
1428 /**
1429 * Makes the final path.
1430 * @returns {void}
1431 */
1432 makeFinal() {
1433 const segments = this.currentSegments;
1434
1435 if (segments.length > 0 && segments[0].reachable) {
1436 this.returnedForkContext.add(segments);
1437 }
1438 }
1439 }
1440
1441 module.exports = CodePathState;