codtracker-js/node_modules/eslint/lib/rules/no-constant-binary-expression.js
2025-04-19 23:12:19 -04:00

597 lines
19 KiB
JavaScript

/**
* @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit
* @author Jordan Eldredge <https://jordaneldredge.com>
*/
'use strict';
const {
isNullLiteral,
isConstant,
isReferenceToGlobalVariable,
isLogicalAssignmentOperator,
ECMASCRIPT_GLOBALS,
} = 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.at(-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.at(-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.at(-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.hasOwn(ECMASCRIPT_GLOBALS, 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.at(-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('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description:
"Disallow expressions where the operation doesn't affect the value",
recommended: true,
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
* dependent on https://github.com/eslint/eslint/issues/13776
*
* AssignmentExpression() {},
*/
};
},
};