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;
 |