diff --git a/docs/rules/no-constant-binary-operand.md b/docs/rules/no-constant-binary-operand.md new file mode 100644 index 000000000000..91a1d276d4e2 --- /dev/null +++ b/docs/rules/no-constant-binary-operand.md @@ -0,0 +1,41 @@ +# disallow constant comparisons and expressions that always/never short circuit (no-constant-binary-operand) + +Comparisons which either will always evaluates to true, or always evaluates to false, and logical expressions (`||`, `&&`, `||`) which either always short circuit or never short circuit are both likely indications of programmer error. + +These errors are especially common in complex expressions where operator precedence is easy to misjudge. + +## Rule Details + +This rule identifies `==` and `===` comparisons which, based on the semantics of the JavaScript language, will always evaluate a constant `true` or `false`. + +It also identifies `||`, `&&` and `??` logical expressions which will either always, or never short circuit. + +Examples of **incorrect** code for this rule: + +```js +/*eslint no-constant-binary-operand: "error"*/ +const value1 = +x == null; + +const value2 = condition ? x : {} || DEFAULT; + +const value3 = !foo == null; + +const value4 = new Boolean(foo) === true +``` + +Examples of **correct** code for this rule: + +```js +/*eslint no-constant-binary-operand: "error"*/ +const value1 = x == null; + +const value2 = (condition ? x : {}) || DEFAULT; + +const value3 = !(foo == null); + +const value4 = Boolean(foo) === true +``` + +See Also: + +- [`no-constant-condition`](https://eslint.org/docs/rules/no-constant-condition) which disallows constant values as test conditions. diff --git a/docs/rules/no-constant-condition.md b/docs/rules/no-constant-condition.md index 036bbb786b92..58ba36e3beca 100644 --- a/docs/rules/no-constant-condition.md +++ b/docs/rules/no-constant-condition.md @@ -75,6 +75,10 @@ do { var result = x !== 0 ? a : b; ``` +See Also: + +* [`no-constant-binary-operand`](https://eslint.org/docs/rules/no-constant-binary-operand) which disallows constant comparisons and logical expressions (`||`, `&&`, `??`) which always or never short circuit. + ## Options ### checkLoops diff --git a/lib/rules/index.js b/lib/rules/index.js index c88febd85b9f..14c150229d2c 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -103,6 +103,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "no-confusing-arrow": () => require("./no-confusing-arrow"), "no-console": () => require("./no-console"), "no-const-assign": () => require("./no-const-assign"), + "no-constant-binary-operand": () => require("./no-constant-binary-operand"), "no-constant-condition": () => require("./no-constant-condition"), "no-constructor-return": () => require("./no-constructor-return"), "no-continue": () => require("./no-continue"), diff --git a/lib/rules/no-constant-binary-operand.js b/lib/rules/no-constant-binary-operand.js new file mode 100644 index 000000000000..664c00db3911 --- /dev/null +++ b/lib/rules/no-constant-binary-operand.js @@ -0,0 +1,431 @@ +/** + * @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit + * @author Jordan Eldredge + */ + +"use strict"; + +const { isNullOrUndefined } = require("./utils/ast-utils"); + +// Object keys which, if redefined, could impact how equality is computed. +const OBJECT_EQUALITY_KEYS = new Set(["valueOf", "toString", "__proto__"]); +const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&"]); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Test if an AST node has a statically knowable constant truthiness. Meaning, + * it will alwasy coerce to either `true` or `false`. + * @param {ASTNode} node The AST node being tested. + * @returns {boolean} Does `node` have constant truthiness? + */ +function hasConstantTruthiness(node) { + switch (node.type) { + case "ObjectExpression": // Objects are always truthy + case "ArrayExpression": // Arrays are always truthy + case "ArrowFunctionExpression": // Functions are always truthy + case "FunctionExpression": // Functions are always truthy + case "ClassExpression": // Classes are always truthy + case "NewExpression": // Objects are always truthy + case "Literal": // Truthy, or falsy, literals never change + return true; + case "CallExpression": { + if (node.callee.name === "Boolean") { + return node.arguments.length === 0 || hasConstantTruthiness(node.arguments[0]); + } + return false; + } + case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior. + case "JSXFragment": + return false; + case "AssignmentExpression": + if (node.operator !== "=") { + return false; // We won't go so far as to try to evaluate += etc. + } + return hasConstantTruthiness(node.right); + case "TemplateLiteral": + + /* + * TODO: If all quasis are empty, we could look at node.expressions + * and try to determine if they are static truthinesss. + */ + return node.quasis.some(quasi => quasi.value.cooked.length); + case "UnaryExpression": + if (node.operator === "void" || // Always returns `undefined` + node.operator === "typeof" // All type strings are truthy + ) { + return true; + } + if (node.operator === "!") { + return hasConstantTruthiness(node.argument); + } + + /* + * 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 "SequenceExpression": { + const last = node.expressions[node.expressions.length - 1]; + + return hasConstantTruthiness(last); + } + case "Identifier": { + return node.name === "undefined"; + } + + default: + return false; + } +} + +/** + * Test if an AST node has a statically knowable constant nullishness. Meaning, + * it will alwasy resolve to a constatn 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 {ASTNode} node The AST node being tested. + * @returns {boolean} Does `node` have constant nullishness? + */ +function hasConstantNullishness(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": { + const functionName = node.callee.name; + + return (functionName === "Boolean" || functionName === "String" || functionName === "Number"); + } + case "AssignmentExpression": + if (node.operator === "=") { + return hasConstantNullishness(node.right); + } + + /* + * Handling short-circuting assignment operators would require + * walking the scope. We won't attempt that (for now...) / + */ + if ( + node.operator === "&&=" || + node.operator === "||=" || + 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(last); + } + case "Identifier": { + return node.name === "undefined"; + } + 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 + * @param {ASTNode} node The node to test + * @returns {boolean} Is `node` guaranteed to be a boolean? + */ +function isStaticBoolean(node) { + switch (node.type) { + case "Literal": + return typeof node.value === "boolean"; + case "CallExpression": + return node.callee.name === "Boolean" && + (node.arguments.length === 0 || hasConstantTruthiness(node.arguments[0])); + case "UnaryExpression": + return node.operator === "!" && hasConstantTruthiness(node.argument); + default: + return false; + } +} + + +/** + * Test if an AST node will always give the same result when compared to a + * bolean value. Note that comparison to boolean values is different to + * 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 {ASTNode} node The node to test + * @returns {boolean} Will `node` always coerce to the same boolean value? + */ +function hasConstantLooseBooleanComparison(node) { + switch (node.type) { + case "ObjectExpression": + return !node.properties.some(prop => OBJECT_EQUALITY_KEYS.has(prop.key.name)); + case "ArrayExpression": + if (node.elements.length === 1) { + + /* + * TODO: We could check if the single value would result in + * variable boolean comparison. + */ + + return false; // `[0]` and `[1]` would compare differently. + } + return true; + case "ArrowFunctionExpression": + case "FunctionExpression": + return true; + case "UnaryExpression": + if (node.operator === "void" || // Always returns `undefined` + node.operator === "typeof" // All type strings are truthy + ) { + return true; + } + if (node.operator === "!") { + return hasConstantTruthiness(node.argument); + } + + /* + * 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 "ClassExpression": + return !node.body.body.some(prop => OBJECT_EQUALITY_KEYS.has(prop.key.name)); + case "NewExpression": // Objects might have custom `.valueOf` or `.toString`. + return false; + case "CallExpression": { + if (node.callee.name === "Boolean") { + return node.arguments.length === 0 || hasConstantTruthiness(node.arguments[0]); + } + return false; + } + case "Literal": // True or false, literals never change + return true; + case "Identifier": + return node.name === "undefined"; + case "TemplateLiteral": + + /* + * In theory we could try to check if the quasi are sufficient to + * prove that the expression will alwasy 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(node.right); + } + + /* + * Handling short-circuting 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(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 bolean value. This can happen if the expression can never be boolean, or + * if it is always the same boolean value. + * @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(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 hasConstantTruthiness(node.argument); + } + + /* + * 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(last); + } + case "Identifier": + return node.name === "undefined"; + case "AssignmentExpression": + if (node.operator === "=") { + return hasConstantStrictBooleanComparison(node.right); + } + + /* + * Handling short-circuting assignment operators would require + * walking the scope. We won't attempt that (for now...) + */ + if (node.operator === "&&=" || node.operator === "||=" || 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 "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 {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(a, b, operator) { + if (operator === "==" || operator === "!=") { + if ( + (isNullOrUndefined(a) && hasConstantNullishness(b)) || + (isStaticBoolean(a) && hasConstantLooseBooleanComparison(b)) + ) { + return b; + } + } else if (operator === "===" || operator === "!==") { + if ( + (isNullOrUndefined(a) && hasConstantNullishness(b)) || + (isStaticBoolean(a) && hasConstantStrictBooleanComparison(b)) + ) { + return b; + } + } + return null; +} + + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "problem", + docs: { + description: "disallow binary expressions which evaluate to a constant", + recommended: true, + url: "https://eslint.org/docs/rules/no-constant-binary-operand" + }, + schema: [], + messages: { + constantBinaryOperand: "Unexpected constant operand. Compares constantly with the {{otherSide}}-hand side of the `{{operator}}`.", + constantShortCircuit: "Unexpected constant {{property}} on the left-hand side of a `{{operator}}` expression." + } + }, + + create(context) { + return { + LogicalExpression(node) { + const { operator, left } = node; + + if ((operator === "&&" || operator === "||") && hasConstantTruthiness(left)) { + context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } }); + } else if (operator === "??" && hasConstantNullishness(left)) { + context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } }); + } + }, + BinaryExpression(node) { + + const { right, left, operator } = node; + const rightConstantOperand = findBinaryExpressionConstantOperand(left, right, operator); + const leftConstantOperand = findBinaryExpressionConstantOperand(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" } }); + } + + } + + /* + * In theory we could handle short circuting 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() {}, + */ + }; + } +}; diff --git a/tests/lib/rules/no-constant-binary-operand.js b/tests/lib/rules/no-constant-binary-operand.js new file mode 100644 index 000000000000..0ebcd6b24a2a --- /dev/null +++ b/tests/lib/rules/no-constant-binary-operand.js @@ -0,0 +1,269 @@ +/** + * @fileoverview Tests for no-constant-binary-operand rule. + * @author Jordan Eldredge + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-constant-binary-operand"); +const { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2021, ecmaFeatures: { jsx: true } } }); + +const invalid = [ + + // Error messages + { code: "[] && greeting", errors: [{ message: "Unexpected constant truthiness on the left-hand side of a `&&` expression." }] }, + { code: "[] || greeting", errors: [{ message: "Unexpected constant truthiness on the left-hand side of a `||` expression." }] }, + { code: "[] ?? greeting", errors: [{ message: "Unexpected constant nullishness on the left-hand side of a `??` expression." }] }, + { code: "[] == true", errors: [{ message: "Unexpected constant operand. Compares constantly with the right-hand side of the `==`." }] }, + { code: "true == []", errors: [{ message: "Unexpected constant operand. Compares constantly with the left-hand side of the `==`." }] }, + { code: "[] != true", errors: [{ message: "Unexpected constant operand. Compares constantly with the right-hand side of the `!=`." }] }, + { code: "[] === true", errors: [{ message: "Unexpected constant operand. Compares constantly with the right-hand side of the `===`." }] }, + { code: "[] !== true", errors: [{ message: "Unexpected constant operand. Compares constantly with the right-hand side of the `!==`." }] }, + + // Motivating examples from the original proposal https://github.com/eslint/eslint/issues/13752 + { code: "!foo == null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "!foo ?? bar", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a + b) / 2 ?? bar", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "String(foo.bar) ?? baz", errors: [{ messageId: "constantShortCircuit" }] }, + { code: '"hello" + name ?? ""', errors: [{ messageId: "constantShortCircuit" }] }, + { code: '[foo?.bar ?? ""] ?? []', errors: [{ messageId: "constantShortCircuit" }] }, + + // Logical expression with constant truthiness + { code: "true && hello", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "true || hello", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "true && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "'' && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "100 && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "/[a-z]/ && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "Boolean([]) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "Boolean() && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "Boolean([], n) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "({}) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "[] && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(() => {}) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(function() {}) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(class {}) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "new Foo() && foo", errors: [{ messageId: "constantShortCircuit" }] }, + + // (boxed values are always truthy) + { code: "new Boolean(unknown) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(bar = false) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(bar.baz = false) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(bar[0] = false) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "`hello ${hello}` && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "void bar && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "!true && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "typeof bar && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(bar, baz, true) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "undefined && foo", errors: [{ messageId: "constantShortCircuit" }] }, + + // Logical expression with constant nullishness + { code: "({}) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "([]) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(() => {}) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(function() {}) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(class {}) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "new Foo() ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "1 ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "/[a-z]/ ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "`${''}` ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a = true) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a += 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a -= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a *= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a /= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a %= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a <<= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a >>= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a >>>= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a |= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a ^= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(a &= 1) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "undefined ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "!bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "void bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "typeof bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "+bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "-bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "~bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "++bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "bar++ ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "--bar ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "bar-- ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(x == y) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(x + y) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(x / y) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(x instanceof String) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(x in y) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "Boolean(x) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "String(x) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "Number(x) ?? foo", errors: [{ messageId: "constantShortCircuit" }] }, + + // Binary expression with comparison to null + { code: "({}) != null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "null == ({})", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "undefined == ({})", errors: [{ messageId: "constantBinaryOperand" }] }, + + // Binary expression with loose comparison to boolean + { code: "({}) != true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "([]) == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "([a, b]) == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(() => {}) == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(function() {}) == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "void foo == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "typeof foo == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "![] == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "true == class {}", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "true == 1", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "undefined == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "true == undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "`hello` == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "/[a-z]/ == true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == Boolean({})", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == Boolean()", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == Boolean(() => {}, foo)", errors: [{ messageId: "constantBinaryOperand" }] }, + + // Binary expression with strict comparison to boolean + { code: "({}) !== true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == !({})", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "([]) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(function() {}) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(() => {}) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "!{} === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "typeof n === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "void n === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "+n === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "-n === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "~n === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "true === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "1 === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "'hello' === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "/[a-z]/ === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "undefined === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a = {}) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a += 1) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a -= 1) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a *= 1) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a %= 1) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "--a === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "a-- === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "++a === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "a++ === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a + b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a - b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a * b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a / b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a % b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a | b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a ^ b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a & b) === true", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "Boolean(0) == !({})", errors: [{ messageId: "constantBinaryOperand" }] }, + + // Binary expression with strict comparison to null + { code: "({}) !== null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "([]) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(() => {}) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(function() {}) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(class {}) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "new Foo() === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "`` === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "1 === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "'hello' === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "/[a-z]/ === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "true === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "null === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "a++ === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "++a === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "--a === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "a-- === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "!a === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "typeof a === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "delete a === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "void a === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "undefined === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(x = {}) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(x += y) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(x -= y) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a, b, {}) === null", errors: [{ messageId: "constantBinaryOperand" }] }, + + // Binary expression with strict comparison to undefined + { code: "({}) !== undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "([]) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(() => {}) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(function() {}) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(class {}) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "new Foo() === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "`` === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "1 === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "'hello' === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "/[a-z]/ === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "true === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "null === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "a++ === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "++a === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "--a === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "a-- === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "!a === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "typeof a === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "delete a === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "void a === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "undefined === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(x = {}) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(x += y) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(x -= y) === undefined", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "(a, b, {}) === undefined", errors: [{ messageId: "constantBinaryOperand" }] } +]; + +ruleTester.run("no-constant-binary-operand", rule, { + valid: [ + + // While this _would_ be a constant condition in React, ESLint has a polciy of not attributing any specific behavior to JSX. + "

&& foo", + "<> && foo", + "

?? foo", + "<> ?? foo", + "arbitraryFunction(n) ?? foo", + "foo.Boolean(n) ?? foo", + "(x += 1) && foo", + "`${bar}` && foo", + "bar && foo", + "+1 && foo", + "-1 && foo", + "~1 && foo", + "delete bar.baz && foo", + "true ? foo : bar", // We leave ConditionalExpression for `no-constant-condition`. + "({valueOf: () => Math.random()}) == true", + "({toString: () => Math.random()}) == true", + "({__proto__: someObj}) == true", + + "true == class {valueOf() { return Math.random() }}", + "true == class {toString() { return Math.random() }}", + "new Foo() == true", + "foo == true", + "`${foo}` == true", + "`${foo}${bar}` == true", + "`0${foo}` == true", + "`00000000${foo}` == true", + "`0${foo}.000` == true", + "[n] == true", + + "delete bar.baz === true" + ], + invalid +});