| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 | /** * @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit * @author Jordan Eldredge <https://jordaneldredge.com> */"use strict";const globals = require("globals");const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils");const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);//------------------------------------------------------------------------------// Helpers//------------------------------------------------------------------------------/** * Test if an AST node has a statically knowable constant nullishness. Meaning, * it will always resolve to a constant value of either: `null`, `undefined` * or not `null` _or_ `undefined`. An expression that can vary between those * three states at runtime would return `false`. * @param {Scope} scope The scope in which the node was found. * @param {ASTNode} node The AST node being tested. * @returns {boolean} Does `node` have constant nullishness? */function hasConstantNullishness(scope, node) {    switch (node.type) {        case "ObjectExpression": // Objects are never nullish        case "ArrayExpression": // Arrays are never nullish        case "ArrowFunctionExpression": // Functions never nullish        case "FunctionExpression": // Functions are never nullish        case "ClassExpression": // Classes are never nullish        case "NewExpression": // Objects are never nullish        case "Literal": // Nullish, or non-nullish, literals never change        case "TemplateLiteral": // A string is never nullish        case "UpdateExpression": // Numbers are never nullish        case "BinaryExpression": // Numbers, strings, or booleans are never nullish            return true;        case "CallExpression": {            if (node.callee.type !== "Identifier") {                return false;            }            const functionName = node.callee.name;            return (functionName === "Boolean" || functionName === "String" || functionName === "Number") &&                isReferenceToGlobalVariable(scope, node.callee);        }        case "AssignmentExpression":            if (node.operator === "=") {                return hasConstantNullishness(scope, node.right);            }            /*             * Handling short-circuiting assignment operators would require             * walking the scope. We won't attempt that (for now...) /             */            if (isLogicalAssignmentOperator(node.operator)) {                return false;            }            /*             * The remaining assignment expressions all result in a numeric or             * string (non-nullish) value:             *   "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="             */            return true;        case "UnaryExpression":            /*             * "void" Always returns `undefined`             * "typeof" All types are strings, and thus non-nullish             * "!" Boolean is never nullish             * "delete" Returns a boolean, which is never nullish             * Math operators always return numbers or strings, neither of which             * are non-nullish "+", "-", "~"             */            return true;        case "SequenceExpression": {            const last = node.expressions[node.expressions.length - 1];            return hasConstantNullishness(scope, last);        }        case "Identifier":            return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);        case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.        case "JSXFragment":            return false;        default:            return false;    }}/** * Test if an AST node is a boolean value that never changes. Specifically we * test for: * 1. Literal booleans (`true` or `false`) * 2. Unary `!` expressions with a constant value * 3. Constant booleans created via the `Boolean` global function * @param {Scope} scope The scope in which the node was found. * @param {ASTNode} node The node to test * @returns {boolean} Is `node` guaranteed to be a boolean? */function isStaticBoolean(scope, node) {    switch (node.type) {        case "Literal":            return typeof node.value === "boolean";        case "CallExpression":            return node.callee.type === "Identifier" && node.callee.name === "Boolean" &&              isReferenceToGlobalVariable(scope, node.callee) &&              (node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));        case "UnaryExpression":            return node.operator === "!" && isConstant(scope, node.argument, true);        default:            return false;    }}/** * Test if an AST node will always give the same result when compared to a * boolean value. Note that comparison to boolean values is different than * truthiness. * https://262.ecma-international.org/5.1/#sec-11.9.3 * * Javascript `==` operator works by converting the boolean to `1` (true) or * `+0` (false) and then checks the values `==` equality to that number. * @param {Scope} scope The scope in which node was found. * @param {ASTNode} node The node to test. * @returns {boolean} Will `node` always coerce to the same boolean value? */function hasConstantLooseBooleanComparison(scope, node) {    switch (node.type) {        case "ObjectExpression":        case "ClassExpression":            /**             * In theory objects like:             *             * `{toString: () => a}`             * `{valueOf: () => a}`             *             * Or a classes like:             *             * `class { static toString() { return a } }`             * `class { static valueOf() { return a } }`             *             * Are not constant verifiably when `inBooleanPosition` is             * false, but it's an edge case we've opted not to handle.             */            return true;        case "ArrayExpression": {            const nonSpreadElements = node.elements.filter(e =>                // Elements can be `null` in sparse arrays: `[,,]`;                e !== null && e.type !== "SpreadElement");            /*             * Possible future direction if needed: We could check if the             * single value would result in variable boolean comparison.             * For now we will err on the side of caution since `[x]` could             * evaluate to `[0]` or `[1]`.             */            return node.elements.length === 0 || nonSpreadElements.length > 1;        }        case "ArrowFunctionExpression":        case "FunctionExpression":            return true;        case "UnaryExpression":            if (node.operator === "void" || // Always returns `undefined`                node.operator === "typeof" // All `typeof` strings, when coerced to number, are not 0 or 1.            ) {                return true;            }            if (node.operator === "!") {                return isConstant(scope, node.argument, true);            }            /*             * We won't try to reason about +, -, ~, or delete             * In theory, for the mathematical operators, we could look at the             * argument and try to determine if it coerces to a constant numeric             * value.             */            return false;        case "NewExpression": // Objects might have custom `.valueOf` or `.toString`.            return false;        case "CallExpression": {            if (node.callee.type === "Identifier" &&                node.callee.name === "Boolean" &&                isReferenceToGlobalVariable(scope, node.callee)            ) {                return node.arguments.length === 0 || isConstant(scope, node.arguments[0], true);            }            return false;        }        case "Literal": // True or false, literals never change            return true;        case "Identifier":            return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);        case "TemplateLiteral":            /*             * In theory we could try to check if the quasi are sufficient to             * prove that the expression will always be true, but it would be             * tricky to get right. For example: `000.${foo}000`             */            return node.expressions.length === 0;        case "AssignmentExpression":            if (node.operator === "=") {                return hasConstantLooseBooleanComparison(scope, node.right);            }            /*             * Handling short-circuiting assignment operators would require             * walking the scope. We won't attempt that (for now...)             *             * The remaining assignment expressions all result in a numeric or             * string (non-nullish) values which could be truthy or falsy:             *   "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="             */            return false;        case "SequenceExpression": {            const last = node.expressions[node.expressions.length - 1];            return hasConstantLooseBooleanComparison(scope, last);        }        case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.        case "JSXFragment":            return false;        default:            return false;    }}/** * Test if an AST node will always give the same result when _strictly_ compared * to a boolean value. This can happen if the expression can never be boolean, or * if it is always the same boolean value. * @param {Scope} scope The scope in which the node was found. * @param {ASTNode} node The node to test * @returns {boolean} Will `node` always give the same result when compared to a * static boolean value? */function hasConstantStrictBooleanComparison(scope, node) {    switch (node.type) {        case "ObjectExpression": // Objects are not booleans        case "ArrayExpression": // Arrays are not booleans        case "ArrowFunctionExpression": // Functions are not booleans        case "FunctionExpression":        case "ClassExpression": // Classes are not booleans        case "NewExpression": // Objects are not booleans        case "TemplateLiteral": // Strings are not booleans        case "Literal": // True, false, or not boolean, literals never change.        case "UpdateExpression": // Numbers are not booleans            return true;        case "BinaryExpression":            return NUMERIC_OR_STRING_BINARY_OPERATORS.has(node.operator);        case "UnaryExpression": {            if (node.operator === "delete") {                return false;            }            if (node.operator === "!") {                return isConstant(scope, node.argument, true);            }            /*             * The remaining operators return either strings or numbers, neither             * of which are boolean.             */            return true;        }        case "SequenceExpression": {            const last = node.expressions[node.expressions.length - 1];            return hasConstantStrictBooleanComparison(scope, last);        }        case "Identifier":            return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);        case "AssignmentExpression":            if (node.operator === "=") {                return hasConstantStrictBooleanComparison(scope, node.right);            }            /*             * Handling short-circuiting assignment operators would require             * walking the scope. We won't attempt that (for now...)             */            if (isLogicalAssignmentOperator(node.operator)) {                return false;            }            /*             * The remaining assignment expressions all result in either a number             * or a string, neither of which can ever be boolean.             */            return true;        case "CallExpression": {            if (node.callee.type !== "Identifier") {                return false;            }            const functionName = node.callee.name;            if (                (functionName === "String" || functionName === "Number") &&                isReferenceToGlobalVariable(scope, node.callee)            ) {                return true;            }            if (functionName === "Boolean" && isReferenceToGlobalVariable(scope, node.callee)) {                return (                    node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));            }            return false;        }        case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.        case "JSXFragment":            return false;        default:            return false;    }}/** * Test if an AST node will always result in a newly constructed object * @param {Scope} scope The scope in which the node was found. * @param {ASTNode} node The node to test * @returns {boolean} Will `node` always be new? */function isAlwaysNew(scope, node) {    switch (node.type) {        case "ObjectExpression":        case "ArrayExpression":        case "ArrowFunctionExpression":        case "FunctionExpression":        case "ClassExpression":            return true;        case "NewExpression": {            if (node.callee.type !== "Identifier") {                return false;            }            /*             * All the built-in constructors are always new, but             * user-defined constructors could return a sentinel             * object.             *             * Catching these is especially useful for primitive constructures             * which return boxed values, a surprising gotcha' in JavaScript.             */            return Object.hasOwnProperty.call(globals.builtin, node.callee.name) &&              isReferenceToGlobalVariable(scope, node.callee);        }        case "Literal":            // Regular expressions are objects, and thus always new            return typeof node.regex === "object";        case "SequenceExpression": {            const last = node.expressions[node.expressions.length - 1];            return isAlwaysNew(scope, last);        }        case "AssignmentExpression":            if (node.operator === "=") {                return isAlwaysNew(scope, node.right);            }            return false;        case "ConditionalExpression":            return isAlwaysNew(scope, node.consequent) && isAlwaysNew(scope, node.alternate);        case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.        case "JSXFragment":            return false;        default:            return false;    }}/** * Checks whether or not a node is `null` or `undefined`. Similar to the one * found in ast-utils.js, but this one correctly handles the edge case that * `undefined` has been redefined. * @param {Scope} scope Scope in which the expression was found. * @param {ASTNode} node A node to check. * @returns {boolean} Whether or not the node is a `null` or `undefined`. * @public */function isNullOrUndefined(scope, node) {    return (        isNullLiteral(node) ||        (node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||        (node.type === "UnaryExpression" && node.operator === "void")    );}/** * Checks if one operand will cause the result to be constant. * @param {Scope} scope Scope in which the expression was found. * @param {ASTNode} a One side of the expression * @param {ASTNode} b The other side of the expression * @param {string} operator The binary expression operator * @returns {ASTNode | null} The node which will cause the expression to have a constant result. */function findBinaryExpressionConstantOperand(scope, a, b, operator) {    if (operator === "==" || operator === "!=") {        if (            (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||            (isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b))        ) {            return b;        }    } else if (operator === "===" || operator === "!==") {        if (            (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||            (isStaticBoolean(scope, a) && hasConstantStrictBooleanComparison(scope, b))        ) {            return b;        }    }    return null;}//------------------------------------------------------------------------------// Rule Definition//------------------------------------------------------------------------------/** @type {import('../shared/types').Rule} */module.exports = {    meta: {        type: "problem",        docs: {            description: "Disallow expressions where the operation doesn't affect the value",            recommended: false,            url: "https://eslint.org/docs/rules/no-constant-binary-expression"        },        schema: [],        messages: {            constantBinaryOperand: "Unexpected constant binary expression. Compares constantly with the {{otherSide}}-hand side of the `{{operator}}`.",            constantShortCircuit: "Unexpected constant {{property}} on the left-hand side of a `{{operator}}` expression.",            alwaysNew: "Unexpected comparison to newly constructed object. These two values can never be equal.",            bothAlwaysNew: "Unexpected comparison of two newly constructed objects. These two values can never be equal."        }    },    create(context) {        return {            LogicalExpression(node) {                const { operator, left } = node;                const scope = context.getScope();                if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) {                    context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } });                } else if (operator === "??" && hasConstantNullishness(scope, left)) {                    context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } });                }            },            BinaryExpression(node) {                const scope = context.getScope();                const { right, left, operator } = node;                const rightConstantOperand = findBinaryExpressionConstantOperand(scope, left, right, operator);                const leftConstantOperand = findBinaryExpressionConstantOperand(scope, right, left, operator);                if (rightConstantOperand) {                    context.report({ node: rightConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "left" } });                } else if (leftConstantOperand) {                    context.report({ node: leftConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "right" } });                } else if (operator === "===" || operator === "!==") {                    if (isAlwaysNew(scope, left)) {                        context.report({ node: left, messageId: "alwaysNew" });                    } else if (isAlwaysNew(scope, right)) {                        context.report({ node: right, messageId: "alwaysNew" });                    }                } else if (operator === "==" || operator === "!=") {                    /*                     * If both sides are "new", then both sides are objects and                     * therefore they will be compared by reference even with `==`                     * equality.                     */                    if (isAlwaysNew(scope, left) && isAlwaysNew(scope, right)) {                        context.report({ node: left, messageId: "bothAlwaysNew" });                    }                }            }            /*             * In theory we could handle short-circuiting assignment operators,             * for some constant values, but that would require walking the             * scope to find the value of the variable being assigned. This is             * dependant on https://github.com/eslint/eslint/issues/13776             *             * AssignmentExpression() {},             */        };    }};
 |