|                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |  | /** * @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;
 |