| /** | |
|  * @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 | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** | |
|  * 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") | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * 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. | |
|  * @param {boolean} nonNullish if `true` then nullish values are not considered constant. | |
|  * @returns {boolean} Does `node` have constant nullishness? | |
|  */ | |
| function hasConstantNullishness(scope, node, nonNullish) { | |
|     if (nonNullish && isNullOrUndefined(scope, node)) { | |
|         return false; | |
|     } | |
| 
 | |
|     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 "LogicalExpression": { | |
|             return node.operator === "??" && hasConstantNullishness(scope, node.right, true); | |
|         } | |
|         case "AssignmentExpression": | |
|             if (node.operator === "=") { | |
|                 return hasConstantNullishness(scope, node.right, nonNullish); | |
|             } | |
| 
 | |
|             /* | |
|              * 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, nonNullish); | |
|         } | |
|         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 constructors | |
|              * 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 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, false)) || | |
|             (isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b)) | |
|         ) { | |
|             return b; | |
|         } | |
|     } else if (operator === "===" || operator === "!==") { | |
|         if ( | |
|             (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b, false)) || | |
|             (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/latest/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) { | |
|         const sourceCode = context.sourceCode; | |
| 
 | |
|         return { | |
|             LogicalExpression(node) { | |
|                 const { operator, left } = node; | |
|                 const scope = sourceCode.getScope(node); | |
| 
 | |
|                 if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) { | |
|                     context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } }); | |
|                 } else if (operator === "??" && hasConstantNullishness(scope, left, false)) { | |
|                     context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } }); | |
|                 } | |
|             }, | |
|             BinaryExpression(node) { | |
|                 const scope = sourceCode.getScope(node); | |
|                 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() {}, | |
|              */ | |
|         }; | |
|     } | |
| };
 |