From 8957a03032a59b82e6bbf557843a0af312affa15 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Sat, 25 May 2024 02:32:22 +0800 Subject: [PATCH] Add `no-negation-in-equality-check` rule (#2353) --- docs/rules/no-negation-in-equality-check.md | 30 ++ readme.md | 1 + rules/no-negation-in-equality-check.js | 104 +++++++ test/no-negation-in-equality-check.mjs | 50 ++++ .../no-negation-in-equality-check.mjs.md | 268 ++++++++++++++++++ .../no-negation-in-equality-check.mjs.snap | Bin 0 -> 799 bytes 6 files changed, 453 insertions(+) create mode 100644 docs/rules/no-negation-in-equality-check.md create mode 100644 rules/no-negation-in-equality-check.js create mode 100644 test/no-negation-in-equality-check.mjs create mode 100644 test/snapshots/no-negation-in-equality-check.mjs.md create mode 100644 test/snapshots/no-negation-in-equality-check.mjs.snap diff --git a/docs/rules/no-negation-in-equality-check.md b/docs/rules/no-negation-in-equality-check.md new file mode 100644 index 0000000000..248261ebeb --- /dev/null +++ b/docs/rules/no-negation-in-equality-check.md @@ -0,0 +1,30 @@ +# Disallow negated expression in equality check + +πŸ’Ό This rule is enabled in the βœ… `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs). + +πŸ’‘ This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). + + + + +Using a negated expression in equality check is most likely a mistake. + +## Fail + +```js +if (!foo === bar) {} +``` + +```js +if (!foo !== bar) {} +``` + +## Pass + +```js +if (foo !== bar) {} +``` + +```js +if (!(foo === bar)) {} +``` diff --git a/readme.md b/readme.md index f21ccaae94..163a4b7572 100644 --- a/readme.md +++ b/readme.md @@ -145,6 +145,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c | [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. | βœ… | πŸ”§ | | | [no-magic-array-flat-depth](docs/rules/no-magic-array-flat-depth.md) | Disallow a magic number as the `depth` argument in `Array#flat(…).` | βœ… | | | | [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. | βœ… | πŸ”§ | | +| [no-negation-in-equality-check](docs/rules/no-negation-in-equality-check.md) | Disallow negated expression in equality check. | βœ… | | πŸ’‘ | | [no-nested-ternary](docs/rules/no-nested-ternary.md) | Disallow nested ternary expressions. | βœ… | πŸ”§ | | | [no-new-array](docs/rules/no-new-array.md) | Disallow `new Array()`. | βœ… | πŸ”§ | πŸ’‘ | | [no-new-buffer](docs/rules/no-new-buffer.md) | Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. | βœ… | πŸ”§ | πŸ’‘ | diff --git a/rules/no-negation-in-equality-check.js b/rules/no-negation-in-equality-check.js new file mode 100644 index 0000000000..9bcdbb8115 --- /dev/null +++ b/rules/no-negation-in-equality-check.js @@ -0,0 +1,104 @@ +'use strict'; +const { + fixSpaceAroundKeyword, + addParenthesizesToReturnOrThrowExpression, +} = require('./fix/index.js'); +const { + needsSemicolon, + isParenthesized, + isOnSameLine, +} = require('./utils/index.js'); + +const MESSAGE_ID_ERROR = 'no-negation-in-equality-check/error'; +const MESSAGE_ID_SUGGESTION = 'no-negation-in-equality-check/suggestion'; +const messages = { + [MESSAGE_ID_ERROR]: 'Negated expression in not allowed in equality check.', + [MESSAGE_ID_SUGGESTION]: 'Switch to \'{{operator}}\' check.', +}; + +const EQUALITY_OPERATORS = new Set([ + '===', + '!==', + '==', + '!=', +]); + +const isEqualityCheck = node => node.type === 'BinaryExpression' && EQUALITY_OPERATORS.has(node.operator); +const isNegatedExpression = node => node.type === 'UnaryExpression' && node.prefix && node.operator === '!'; + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => ({ + BinaryExpression(binaryExpression) { + const {operator, left} = binaryExpression; + + if ( + !isEqualityCheck(binaryExpression) + || !isNegatedExpression(left) + ) { + return; + } + + const {sourceCode} = context; + const bangToken = sourceCode.getFirstToken(left); + const negatedOperator = `${operator.startsWith('!') ? '=' : '!'}${operator.slice(1)}`; + + return { + node: bangToken, + messageId: MESSAGE_ID_ERROR, + /** @param {import('eslint').Rule.RuleFixer} fixer */ + suggest: [ + { + messageId: MESSAGE_ID_SUGGESTION, + data: { + operator: negatedOperator, + }, + /** @param {import('eslint').Rule.RuleFixer} fixer */ + * fix(fixer) { + yield * fixSpaceAroundKeyword(fixer, binaryExpression, sourceCode); + + const tokenAfterBang = sourceCode.getTokenAfter(bangToken); + + const {parent} = binaryExpression; + if ( + (parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement') + && !isParenthesized(binaryExpression, sourceCode) + ) { + const returnToken = sourceCode.getFirstToken(parent); + if (!isOnSameLine(returnToken, tokenAfterBang)) { + yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode); + } + } + + yield fixer.remove(bangToken); + + const previousToken = sourceCode.getTokenBefore(bangToken); + if (needsSemicolon(previousToken, sourceCode, tokenAfterBang.value)) { + yield fixer.insertTextAfter(bangToken, ';'); + } + + const operatorToken = sourceCode.getTokenAfter( + left, + token => token.type === 'Punctuator' && token.value === operator, + ); + yield fixer.replaceText(operatorToken, negatedOperator); + }, + }, + ], + }; + }, +}); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + create, + meta: { + type: 'problem', + docs: { + description: 'Disallow negated expression in equality check.', + recommended: true, + }, + + hasSuggestions: true, + messages, + }, +}; diff --git a/test/no-negation-in-equality-check.mjs b/test/no-negation-in-equality-check.mjs new file mode 100644 index 0000000000..70d0e58025 --- /dev/null +++ b/test/no-negation-in-equality-check.mjs @@ -0,0 +1,50 @@ +import outdent from 'outdent'; +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +test.snapshot({ + valid: [ + '!foo instanceof bar', + '+foo === bar', + '!(foo === bar)', + // We are not checking right side + 'foo === !bar', + ], + invalid: [ + '!foo === bar', + '!foo !== bar', + '!foo == bar', + '!foo != bar', + outdent` + function x() { + return!foo === bar; + } + `, + outdent` + function x() { + return! + foo === bar; + throw! + foo === bar; + } + `, + outdent` + foo + !(a) === b + `, + outdent` + foo + ![a, b].join('') === c + `, + outdent` + foo + ! [a, b].join('') === c + `, + outdent` + foo + !/* comment */[a, b].join('') === c + `, + '!!foo === bar', + ], +}); diff --git a/test/snapshots/no-negation-in-equality-check.mjs.md b/test/snapshots/no-negation-in-equality-check.mjs.md new file mode 100644 index 0000000000..6888103721 --- /dev/null +++ b/test/snapshots/no-negation-in-equality-check.mjs.md @@ -0,0 +1,268 @@ +# Snapshot report for `test/no-negation-in-equality-check.mjs` + +The actual snapshot is saved in `no-negation-in-equality-check.mjs.snap`. + +Generated by [AVA](https://avajs.dev). + +## invalid(1): !foo === bar + +> Input + + `␊ + 1 | !foo === bar␊ + ` + +> Error 1/1 + + `␊ + > 1 | !foo === bar␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | foo !== bar␊ + ` + +## invalid(2): !foo !== bar + +> Input + + `␊ + 1 | !foo !== bar␊ + ` + +> Error 1/1 + + `␊ + > 1 | !foo !== bar␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '===' check.␊ + 1 | foo === bar␊ + ` + +## invalid(3): !foo == bar + +> Input + + `␊ + 1 | !foo == bar␊ + ` + +> Error 1/1 + + `␊ + > 1 | !foo == bar␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!=' check.␊ + 1 | foo != bar␊ + ` + +## invalid(4): !foo != bar + +> Input + + `␊ + 1 | !foo != bar␊ + ` + +> Error 1/1 + + `␊ + > 1 | !foo != bar␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '==' check.␊ + 1 | foo == bar␊ + ` + +## invalid(5): function x() { return!foo === bar; } + +> Input + + `␊ + 1 | function x() {␊ + 2 | return!foo === bar;␊ + 3 | }␊ + ` + +> Error 1/1 + + `␊ + 1 | function x() {␊ + > 2 | return!foo === bar;␊ + | ^ Negated expression in not allowed in equality check.␊ + 3 | }␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | function x() {␊ + 2 | return foo !== bar;␊ + 3 | }␊ + ` + +## invalid(6): function x() { return! foo === bar; throw! foo === bar; } + +> Input + + `␊ + 1 | function x() {␊ + 2 | return!␊ + 3 | foo === bar;␊ + 4 | throw!␊ + 5 | foo === bar;␊ + 6 | }␊ + ` + +> Error 1/2 + + `␊ + 1 | function x() {␊ + > 2 | return!␊ + | ^ Negated expression in not allowed in equality check.␊ + 3 | foo === bar;␊ + 4 | throw!␊ + 5 | foo === bar;␊ + 6 | }␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | function x() {␊ + 2 | return (␊ + 3 | foo !== bar);␊ + 4 | throw!␊ + 5 | foo === bar;␊ + 6 | }␊ + ` + +> Error 2/2 + + `␊ + 1 | function x() {␊ + 2 | return!␊ + 3 | foo === bar;␊ + > 4 | throw!␊ + | ^ Negated expression in not allowed in equality check.␊ + 5 | foo === bar;␊ + 6 | }␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | function x() {␊ + 2 | return!␊ + 3 | foo === bar;␊ + 4 | throw (␊ + 5 | foo !== bar);␊ + 6 | }␊ + ` + +## invalid(7): foo !(a) === b + +> Input + + `␊ + 1 | foo␊ + 2 | !(a) === b␊ + ` + +> Error 1/1 + + `␊ + 1 | foo␊ + > 2 | !(a) === b␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | foo␊ + 2 | ;(a) !== b␊ + ` + +## invalid(8): foo ![a, b].join('') === c + +> Input + + `␊ + 1 | foo␊ + 2 | ![a, b].join('') === c␊ + ` + +> Error 1/1 + + `␊ + 1 | foo␊ + > 2 | ![a, b].join('') === c␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | foo␊ + 2 | ;[a, b].join('') !== c␊ + ` + +## invalid(9): foo ! [a, b].join('') === c + +> Input + + `␊ + 1 | foo␊ + 2 | ! [a, b].join('') === c␊ + ` + +> Error 1/1 + + `␊ + 1 | foo␊ + > 2 | ! [a, b].join('') === c␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | foo␊ + 2 | ; [a, b].join('') !== c␊ + ` + +## invalid(10): foo !/* comment */[a, b].join('') === c + +> Input + + `␊ + 1 | foo␊ + 2 | !/* comment */[a, b].join('') === c␊ + ` + +> Error 1/1 + + `␊ + 1 | foo␊ + > 2 | !/* comment */[a, b].join('') === c␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | foo␊ + 2 | ;/* comment */[a, b].join('') !== c␊ + ` + +## invalid(11): !!foo === bar + +> Input + + `␊ + 1 | !!foo === bar␊ + ` + +> Error 1/1 + + `␊ + > 1 | !!foo === bar␊ + | ^ Negated expression in not allowed in equality check.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to '!==' check.␊ + 1 | !foo !== bar␊ + ` diff --git a/test/snapshots/no-negation-in-equality-check.mjs.snap b/test/snapshots/no-negation-in-equality-check.mjs.snap new file mode 100644 index 0000000000000000000000000000000000000000..9df853b5dd8c5053062066810663133c557a2e25 GIT binary patch literal 799 zcmV+)1K|8YRzV7zJB;7hCv|JseR#>Q^}G!uOog5yJogrtktD;{%=I9^lE)!LDl(8%!H) zLP57IGR8gcBYri~Et9?hV}OmrilW>*=cVkA4i2zOz3pq&S{wGqCN%~?2^5j071_q~ z`n@_3SH8LDIK=Tn1(EAaWRyWP9<+6u<&2Yi!#ZJmQCIm`zE%CO{Q;CZ9{)#8?(%lk_vCZ!F1%<;f}cr z$D;5LIpKHM-drvPpbk?gNKz^QTQG%V37)5UjpI|v%aeDKl6&+DFk2ZRvIG9>j-Q}t z_&SIxDnj35&|9(4ful|b4jD(Dg`Du-cY@0~0jF_-f7U`FLapIFoNucDG-Y3D#k%GT z=bDeHTyu_2$zII=>@X;C<@sh)Gr(FHvd(~g=74>O0hY!&oycJVrkRm0C$AL0tU1!A z6sd$)y;^4tlK7TdWa4D4%7cW)Bl-%7rM_YFNtZFip9AghRCADu_%cwq$juqhHeJE@drg`;^>LNi6;6(O5gX)@)-T~?coqLOD(w>g$q*h}VF d!>CgZ)sMw%=E_W?B!i+r=|7(B?|l6e0050%gNXnD literal 0 HcmV?d00001