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