853 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			853 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @fileoverview A class of the code path analyzer.
 | |
|  * @author Toru Nagashima
 | |
|  */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // Requirements
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| const assert = require("assert"),
 | |
|     { breakableTypePattern } = require("../../shared/ast-utils"),
 | |
|     CodePath = require("./code-path"),
 | |
|     CodePathSegment = require("./code-path-segment"),
 | |
|     IdGenerator = require("./id-generator"),
 | |
|     debug = require("./debug-helpers");
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // Helpers
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| /**
 | |
|  * Checks whether or not a given node is a `case` node (not `default` node).
 | |
|  * @param {ASTNode} node A `SwitchCase` node to check.
 | |
|  * @returns {boolean} `true` if the node is a `case` node (not `default` node).
 | |
|  */
 | |
| function isCaseNode(node) {
 | |
|     return Boolean(node.test);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks if a given node appears as the value of a PropertyDefinition node.
 | |
|  * @param {ASTNode} node THe node to check.
 | |
|  * @returns {boolean} `true` if the node is a PropertyDefinition value,
 | |
|  *      false if not.
 | |
|  */
 | |
| function isPropertyDefinitionValue(node) {
 | |
|     const parent = node.parent;
 | |
| 
 | |
|     return parent && parent.type === "PropertyDefinition" && parent.value === node;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks whether the given logical operator is taken into account for the code
 | |
|  * path analysis.
 | |
|  * @param {string} operator The operator found in the LogicalExpression node
 | |
|  * @returns {boolean} `true` if the operator is "&&" or "||" or "??"
 | |
|  */
 | |
| function isHandledLogicalOperator(operator) {
 | |
|     return operator === "&&" || operator === "||" || operator === "??";
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks whether the given assignment operator is a logical assignment operator.
 | |
|  * Logical assignments are taken into account for the code path analysis
 | |
|  * because of their short-circuiting semantics.
 | |
|  * @param {string} operator The operator found in the AssignmentExpression node
 | |
|  * @returns {boolean} `true` if the operator is "&&=" or "||=" or "??="
 | |
|  */
 | |
| function isLogicalAssignmentOperator(operator) {
 | |
|     return operator === "&&=" || operator === "||=" || operator === "??=";
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Gets the label if the parent node of a given node is a LabeledStatement.
 | |
|  * @param {ASTNode} node A node to get.
 | |
|  * @returns {string|null} The label or `null`.
 | |
|  */
 | |
| function getLabel(node) {
 | |
|     if (node.parent.type === "LabeledStatement") {
 | |
|         return node.parent.label.name;
 | |
|     }
 | |
|     return null;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks whether or not a given logical expression node goes different path
 | |
|  * between the `true` case and the `false` case.
 | |
|  * @param {ASTNode} node A node to check.
 | |
|  * @returns {boolean} `true` if the node is a test of a choice statement.
 | |
|  */
 | |
| function isForkingByTrueOrFalse(node) {
 | |
|     const parent = node.parent;
 | |
| 
 | |
|     switch (parent.type) {
 | |
|         case "ConditionalExpression":
 | |
|         case "IfStatement":
 | |
|         case "WhileStatement":
 | |
|         case "DoWhileStatement":
 | |
|         case "ForStatement":
 | |
|             return parent.test === node;
 | |
| 
 | |
|         case "LogicalExpression":
 | |
|             return isHandledLogicalOperator(parent.operator);
 | |
| 
 | |
|         case "AssignmentExpression":
 | |
|             return isLogicalAssignmentOperator(parent.operator);
 | |
| 
 | |
|         default:
 | |
|             return false;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Gets the boolean value of a given literal node.
 | |
|  *
 | |
|  * This is used to detect infinity loops (e.g. `while (true) {}`).
 | |
|  * Statements preceded by an infinity loop are unreachable if the loop didn't
 | |
|  * have any `break` statement.
 | |
|  * @param {ASTNode} node A node to get.
 | |
|  * @returns {boolean|undefined} a boolean value if the node is a Literal node,
 | |
|  *   otherwise `undefined`.
 | |
|  */
 | |
| function getBooleanValueIfSimpleConstant(node) {
 | |
|     if (node.type === "Literal") {
 | |
|         return Boolean(node.value);
 | |
|     }
 | |
|     return void 0;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks that a given identifier node is a reference or not.
 | |
|  *
 | |
|  * This is used to detect the first throwable node in a `try` block.
 | |
|  * @param {ASTNode} node An Identifier node to check.
 | |
|  * @returns {boolean} `true` if the node is a reference.
 | |
|  */
 | |
| function isIdentifierReference(node) {
 | |
|     const parent = node.parent;
 | |
| 
 | |
|     switch (parent.type) {
 | |
|         case "LabeledStatement":
 | |
|         case "BreakStatement":
 | |
|         case "ContinueStatement":
 | |
|         case "ArrayPattern":
 | |
|         case "RestElement":
 | |
|         case "ImportSpecifier":
 | |
|         case "ImportDefaultSpecifier":
 | |
|         case "ImportNamespaceSpecifier":
 | |
|         case "CatchClause":
 | |
|             return false;
 | |
| 
 | |
|         case "FunctionDeclaration":
 | |
|         case "FunctionExpression":
 | |
|         case "ArrowFunctionExpression":
 | |
|         case "ClassDeclaration":
 | |
|         case "ClassExpression":
 | |
|         case "VariableDeclarator":
 | |
|             return parent.id !== node;
 | |
| 
 | |
|         case "Property":
 | |
|         case "PropertyDefinition":
 | |
|         case "MethodDefinition":
 | |
|             return (
 | |
|                 parent.key !== node ||
 | |
|                 parent.computed ||
 | |
|                 parent.shorthand
 | |
|             );
 | |
| 
 | |
|         case "AssignmentPattern":
 | |
|             return parent.key !== node;
 | |
| 
 | |
|         default:
 | |
|             return true;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Updates the current segment with the head segment.
 | |
|  * This is similar to local branches and tracking branches of git.
 | |
|  *
 | |
|  * To separate the current and the head is in order to not make useless segments.
 | |
|  *
 | |
|  * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
 | |
|  * events are fired.
 | |
|  * @param {CodePathAnalyzer} analyzer The instance.
 | |
|  * @param {ASTNode} node The current AST node.
 | |
|  * @returns {void}
 | |
|  */
 | |
| function forwardCurrentToHead(analyzer, node) {
 | |
|     const codePath = analyzer.codePath;
 | |
|     const state = CodePath.getState(codePath);
 | |
|     const currentSegments = state.currentSegments;
 | |
|     const headSegments = state.headSegments;
 | |
|     const end = Math.max(currentSegments.length, headSegments.length);
 | |
|     let i, currentSegment, headSegment;
 | |
| 
 | |
|     // Fires leaving events.
 | |
|     for (i = 0; i < end; ++i) {
 | |
|         currentSegment = currentSegments[i];
 | |
|         headSegment = headSegments[i];
 | |
| 
 | |
|         if (currentSegment !== headSegment && currentSegment) {
 | |
| 
 | |
|             const eventName = currentSegment.reachable
 | |
|                 ? "onCodePathSegmentEnd"
 | |
|                 : "onUnreachableCodePathSegmentEnd";
 | |
| 
 | |
|             debug.dump(`${eventName} ${currentSegment.id}`);
 | |
| 
 | |
|             analyzer.emitter.emit(
 | |
|                 eventName,
 | |
|                 currentSegment,
 | |
|                 node
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Update state.
 | |
|     state.currentSegments = headSegments;
 | |
| 
 | |
|     // Fires entering events.
 | |
|     for (i = 0; i < end; ++i) {
 | |
|         currentSegment = currentSegments[i];
 | |
|         headSegment = headSegments[i];
 | |
| 
 | |
|         if (currentSegment !== headSegment && headSegment) {
 | |
| 
 | |
|             const eventName = headSegment.reachable
 | |
|                 ? "onCodePathSegmentStart"
 | |
|                 : "onUnreachableCodePathSegmentStart";
 | |
| 
 | |
|             debug.dump(`${eventName} ${headSegment.id}`);
 | |
| 
 | |
|             CodePathSegment.markUsed(headSegment);
 | |
|             analyzer.emitter.emit(
 | |
|                 eventName,
 | |
|                 headSegment,
 | |
|                 node
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Updates the current segment with empty.
 | |
|  * This is called at the last of functions or the program.
 | |
|  * @param {CodePathAnalyzer} analyzer The instance.
 | |
|  * @param {ASTNode} node The current AST node.
 | |
|  * @returns {void}
 | |
|  */
 | |
| function leaveFromCurrentSegment(analyzer, node) {
 | |
|     const state = CodePath.getState(analyzer.codePath);
 | |
|     const currentSegments = state.currentSegments;
 | |
| 
 | |
|     for (let i = 0; i < currentSegments.length; ++i) {
 | |
|         const currentSegment = currentSegments[i];
 | |
|         const eventName = currentSegment.reachable
 | |
|             ? "onCodePathSegmentEnd"
 | |
|             : "onUnreachableCodePathSegmentEnd";
 | |
| 
 | |
|         debug.dump(`${eventName} ${currentSegment.id}`);
 | |
| 
 | |
|         analyzer.emitter.emit(
 | |
|             eventName,
 | |
|             currentSegment,
 | |
|             node
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     state.currentSegments = [];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Updates the code path due to the position of a given node in the parent node
 | |
|  * thereof.
 | |
|  *
 | |
|  * For example, if the node is `parent.consequent`, this creates a fork from the
 | |
|  * current path.
 | |
|  * @param {CodePathAnalyzer} analyzer The instance.
 | |
|  * @param {ASTNode} node The current AST node.
 | |
|  * @returns {void}
 | |
|  */
 | |
| function preprocess(analyzer, node) {
 | |
|     const codePath = analyzer.codePath;
 | |
|     const state = CodePath.getState(codePath);
 | |
|     const parent = node.parent;
 | |
| 
 | |
|     switch (parent.type) {
 | |
| 
 | |
|         // The `arguments.length == 0` case is in `postprocess` function.
 | |
|         case "CallExpression":
 | |
|             if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) {
 | |
|                 state.makeOptionalRight();
 | |
|             }
 | |
|             break;
 | |
|         case "MemberExpression":
 | |
|             if (parent.optional === true && parent.property === node) {
 | |
|                 state.makeOptionalRight();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "LogicalExpression":
 | |
|             if (
 | |
|                 parent.right === node &&
 | |
|                 isHandledLogicalOperator(parent.operator)
 | |
|             ) {
 | |
|                 state.makeLogicalRight();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "AssignmentExpression":
 | |
|             if (
 | |
|                 parent.right === node &&
 | |
|                 isLogicalAssignmentOperator(parent.operator)
 | |
|             ) {
 | |
|                 state.makeLogicalRight();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "ConditionalExpression":
 | |
|         case "IfStatement":
 | |
| 
 | |
|             /*
 | |
|              * Fork if this node is at `consequent`/`alternate`.
 | |
|              * `popForkContext()` exists at `IfStatement:exit` and
 | |
|              * `ConditionalExpression:exit`.
 | |
|              */
 | |
|             if (parent.consequent === node) {
 | |
|                 state.makeIfConsequent();
 | |
|             } else if (parent.alternate === node) {
 | |
|                 state.makeIfAlternate();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "SwitchCase":
 | |
|             if (parent.consequent[0] === node) {
 | |
|                 state.makeSwitchCaseBody(false, !parent.test);
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "TryStatement":
 | |
|             if (parent.handler === node) {
 | |
|                 state.makeCatchBlock();
 | |
|             } else if (parent.finalizer === node) {
 | |
|                 state.makeFinallyBlock();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "WhileStatement":
 | |
|             if (parent.test === node) {
 | |
|                 state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
 | |
|             } else {
 | |
|                 assert(parent.body === node);
 | |
|                 state.makeWhileBody();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "DoWhileStatement":
 | |
|             if (parent.body === node) {
 | |
|                 state.makeDoWhileBody();
 | |
|             } else {
 | |
|                 assert(parent.test === node);
 | |
|                 state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "ForStatement":
 | |
|             if (parent.test === node) {
 | |
|                 state.makeForTest(getBooleanValueIfSimpleConstant(node));
 | |
|             } else if (parent.update === node) {
 | |
|                 state.makeForUpdate();
 | |
|             } else if (parent.body === node) {
 | |
|                 state.makeForBody();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "ForInStatement":
 | |
|         case "ForOfStatement":
 | |
|             if (parent.left === node) {
 | |
|                 state.makeForInOfLeft();
 | |
|             } else if (parent.right === node) {
 | |
|                 state.makeForInOfRight();
 | |
|             } else {
 | |
|                 assert(parent.body === node);
 | |
|                 state.makeForInOfBody();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "AssignmentPattern":
 | |
| 
 | |
|             /*
 | |
|              * Fork if this node is at `right`.
 | |
|              * `left` is executed always, so it uses the current path.
 | |
|              * `popForkContext()` exists at `AssignmentPattern:exit`.
 | |
|              */
 | |
|             if (parent.right === node) {
 | |
|                 state.pushForkContext();
 | |
|                 state.forkBypassPath();
 | |
|                 state.forkPath();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         default:
 | |
|             break;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Updates the code path due to the type of a given node in entering.
 | |
|  * @param {CodePathAnalyzer} analyzer The instance.
 | |
|  * @param {ASTNode} node The current AST node.
 | |
|  * @returns {void}
 | |
|  */
 | |
| function processCodePathToEnter(analyzer, node) {
 | |
|     let codePath = analyzer.codePath;
 | |
|     let state = codePath && CodePath.getState(codePath);
 | |
|     const parent = node.parent;
 | |
| 
 | |
|     /**
 | |
|      * Creates a new code path and trigger the onCodePathStart event
 | |
|      * based on the currently selected node.
 | |
|      * @param {string} origin The reason the code path was started.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     function startCodePath(origin) {
 | |
|         if (codePath) {
 | |
| 
 | |
|             // Emits onCodePathSegmentStart events if updated.
 | |
|             forwardCurrentToHead(analyzer, node);
 | |
|             debug.dumpState(node, state, false);
 | |
|         }
 | |
| 
 | |
|         // Create the code path of this scope.
 | |
|         codePath = analyzer.codePath = new CodePath({
 | |
|             id: analyzer.idGenerator.next(),
 | |
|             origin,
 | |
|             upper: codePath,
 | |
|             onLooped: analyzer.onLooped
 | |
|         });
 | |
|         state = CodePath.getState(codePath);
 | |
| 
 | |
|         // Emits onCodePathStart events.
 | |
|         debug.dump(`onCodePathStart ${codePath.id}`);
 | |
|         analyzer.emitter.emit("onCodePathStart", codePath, node);
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|      * Special case: The right side of class field initializer is considered
 | |
|      * to be its own function, so we need to start a new code path in this
 | |
|      * case.
 | |
|      */
 | |
|     if (isPropertyDefinitionValue(node)) {
 | |
|         startCodePath("class-field-initializer");
 | |
| 
 | |
|         /*
 | |
|          * Intentional fall through because `node` needs to also be
 | |
|          * processed by the code below. For example, if we have:
 | |
|          *
 | |
|          * class Foo {
 | |
|          *     a = () => {}
 | |
|          * }
 | |
|          *
 | |
|          * In this case, we also need start a second code path.
 | |
|          */
 | |
| 
 | |
|     }
 | |
| 
 | |
|     switch (node.type) {
 | |
|         case "Program":
 | |
|             startCodePath("program");
 | |
|             break;
 | |
| 
 | |
|         case "FunctionDeclaration":
 | |
|         case "FunctionExpression":
 | |
|         case "ArrowFunctionExpression":
 | |
|             startCodePath("function");
 | |
|             break;
 | |
| 
 | |
|         case "StaticBlock":
 | |
|             startCodePath("class-static-block");
 | |
|             break;
 | |
| 
 | |
|         case "ChainExpression":
 | |
|             state.pushChainContext();
 | |
|             break;
 | |
|         case "CallExpression":
 | |
|             if (node.optional === true) {
 | |
|                 state.makeOptionalNode();
 | |
|             }
 | |
|             break;
 | |
|         case "MemberExpression":
 | |
|             if (node.optional === true) {
 | |
|                 state.makeOptionalNode();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "LogicalExpression":
 | |
|             if (isHandledLogicalOperator(node.operator)) {
 | |
|                 state.pushChoiceContext(
 | |
|                     node.operator,
 | |
|                     isForkingByTrueOrFalse(node)
 | |
|                 );
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "AssignmentExpression":
 | |
|             if (isLogicalAssignmentOperator(node.operator)) {
 | |
|                 state.pushChoiceContext(
 | |
|                     node.operator.slice(0, -1), // removes `=` from the end
 | |
|                     isForkingByTrueOrFalse(node)
 | |
|                 );
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "ConditionalExpression":
 | |
|         case "IfStatement":
 | |
|             state.pushChoiceContext("test", false);
 | |
|             break;
 | |
| 
 | |
|         case "SwitchStatement":
 | |
|             state.pushSwitchContext(
 | |
|                 node.cases.some(isCaseNode),
 | |
|                 getLabel(node)
 | |
|             );
 | |
|             break;
 | |
| 
 | |
|         case "TryStatement":
 | |
|             state.pushTryContext(Boolean(node.finalizer));
 | |
|             break;
 | |
| 
 | |
|         case "SwitchCase":
 | |
| 
 | |
|             /*
 | |
|              * Fork if this node is after the 2st node in `cases`.
 | |
|              * It's similar to `else` blocks.
 | |
|              * The next `test` node is processed in this path.
 | |
|              */
 | |
|             if (parent.discriminant !== node && parent.cases[0] !== node) {
 | |
|                 state.forkPath();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "WhileStatement":
 | |
|         case "DoWhileStatement":
 | |
|         case "ForStatement":
 | |
|         case "ForInStatement":
 | |
|         case "ForOfStatement":
 | |
|             state.pushLoopContext(node.type, getLabel(node));
 | |
|             break;
 | |
| 
 | |
|         case "LabeledStatement":
 | |
|             if (!breakableTypePattern.test(node.body.type)) {
 | |
|                 state.pushBreakContext(false, node.label.name);
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         default:
 | |
|             break;
 | |
|     }
 | |
| 
 | |
|     // Emits onCodePathSegmentStart events if updated.
 | |
|     forwardCurrentToHead(analyzer, node);
 | |
|     debug.dumpState(node, state, false);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Updates the code path due to the type of a given node in leaving.
 | |
|  * @param {CodePathAnalyzer} analyzer The instance.
 | |
|  * @param {ASTNode} node The current AST node.
 | |
|  * @returns {void}
 | |
|  */
 | |
| function processCodePathToExit(analyzer, node) {
 | |
| 
 | |
|     const codePath = analyzer.codePath;
 | |
|     const state = CodePath.getState(codePath);
 | |
|     let dontForward = false;
 | |
| 
 | |
|     switch (node.type) {
 | |
|         case "ChainExpression":
 | |
|             state.popChainContext();
 | |
|             break;
 | |
| 
 | |
|         case "IfStatement":
 | |
|         case "ConditionalExpression":
 | |
|             state.popChoiceContext();
 | |
|             break;
 | |
| 
 | |
|         case "LogicalExpression":
 | |
|             if (isHandledLogicalOperator(node.operator)) {
 | |
|                 state.popChoiceContext();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "AssignmentExpression":
 | |
|             if (isLogicalAssignmentOperator(node.operator)) {
 | |
|                 state.popChoiceContext();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "SwitchStatement":
 | |
|             state.popSwitchContext();
 | |
|             break;
 | |
| 
 | |
|         case "SwitchCase":
 | |
| 
 | |
|             /*
 | |
|              * This is the same as the process at the 1st `consequent` node in
 | |
|              * `preprocess` function.
 | |
|              * Must do if this `consequent` is empty.
 | |
|              */
 | |
|             if (node.consequent.length === 0) {
 | |
|                 state.makeSwitchCaseBody(true, !node.test);
 | |
|             }
 | |
|             if (state.forkContext.reachable) {
 | |
|                 dontForward = true;
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "TryStatement":
 | |
|             state.popTryContext();
 | |
|             break;
 | |
| 
 | |
|         case "BreakStatement":
 | |
|             forwardCurrentToHead(analyzer, node);
 | |
|             state.makeBreak(node.label && node.label.name);
 | |
|             dontForward = true;
 | |
|             break;
 | |
| 
 | |
|         case "ContinueStatement":
 | |
|             forwardCurrentToHead(analyzer, node);
 | |
|             state.makeContinue(node.label && node.label.name);
 | |
|             dontForward = true;
 | |
|             break;
 | |
| 
 | |
|         case "ReturnStatement":
 | |
|             forwardCurrentToHead(analyzer, node);
 | |
|             state.makeReturn();
 | |
|             dontForward = true;
 | |
|             break;
 | |
| 
 | |
|         case "ThrowStatement":
 | |
|             forwardCurrentToHead(analyzer, node);
 | |
|             state.makeThrow();
 | |
|             dontForward = true;
 | |
|             break;
 | |
| 
 | |
|         case "Identifier":
 | |
|             if (isIdentifierReference(node)) {
 | |
|                 state.makeFirstThrowablePathInTryBlock();
 | |
|                 dontForward = true;
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         case "CallExpression":
 | |
|         case "ImportExpression":
 | |
|         case "MemberExpression":
 | |
|         case "NewExpression":
 | |
|         case "YieldExpression":
 | |
|             state.makeFirstThrowablePathInTryBlock();
 | |
|             break;
 | |
| 
 | |
|         case "WhileStatement":
 | |
|         case "DoWhileStatement":
 | |
|         case "ForStatement":
 | |
|         case "ForInStatement":
 | |
|         case "ForOfStatement":
 | |
|             state.popLoopContext();
 | |
|             break;
 | |
| 
 | |
|         case "AssignmentPattern":
 | |
|             state.popForkContext();
 | |
|             break;
 | |
| 
 | |
|         case "LabeledStatement":
 | |
|             if (!breakableTypePattern.test(node.body.type)) {
 | |
|                 state.popBreakContext();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         default:
 | |
|             break;
 | |
|     }
 | |
| 
 | |
|     // Emits onCodePathSegmentStart events if updated.
 | |
|     if (!dontForward) {
 | |
|         forwardCurrentToHead(analyzer, node);
 | |
|     }
 | |
|     debug.dumpState(node, state, true);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Updates the code path to finalize the current code path.
 | |
|  * @param {CodePathAnalyzer} analyzer The instance.
 | |
|  * @param {ASTNode} node The current AST node.
 | |
|  * @returns {void}
 | |
|  */
 | |
| function postprocess(analyzer, node) {
 | |
| 
 | |
|     /**
 | |
|      * Ends the code path for the current node.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     function endCodePath() {
 | |
|         let codePath = analyzer.codePath;
 | |
| 
 | |
|         // Mark the current path as the final node.
 | |
|         CodePath.getState(codePath).makeFinal();
 | |
| 
 | |
|         // Emits onCodePathSegmentEnd event of the current segments.
 | |
|         leaveFromCurrentSegment(analyzer, node);
 | |
| 
 | |
|         // Emits onCodePathEnd event of this code path.
 | |
|         debug.dump(`onCodePathEnd ${codePath.id}`);
 | |
|         analyzer.emitter.emit("onCodePathEnd", codePath, node);
 | |
|         debug.dumpDot(codePath);
 | |
| 
 | |
|         codePath = analyzer.codePath = analyzer.codePath.upper;
 | |
|         if (codePath) {
 | |
|             debug.dumpState(node, CodePath.getState(codePath), true);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     switch (node.type) {
 | |
|         case "Program":
 | |
|         case "FunctionDeclaration":
 | |
|         case "FunctionExpression":
 | |
|         case "ArrowFunctionExpression":
 | |
|         case "StaticBlock": {
 | |
|             endCodePath();
 | |
|             break;
 | |
|         }
 | |
| 
 | |
|         // The `arguments.length >= 1` case is in `preprocess` function.
 | |
|         case "CallExpression":
 | |
|             if (node.optional === true && node.arguments.length === 0) {
 | |
|                 CodePath.getState(analyzer.codePath).makeOptionalRight();
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|         default:
 | |
|             break;
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|      * Special case: The right side of class field initializer is considered
 | |
|      * to be its own function, so we need to end a code path in this
 | |
|      * case.
 | |
|      *
 | |
|      * We need to check after the other checks in order to close the
 | |
|      * code paths in the correct order for code like this:
 | |
|      *
 | |
|      *
 | |
|      * class Foo {
 | |
|      *     a = () => {}
 | |
|      * }
 | |
|      *
 | |
|      * In this case, The ArrowFunctionExpression code path is closed first
 | |
|      * and then we need to close the code path for the PropertyDefinition
 | |
|      * value.
 | |
|      */
 | |
|     if (isPropertyDefinitionValue(node)) {
 | |
|         endCodePath();
 | |
|     }
 | |
| }
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // Public Interface
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| /**
 | |
|  * The class to analyze code paths.
 | |
|  * This class implements the EventGenerator interface.
 | |
|  */
 | |
| class CodePathAnalyzer {
 | |
| 
 | |
|     /**
 | |
|      * @param {EventGenerator} eventGenerator An event generator to wrap.
 | |
|      */
 | |
|     constructor(eventGenerator) {
 | |
|         this.original = eventGenerator;
 | |
|         this.emitter = eventGenerator.emitter;
 | |
|         this.codePath = null;
 | |
|         this.idGenerator = new IdGenerator("s");
 | |
|         this.currentNode = null;
 | |
|         this.onLooped = this.onLooped.bind(this);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Does the process to enter a given AST node.
 | |
|      * This updates state of analysis and calls `enterNode` of the wrapped.
 | |
|      * @param {ASTNode} node A node which is entering.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     enterNode(node) {
 | |
|         this.currentNode = node;
 | |
| 
 | |
|         // Updates the code path due to node's position in its parent node.
 | |
|         if (node.parent) {
 | |
|             preprocess(this, node);
 | |
|         }
 | |
| 
 | |
|         /*
 | |
|          * Updates the code path.
 | |
|          * And emits onCodePathStart/onCodePathSegmentStart events.
 | |
|          */
 | |
|         processCodePathToEnter(this, node);
 | |
| 
 | |
|         // Emits node events.
 | |
|         this.original.enterNode(node);
 | |
| 
 | |
|         this.currentNode = null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Does the process to leave a given AST node.
 | |
|      * This updates state of analysis and calls `leaveNode` of the wrapped.
 | |
|      * @param {ASTNode} node A node which is leaving.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     leaveNode(node) {
 | |
|         this.currentNode = node;
 | |
| 
 | |
|         /*
 | |
|          * Updates the code path.
 | |
|          * And emits onCodePathStart/onCodePathSegmentStart events.
 | |
|          */
 | |
|         processCodePathToExit(this, node);
 | |
| 
 | |
|         // Emits node events.
 | |
|         this.original.leaveNode(node);
 | |
| 
 | |
|         // Emits the last onCodePathStart/onCodePathSegmentStart events.
 | |
|         postprocess(this, node);
 | |
| 
 | |
|         this.currentNode = null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This is called on a code path looped.
 | |
|      * Then this raises a looped event.
 | |
|      * @param {CodePathSegment} fromSegment A segment of prev.
 | |
|      * @param {CodePathSegment} toSegment A segment of next.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     onLooped(fromSegment, toSegment) {
 | |
|         if (fromSegment.reachable && toSegment.reachable) {
 | |
|             debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`);
 | |
|             this.emitter.emit(
 | |
|                 "onCodePathSegmentLoop",
 | |
|                 fromSegment,
 | |
|                 toSegment,
 | |
|                 this.currentNode
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| module.exports = CodePathAnalyzer;
 |