Permalink
Cannot retrieve contributors at this time
509 lines (463 sloc)
20.4 KB
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
codeql-action/node_modules/eslint/lib/rules/no-constant-binary-expression.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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() {}, | |
*/ | |
}; | |
} | |
}; |