123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- "use strict";
- const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator, ECMASCRIPT_GLOBALS } = require("./utils/ast-utils");
- const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
- function isNullOrUndefined(scope, node) {
- return (
- isNullLiteral(node) ||
- (node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||
- (node.type === "UnaryExpression" && node.operator === "void")
- );
- }
- function hasConstantNullishness(scope, node, nonNullish) {
- if (nonNullish && isNullOrUndefined(scope, node)) {
- return false;
- }
- switch (node.type) {
- case "ObjectExpression":
- case "ArrayExpression":
- case "ArrowFunctionExpression":
- case "FunctionExpression":
- case "ClassExpression":
- case "NewExpression":
- case "Literal":
- case "TemplateLiteral":
- case "UpdateExpression":
- case "BinaryExpression":
- 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);
- }
-
- if (isLogicalAssignmentOperator(node.operator)) {
- return false;
- }
-
- return true;
- case "UnaryExpression":
-
- 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":
- case "JSXFragment":
- return false;
- default:
- return false;
- }
- }
- 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;
- }
- }
- function hasConstantLooseBooleanComparison(scope, node) {
- switch (node.type) {
- case "ObjectExpression":
- case "ClassExpression":
-
- return true;
- case "ArrayExpression": {
- const nonSpreadElements = node.elements.filter(e =>
-
- e !== null && e.type !== "SpreadElement");
-
- return node.elements.length === 0 || nonSpreadElements.length > 1;
- }
- case "ArrowFunctionExpression":
- case "FunctionExpression":
- return true;
- case "UnaryExpression":
- if (node.operator === "void" ||
- node.operator === "typeof"
- ) {
- return true;
- }
- if (node.operator === "!") {
- return isConstant(scope, node.argument, true);
- }
-
- return false;
- case "NewExpression":
- 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":
- return true;
- case "Identifier":
- return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
- case "TemplateLiteral":
-
- return node.expressions.length === 0;
- case "AssignmentExpression":
- if (node.operator === "=") {
- return hasConstantLooseBooleanComparison(scope, node.right);
- }
-
- return false;
- case "SequenceExpression": {
- const last = node.expressions.at(-1);
- return hasConstantLooseBooleanComparison(scope, last);
- }
- case "JSXElement":
- case "JSXFragment":
- return false;
- default:
- return false;
- }
- }
- function hasConstantStrictBooleanComparison(scope, node) {
- switch (node.type) {
- case "ObjectExpression":
- case "ArrayExpression":
- case "ArrowFunctionExpression":
- case "FunctionExpression":
- case "ClassExpression":
- case "NewExpression":
- case "TemplateLiteral":
- case "Literal":
- case "UpdateExpression":
- 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);
- }
-
- 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);
- }
-
- if (isLogicalAssignmentOperator(node.operator)) {
- return false;
- }
-
- 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":
- case "JSXFragment":
- return false;
- default:
- return false;
- }
- }
- 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;
- }
-
- return Object.hasOwn(ECMASCRIPT_GLOBALS, node.callee.name) &&
- isReferenceToGlobalVariable(scope, node.callee);
- }
- case "Literal":
-
- 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":
- case "JSXFragment":
- return false;
- default:
- return false;
- }
- }
- 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;
- }
- 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 (isAlwaysNew(scope, left) && isAlwaysNew(scope, right)) {
- context.report({ node: left, messageId: "bothAlwaysNew" });
- }
- }
- }
-
- };
- }
- };
|