UNPKG

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