1/** 2 * @fileoverview A class of the code path analyzer. 3 * @author Toru Nagashima 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const assert = require("assert"), 13 { breakableTypePattern } = require("../../shared/ast-utils"), 14 CodePath = require("./code-path"), 15 CodePathSegment = require("./code-path-segment"), 16 IdGenerator = require("./id-generator"), 17 debug = require("./debug-helpers"); 18 19//------------------------------------------------------------------------------ 20// Helpers 21//------------------------------------------------------------------------------ 22 23/** 24 * Checks whether or not a given node is a `case` node (not `default` node). 25 * @param {ASTNode} node A `SwitchCase` node to check. 26 * @returns {boolean} `true` if the node is a `case` node (not `default` node). 27 */ 28function isCaseNode(node) { 29 return Boolean(node.test); 30} 31 32/** 33 * Checks whether the given logical operator is taken into account for the code 34 * path analysis. 35 * @param {string} operator The operator found in the LogicalExpression node 36 * @returns {boolean} `true` if the operator is "&&" or "||" or "??" 37 */ 38function isHandledLogicalOperator(operator) { 39 return operator === "&&" || operator === "||" || operator === "??"; 40} 41 42/** 43 * Checks whether the given assignment operator is a logical assignment operator. 44 * Logical assignments are taken into account for the code path analysis 45 * because of their short-circuiting semantics. 46 * @param {string} operator The operator found in the AssignmentExpression node 47 * @returns {boolean} `true` if the operator is "&&=" or "||=" or "??=" 48 */ 49function isLogicalAssignmentOperator(operator) { 50 return operator === "&&=" || operator === "||=" || operator === "??="; 51} 52 53/** 54 * Gets the label if the parent node of a given node is a LabeledStatement. 55 * @param {ASTNode} node A node to get. 56 * @returns {string|null} The label or `null`. 57 */ 58function getLabel(node) { 59 if (node.parent.type === "LabeledStatement") { 60 return node.parent.label.name; 61 } 62 return null; 63} 64 65/** 66 * Checks whether or not a given logical expression node goes different path 67 * between the `true` case and the `false` case. 68 * @param {ASTNode} node A node to check. 69 * @returns {boolean} `true` if the node is a test of a choice statement. 70 */ 71function isForkingByTrueOrFalse(node) { 72 const parent = node.parent; 73 74 switch (parent.type) { 75 case "ConditionalExpression": 76 case "IfStatement": 77 case "WhileStatement": 78 case "DoWhileStatement": 79 case "ForStatement": 80 return parent.test === node; 81 82 case "LogicalExpression": 83 return isHandledLogicalOperator(parent.operator); 84 85 case "AssignmentExpression": 86 return isLogicalAssignmentOperator(parent.operator); 87 88 default: 89 return false; 90 } 91} 92 93/** 94 * Gets the boolean value of a given literal node. 95 * 96 * This is used to detect infinity loops (e.g. `while (true) {}`). 97 * Statements preceded by an infinity loop are unreachable if the loop didn't 98 * have any `break` statement. 99 * @param {ASTNode} node A node to get. 100 * @returns {boolean|undefined} a boolean value if the node is a Literal node, 101 * otherwise `undefined`. 102 */ 103function getBooleanValueIfSimpleConstant(node) { 104 if (node.type === "Literal") { 105 return Boolean(node.value); 106 } 107 return void 0; 108} 109 110/** 111 * Checks that a given identifier node is a reference or not. 112 * 113 * This is used to detect the first throwable node in a `try` block. 114 * @param {ASTNode} node An Identifier node to check. 115 * @returns {boolean} `true` if the node is a reference. 116 */ 117function isIdentifierReference(node) { 118 const parent = node.parent; 119 120 switch (parent.type) { 121 case "LabeledStatement": 122 case "BreakStatement": 123 case "ContinueStatement": 124 case "ArrayPattern": 125 case "RestElement": 126 case "ImportSpecifier": 127 case "ImportDefaultSpecifier": 128 case "ImportNamespaceSpecifier": 129 case "CatchClause": 130 return false; 131 132 case "FunctionDeclaration": 133 case "FunctionExpression": 134 case "ArrowFunctionExpression": 135 case "ClassDeclaration": 136 case "ClassExpression": 137 case "VariableDeclarator": 138 return parent.id !== node; 139 140 case "Property": 141 case "MethodDefinition": 142 return ( 143 parent.key !== node || 144 parent.computed || 145 parent.shorthand 146 ); 147 148 case "AssignmentPattern": 149 return parent.key !== node; 150 151 default: 152 return true; 153 } 154} 155 156/** 157 * Updates the current segment with the head segment. 158 * This is similar to local branches and tracking branches of git. 159 * 160 * To separate the current and the head is in order to not make useless segments. 161 * 162 * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd" 163 * events are fired. 164 * @param {CodePathAnalyzer} analyzer The instance. 165 * @param {ASTNode} node The current AST node. 166 * @returns {void} 167 */ 168function forwardCurrentToHead(analyzer, node) { 169 const codePath = analyzer.codePath; 170 const state = CodePath.getState(codePath); 171 const currentSegments = state.currentSegments; 172 const headSegments = state.headSegments; 173 const end = Math.max(currentSegments.length, headSegments.length); 174 let i, currentSegment, headSegment; 175 176 // Fires leaving events. 177 for (i = 0; i < end; ++i) { 178 currentSegment = currentSegments[i]; 179 headSegment = headSegments[i]; 180 181 if (currentSegment !== headSegment && currentSegment) { 182 debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`); 183 184 if (currentSegment.reachable) { 185 analyzer.emitter.emit( 186 "onCodePathSegmentEnd", 187 currentSegment, 188 node 189 ); 190 } 191 } 192 } 193 194 // Update state. 195 state.currentSegments = headSegments; 196 197 // Fires entering events. 198 for (i = 0; i < end; ++i) { 199 currentSegment = currentSegments[i]; 200 headSegment = headSegments[i]; 201 202 if (currentSegment !== headSegment && headSegment) { 203 debug.dump(`onCodePathSegmentStart ${headSegment.id}`); 204 205 CodePathSegment.markUsed(headSegment); 206 if (headSegment.reachable) { 207 analyzer.emitter.emit( 208 "onCodePathSegmentStart", 209 headSegment, 210 node 211 ); 212 } 213 } 214 } 215 216} 217 218/** 219 * Updates the current segment with empty. 220 * This is called at the last of functions or the program. 221 * @param {CodePathAnalyzer} analyzer The instance. 222 * @param {ASTNode} node The current AST node. 223 * @returns {void} 224 */ 225function leaveFromCurrentSegment(analyzer, node) { 226 const state = CodePath.getState(analyzer.codePath); 227 const currentSegments = state.currentSegments; 228 229 for (let i = 0; i < currentSegments.length; ++i) { 230 const currentSegment = currentSegments[i]; 231 232 debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`); 233 if (currentSegment.reachable) { 234 analyzer.emitter.emit( 235 "onCodePathSegmentEnd", 236 currentSegment, 237 node 238 ); 239 } 240 } 241 242 state.currentSegments = []; 243} 244 245/** 246 * Updates the code path due to the position of a given node in the parent node 247 * thereof. 248 * 249 * For example, if the node is `parent.consequent`, this creates a fork from the 250 * current path. 251 * @param {CodePathAnalyzer} analyzer The instance. 252 * @param {ASTNode} node The current AST node. 253 * @returns {void} 254 */ 255function preprocess(analyzer, node) { 256 const codePath = analyzer.codePath; 257 const state = CodePath.getState(codePath); 258 const parent = node.parent; 259 260 switch (parent.type) { 261 262 // The `arguments.length == 0` case is in `postprocess` function. 263 case "CallExpression": 264 if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) { 265 state.makeOptionalRight(); 266 } 267 break; 268 case "MemberExpression": 269 if (parent.optional === true && parent.property === node) { 270 state.makeOptionalRight(); 271 } 272 break; 273 274 case "LogicalExpression": 275 if ( 276 parent.right === node && 277 isHandledLogicalOperator(parent.operator) 278 ) { 279 state.makeLogicalRight(); 280 } 281 break; 282 283 case "AssignmentExpression": 284 if ( 285 parent.right === node && 286 isLogicalAssignmentOperator(parent.operator) 287 ) { 288 state.makeLogicalRight(); 289 } 290 break; 291 292 case "ConditionalExpression": 293 case "IfStatement": 294 295 /* 296 * Fork if this node is at `consequent`/`alternate`. 297 * `popForkContext()` exists at `IfStatement:exit` and 298 * `ConditionalExpression:exit`. 299 */ 300 if (parent.consequent === node) { 301 state.makeIfConsequent(); 302 } else if (parent.alternate === node) { 303 state.makeIfAlternate(); 304 } 305 break; 306 307 case "SwitchCase": 308 if (parent.consequent[0] === node) { 309 state.makeSwitchCaseBody(false, !parent.test); 310 } 311 break; 312 313 case "TryStatement": 314 if (parent.handler === node) { 315 state.makeCatchBlock(); 316 } else if (parent.finalizer === node) { 317 state.makeFinallyBlock(); 318 } 319 break; 320 321 case "WhileStatement": 322 if (parent.test === node) { 323 state.makeWhileTest(getBooleanValueIfSimpleConstant(node)); 324 } else { 325 assert(parent.body === node); 326 state.makeWhileBody(); 327 } 328 break; 329 330 case "DoWhileStatement": 331 if (parent.body === node) { 332 state.makeDoWhileBody(); 333 } else { 334 assert(parent.test === node); 335 state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node)); 336 } 337 break; 338 339 case "ForStatement": 340 if (parent.test === node) { 341 state.makeForTest(getBooleanValueIfSimpleConstant(node)); 342 } else if (parent.update === node) { 343 state.makeForUpdate(); 344 } else if (parent.body === node) { 345 state.makeForBody(); 346 } 347 break; 348 349 case "ForInStatement": 350 case "ForOfStatement": 351 if (parent.left === node) { 352 state.makeForInOfLeft(); 353 } else if (parent.right === node) { 354 state.makeForInOfRight(); 355 } else { 356 assert(parent.body === node); 357 state.makeForInOfBody(); 358 } 359 break; 360 361 case "AssignmentPattern": 362 363 /* 364 * Fork if this node is at `right`. 365 * `left` is executed always, so it uses the current path. 366 * `popForkContext()` exists at `AssignmentPattern:exit`. 367 */ 368 if (parent.right === node) { 369 state.pushForkContext(); 370 state.forkBypassPath(); 371 state.forkPath(); 372 } 373 break; 374 375 default: 376 break; 377 } 378} 379 380/** 381 * Updates the code path due to the type of a given node in entering. 382 * @param {CodePathAnalyzer} analyzer The instance. 383 * @param {ASTNode} node The current AST node. 384 * @returns {void} 385 */ 386function processCodePathToEnter(analyzer, node) { 387 let codePath = analyzer.codePath; 388 let state = codePath && CodePath.getState(codePath); 389 const parent = node.parent; 390 391 switch (node.type) { 392 case "Program": 393 case "FunctionDeclaration": 394 case "FunctionExpression": 395 case "ArrowFunctionExpression": 396 if (codePath) { 397 398 // Emits onCodePathSegmentStart events if updated. 399 forwardCurrentToHead(analyzer, node); 400 debug.dumpState(node, state, false); 401 } 402 403 // Create the code path of this scope. 404 codePath = analyzer.codePath = new CodePath( 405 analyzer.idGenerator.next(), 406 codePath, 407 analyzer.onLooped 408 ); 409 state = CodePath.getState(codePath); 410 411 // Emits onCodePathStart events. 412 debug.dump(`onCodePathStart ${codePath.id}`); 413 analyzer.emitter.emit("onCodePathStart", codePath, node); 414 break; 415 416 case "ChainExpression": 417 state.pushChainContext(); 418 break; 419 case "CallExpression": 420 if (node.optional === true) { 421 state.makeOptionalNode(); 422 } 423 break; 424 case "MemberExpression": 425 if (node.optional === true) { 426 state.makeOptionalNode(); 427 } 428 break; 429 430 case "LogicalExpression": 431 if (isHandledLogicalOperator(node.operator)) { 432 state.pushChoiceContext( 433 node.operator, 434 isForkingByTrueOrFalse(node) 435 ); 436 } 437 break; 438 439 case "AssignmentExpression": 440 if (isLogicalAssignmentOperator(node.operator)) { 441 state.pushChoiceContext( 442 node.operator.slice(0, -1), // removes `=` from the end 443 isForkingByTrueOrFalse(node) 444 ); 445 } 446 break; 447 448 case "ConditionalExpression": 449 case "IfStatement": 450 state.pushChoiceContext("test", false); 451 break; 452 453 case "SwitchStatement": 454 state.pushSwitchContext( 455 node.cases.some(isCaseNode), 456 getLabel(node) 457 ); 458 break; 459 460 case "TryStatement": 461 state.pushTryContext(Boolean(node.finalizer)); 462 break; 463 464 case "SwitchCase": 465 466 /* 467 * Fork if this node is after the 2st node in `cases`. 468 * It's similar to `else` blocks. 469 * The next `test` node is processed in this path. 470 */ 471 if (parent.discriminant !== node && parent.cases[0] !== node) { 472 state.forkPath(); 473 } 474 break; 475 476 case "WhileStatement": 477 case "DoWhileStatement": 478 case "ForStatement": 479 case "ForInStatement": 480 case "ForOfStatement": 481 state.pushLoopContext(node.type, getLabel(node)); 482 break; 483 484 case "LabeledStatement": 485 if (!breakableTypePattern.test(node.body.type)) { 486 state.pushBreakContext(false, node.label.name); 487 } 488 break; 489 490 default: 491 break; 492 } 493 494 // Emits onCodePathSegmentStart events if updated. 495 forwardCurrentToHead(analyzer, node); 496 debug.dumpState(node, state, false); 497} 498 499/** 500 * Updates the code path due to the type of a given node in leaving. 501 * @param {CodePathAnalyzer} analyzer The instance. 502 * @param {ASTNode} node The current AST node. 503 * @returns {void} 504 */ 505function processCodePathToExit(analyzer, node) { 506 const codePath = analyzer.codePath; 507 const state = CodePath.getState(codePath); 508 let dontForward = false; 509 510 switch (node.type) { 511 case "ChainExpression": 512 state.popChainContext(); 513 break; 514 515 case "IfStatement": 516 case "ConditionalExpression": 517 state.popChoiceContext(); 518 break; 519 520 case "LogicalExpression": 521 if (isHandledLogicalOperator(node.operator)) { 522 state.popChoiceContext(); 523 } 524 break; 525 526 case "AssignmentExpression": 527 if (isLogicalAssignmentOperator(node.operator)) { 528 state.popChoiceContext(); 529 } 530 break; 531 532 case "SwitchStatement": 533 state.popSwitchContext(); 534 break; 535 536 case "SwitchCase": 537 538 /* 539 * This is the same as the process at the 1st `consequent` node in 540 * `preprocess` function. 541 * Must do if this `consequent` is empty. 542 */ 543 if (node.consequent.length === 0) { 544 state.makeSwitchCaseBody(true, !node.test); 545 } 546 if (state.forkContext.reachable) { 547 dontForward = true; 548 } 549 break; 550 551 case "TryStatement": 552 state.popTryContext(); 553 break; 554 555 case "BreakStatement": 556 forwardCurrentToHead(analyzer, node); 557 state.makeBreak(node.label && node.label.name); 558 dontForward = true; 559 break; 560 561 case "ContinueStatement": 562 forwardCurrentToHead(analyzer, node); 563 state.makeContinue(node.label && node.label.name); 564 dontForward = true; 565 break; 566 567 case "ReturnStatement": 568 forwardCurrentToHead(analyzer, node); 569 state.makeReturn(); 570 dontForward = true; 571 break; 572 573 case "ThrowStatement": 574 forwardCurrentToHead(analyzer, node); 575 state.makeThrow(); 576 dontForward = true; 577 break; 578 579 case "Identifier": 580 if (isIdentifierReference(node)) { 581 state.makeFirstThrowablePathInTryBlock(); 582 dontForward = true; 583 } 584 break; 585 586 case "CallExpression": 587 case "ImportExpression": 588 case "MemberExpression": 589 case "NewExpression": 590 case "YieldExpression": 591 state.makeFirstThrowablePathInTryBlock(); 592 break; 593 594 case "WhileStatement": 595 case "DoWhileStatement": 596 case "ForStatement": 597 case "ForInStatement": 598 case "ForOfStatement": 599 state.popLoopContext(); 600 break; 601 602 case "AssignmentPattern": 603 state.popForkContext(); 604 break; 605 606 case "LabeledStatement": 607 if (!breakableTypePattern.test(node.body.type)) { 608 state.popBreakContext(); 609 } 610 break; 611 612 default: 613 break; 614 } 615 616 // Emits onCodePathSegmentStart events if updated. 617 if (!dontForward) { 618 forwardCurrentToHead(analyzer, node); 619 } 620 debug.dumpState(node, state, true); 621} 622 623/** 624 * Updates the code path to finalize the current code path. 625 * @param {CodePathAnalyzer} analyzer The instance. 626 * @param {ASTNode} node The current AST node. 627 * @returns {void} 628 */ 629function postprocess(analyzer, node) { 630 switch (node.type) { 631 case "Program": 632 case "FunctionDeclaration": 633 case "FunctionExpression": 634 case "ArrowFunctionExpression": { 635 let codePath = analyzer.codePath; 636 637 // Mark the current path as the final node. 638 CodePath.getState(codePath).makeFinal(); 639 640 // Emits onCodePathSegmentEnd event of the current segments. 641 leaveFromCurrentSegment(analyzer, node); 642 643 // Emits onCodePathEnd event of this code path. 644 debug.dump(`onCodePathEnd ${codePath.id}`); 645 analyzer.emitter.emit("onCodePathEnd", codePath, node); 646 debug.dumpDot(codePath); 647 648 codePath = analyzer.codePath = analyzer.codePath.upper; 649 if (codePath) { 650 debug.dumpState(node, CodePath.getState(codePath), true); 651 } 652 break; 653 } 654 655 // The `arguments.length >= 1` case is in `preprocess` function. 656 case "CallExpression": 657 if (node.optional === true && node.arguments.length === 0) { 658 CodePath.getState(analyzer.codePath).makeOptionalRight(); 659 } 660 break; 661 662 default: 663 break; 664 } 665} 666 667//------------------------------------------------------------------------------ 668// Public Interface 669//------------------------------------------------------------------------------ 670 671/** 672 * The class to analyze code paths. 673 * This class implements the EventGenerator interface. 674 */ 675class CodePathAnalyzer { 676 677 // eslint-disable-next-line jsdoc/require-description 678 /** 679 * @param {EventGenerator} eventGenerator An event generator to wrap. 680 */ 681 constructor(eventGenerator) { 682 this.original = eventGenerator; 683 this.emitter = eventGenerator.emitter; 684 this.codePath = null; 685 this.idGenerator = new IdGenerator("s"); 686 this.currentNode = null; 687 this.onLooped = this.onLooped.bind(this); 688 } 689 690 /** 691 * Does the process to enter a given AST node. 692 * This updates state of analysis and calls `enterNode` of the wrapped. 693 * @param {ASTNode} node A node which is entering. 694 * @returns {void} 695 */ 696 enterNode(node) { 697 this.currentNode = node; 698 699 // Updates the code path due to node's position in its parent node. 700 if (node.parent) { 701 preprocess(this, node); 702 } 703 704 /* 705 * Updates the code path. 706 * And emits onCodePathStart/onCodePathSegmentStart events. 707 */ 708 processCodePathToEnter(this, node); 709 710 // Emits node events. 711 this.original.enterNode(node); 712 713 this.currentNode = null; 714 } 715 716 /** 717 * Does the process to leave a given AST node. 718 * This updates state of analysis and calls `leaveNode` of the wrapped. 719 * @param {ASTNode} node A node which is leaving. 720 * @returns {void} 721 */ 722 leaveNode(node) { 723 this.currentNode = node; 724 725 /* 726 * Updates the code path. 727 * And emits onCodePathStart/onCodePathSegmentStart events. 728 */ 729 processCodePathToExit(this, node); 730 731 // Emits node events. 732 this.original.leaveNode(node); 733 734 // Emits the last onCodePathStart/onCodePathSegmentStart events. 735 postprocess(this, node); 736 737 this.currentNode = null; 738 } 739 740 /** 741 * This is called on a code path looped. 742 * Then this raises a looped event. 743 * @param {CodePathSegment} fromSegment A segment of prev. 744 * @param {CodePathSegment} toSegment A segment of next. 745 * @returns {void} 746 */ 747 onLooped(fromSegment, toSegment) { 748 if (fromSegment.reachable && toSegment.reachable) { 749 debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`); 750 this.emitter.emit( 751 "onCodePathSegmentLoop", 752 fromSegment, 753 toSegment, 754 this.currentNode 755 ); 756 } 757 } 758} 759 760module.exports = CodePathAnalyzer; 761