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