From 34f0ce9dedcb398caf417a7a0d8afdec599be34f Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 7 Nov 2021 12:19:35 -0800 Subject: [PATCH] feat: Add rule no-constant-binary-expression I proposed the core idea of this rule in https://github.com/eslint/eslint/issues/13752 as an addition to `no-constant-condition`, but on the advice of the TSC, it was restructured as a standalone rule. --- docs/rules/no-constant-binary-expression.md | 66 +++ docs/rules/no-constant-condition.md | 4 + lib/rules/index.js | 1 + lib/rules/no-constant-binary-expression.js | 506 ++++++++++++++++++ .../rules/no-constant-binary-expression.js | 289 ++++++++++ tools/rule-types.json | 1 + 6 files changed, 867 insertions(+) create mode 100644 docs/rules/no-constant-binary-expression.md create mode 100644 lib/rules/no-constant-binary-expression.js create mode 100644 tests/lib/rules/no-constant-binary-expression.js diff --git a/docs/rules/no-constant-binary-expression.md b/docs/rules/no-constant-binary-expression.md new file mode 100644 index 000000000000..f58be2416e36 --- /dev/null +++ b/docs/rules/no-constant-binary-expression.md @@ -0,0 +1,66 @@ +# disallow expressions where the operation doesn’t affect the value (no-constant-binary-expression) + +Comparisons which will always evaluate to true or 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. For example: + +```js +// One might think this would evaluate as `x + (b ?? c)`: +const x = a + b ?? c; + +// But it actually evaluates as `(a + b) ?? c`. Since `a + b` can never be null, +// the `?? c` has no effect. +``` + +Additionally, we detect comparisons to newly constructed objects/arrays/functions/etc. In JavaScript, where objects are compared by reference, a newly constructed object can _never_ `===` any other value. This can be surprising for programmers coming from languages where objects are compared by value. + +```js +// Programmers coming from a language where objects are compared by value might expect this to work: +const isEmpty = x === []; + +// However, this will always result in `isEmpty` being `false`. +``` + +## Rule Details + +This rule identifies `==` and `===` comparisons which, based on the semantics of the JavaScript language, will always evaluate to `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-expression: "error"*/ +const value1 = +x == null; + +const value2 = condition ? x : {} || DEFAULT; + +const value3 = !foo == null; + +const value4 = new Boolean(foo) === true; + +const arrIsEmpty = someObj === {}; + +const arrIsEmpty = someArr === []; +``` + +Examples of **correct** code for this rule: + +```js +/*eslint no-constant-binary-expression: "error"*/ +const value1 = x == null; + +const value2 = (condition ? x : {}) || DEFAULT; + +const value3 = !(foo == null); + +const value4 = Boolean(foo) === true; + +const arrIsEmpty = Object.keys(someObj).length === 0; + +const arrIsEmpty = someArr.length === 0; +``` + +Related Rules: + +* [`no-constant-condition`](https://eslint.org/docs/rules/no-constant-condition) diff --git a/docs/rules/no-constant-condition.md b/docs/rules/no-constant-condition.md index 810c17bead15..a8ea357bdc5b 100644 --- a/docs/rules/no-constant-condition.md +++ b/docs/rules/no-constant-condition.md @@ -125,3 +125,7 @@ do { } } while (true) ``` + +## Related Rules + +* [`no-constant-binary-expression`](https://eslint.org/docs/rules/no-constant-binary-expression) diff --git a/lib/rules/index.js b/lib/rules/index.js index 130b635c9727..aef47f5cadcf 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-expression": () => require("./no-constant-binary-expression"), "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-expression.js b/lib/rules/no-constant-binary-expression.js new file mode 100644 index 000000000000..aa2da86e7d86 --- /dev/null +++ b/lib/rules/no-constant-binary-expression.js @@ -0,0 +1,506 @@ +/** + * @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"); + +const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&"]); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Test if an AST node has a statically knowable constant truthiness. Meaning, + * it will always coerce to either `true` or `false` when cast directly to + * boolean. + * @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.type === "Identifier" && 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": + + /* + * Possible future direction if needed: 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 + * Possible future direction if needed: 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 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 {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": { + if (node.callee.type !== "Identifier") { + return false; + } + const functionName = node.callee.name; + + return (functionName === "Boolean" || functionName === "String" || functionName === "Number"); + } + case "AssignmentExpression": + if (node.operator === "=") { + return hasConstantNullishness(node.right); + } + + /* + * Handling short-circuiting 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. 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 {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.type === "Identifier" && 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 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 {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": + 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": + if (node.elements.length === 1) { + + /* + * 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 false; + } + 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 "NewExpression": // Objects might have custom `.valueOf` or `.toString`. + return false; + case "CallExpression": { + if (node.callee.type === "Identifier" && 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 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(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(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-circuiting 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; + } +} + +/** + * Test if an AST node will always result in a newly constructed object + * @param {ASTNode} node The node to test + * @returns {boolean} Will `node` always be new? + */ +function isAlwaysNew(node) { + switch (node.type) { + case "ObjectExpression": + case "ArrayExpression": + case "ArrowFunctionExpression": + case "FunctionExpression": + case "ClassExpression": + case "NewExpression": + return true; + 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(last); + } + case "AssignmentExpression": + if (node.operator === "=") { + return isAlwaysNew(node.right); + } + return false; + case "ConditionalExpression": + return isAlwaysNew(node.consequent) && isAlwaysNew(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 {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)) || + + /* + * If both sides are "new", then both sides are objects and + * therefore they will be compared by reference even with `==` + * equality. + */ + (isAlwaysNew(a) && isAlwaysNew(b)) + ) { + return b; + } + } else if (operator === "===" || operator === "!==") { + if ( + (isNullOrUndefined(a) && hasConstantNullishness(b)) || + (isStaticBoolean(a) && hasConstantStrictBooleanComparison(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/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." + } + }, + + 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" } }); + } else if (operator === "===" || operator === "!==") { + if (isAlwaysNew(left)) { + context.report({ node: left, messageId: "alwaysNew" }); + } else if (isAlwaysNew(right)) { + context.report({ node: right, messageId: "alwaysNew" }); + } + } + + } + + /* + * 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-expression.js b/tests/lib/rules/no-constant-binary-expression.js new file mode 100644 index 000000000000..f674c600db33 --- /dev/null +++ b/tests/lib/rules/no-constant-binary-expression.js @@ -0,0 +1,289 @@ +/** + * @fileoverview Tests for no-constant-binary-expression rule. + * @author Jordan Eldredge + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-constant-binary-expression"); +const { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2021, ecmaFeatures: { jsx: true } } }); + +ruleTester.run("no-constant-binary-expression", 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`. + "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", + + "foo.Boolean(true) && foo" + ], + 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 binary expression. Compares constantly with the right-hand side of the `==`." }] }, + { code: "true == []", errors: [{ message: "Unexpected constant binary expression. Compares constantly with the left-hand side of the `==`." }] }, + { code: "[] != true", errors: [{ message: "Unexpected constant binary expression. Compares constantly with the right-hand side of the `!=`." }] }, + { code: "[] === true", errors: [{ message: "Unexpected constant binary expression. Compares constantly with the right-hand side of the `===`." }] }, + { code: "[] !== true", errors: [{ message: "Unexpected constant binary expression. 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: "(class { valueOf() { return x; } }) && foo", errors: [{ messageId: "constantShortCircuit" }] }, + { code: "(class { [x]() { return x; } }) && 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" }] }, + + /* + * If both sides are newly constructed objects, we can tell they will + * never be equal, even with == equality. + */ + { code: "[a] == [a]", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "[a] != [a]", errors: [{ messageId: "constantBinaryOperand" }] }, + { code: "({}) == []", errors: [{ messageId: "constantBinaryOperand" }] }, + + // Comparing to always new objects + { code: "x === {}", errors: [{ messageId: "alwaysNew" }] }, + { code: "x !== {}", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === []", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === (() => {})", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === (function() {})", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === (class {})", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === new Foo()", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === (foo, {})", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === (y = {})", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === (y ? {} : [])", errors: [{ messageId: "alwaysNew" }] }, + { code: "x === /[a-z]/", errors: [{ messageId: "alwaysNew" }] }, + + // It's not obvious what this does, but it compares the old value of `x` to the new object. + { code: "x === (x = {})", errors: [{ messageId: "alwaysNew" }] } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 85484c49210e..d69028448a1f 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -90,6 +90,7 @@ "no-confusing-arrow": "suggestion", "no-console": "suggestion", "no-const-assign": "problem", + "no-constant-binary-expression": "problem", "no-constant-condition": "problem", "no-constructor-return": "problem", "no-continue": "suggestion",