.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / linter / code-path-analysis / code-path-analyzer.js
diff --git a/.config/coc/extensions/node_modules/coc-prettier/node_modules/eslint/lib/linter/code-path-analysis/code-path-analyzer.js b/.config/coc/extensions/node_modules/coc-prettier/node_modules/eslint/lib/linter/code-path-analysis/code-path-analyzer.js
new file mode 100644 (file)
index 0000000..47427c1
--- /dev/null
@@ -0,0 +1,760 @@
+/**
+ * @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 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 "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) {
+            debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
+
+            if (currentSegment.reachable) {
+                analyzer.emitter.emit(
+                    "onCodePathSegmentEnd",
+                    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) {
+            debug.dump(`onCodePathSegmentStart ${headSegment.id}`);
+
+            CodePathSegment.markUsed(headSegment);
+            if (headSegment.reachable) {
+                analyzer.emitter.emit(
+                    "onCodePathSegmentStart",
+                    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];
+
+        debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
+        if (currentSegment.reachable) {
+            analyzer.emitter.emit(
+                "onCodePathSegmentEnd",
+                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;
+
+    switch (node.type) {
+        case "Program":
+        case "FunctionDeclaration":
+        case "FunctionExpression":
+        case "ArrowFunctionExpression":
+            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(
+                analyzer.idGenerator.next(),
+                codePath,
+                analyzer.onLooped
+            );
+            state = CodePath.getState(codePath);
+
+            // Emits onCodePathStart events.
+            debug.dump(`onCodePathStart ${codePath.id}`);
+            analyzer.emitter.emit("onCodePathStart", codePath, node);
+            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) {
+    switch (node.type) {
+        case "Program":
+        case "FunctionDeclaration":
+        case "FunctionExpression":
+        case "ArrowFunctionExpression": {
+            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);
+            }
+            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;
+    }
+}
+
+//------------------------------------------------------------------------------
+// Public Interface
+//------------------------------------------------------------------------------
+
+/**
+ * The class to analyze code paths.
+ * This class implements the EventGenerator interface.
+ */
+class CodePathAnalyzer {
+
+    // eslint-disable-next-line jsdoc/require-description
+    /**
+     * @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;