Skip to content

Commit 8957a03

Browse files
authored
Add no-negation-in-equality-check rule (#2353)
1 parent 3a282ac commit 8957a03

6 files changed

+453
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Disallow negated expression in equality check
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).
4+
5+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
Using a negated expression in equality check is most likely a mistake.
11+
12+
## Fail
13+
14+
```js
15+
if (!foo === bar) {}
16+
```
17+
18+
```js
19+
if (!foo !== bar) {}
20+
```
21+
22+
## Pass
23+
24+
```js
25+
if (foo !== bar) {}
26+
```
27+
28+
```js
29+
if (!(foo === bar)) {}
30+
```

readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
145145
| [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. || 🔧 | |
146146
| [no-magic-array-flat-depth](docs/rules/no-magic-array-flat-depth.md) | Disallow a magic number as the `depth` argument in `Array#flat(…).` || | |
147147
| [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. || 🔧 | |
148+
| [no-negation-in-equality-check](docs/rules/no-negation-in-equality-check.md) | Disallow negated expression in equality check. || | 💡 |
148149
| [no-nested-ternary](docs/rules/no-nested-ternary.md) | Disallow nested ternary expressions. || 🔧 | |
149150
| [no-new-array](docs/rules/no-new-array.md) | Disallow `new Array()`. || 🔧 | 💡 |
150151
| [no-new-buffer](docs/rules/no-new-buffer.md) | Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. || 🔧 | 💡 |
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use strict';
2+
const {
3+
fixSpaceAroundKeyword,
4+
addParenthesizesToReturnOrThrowExpression,
5+
} = require('./fix/index.js');
6+
const {
7+
needsSemicolon,
8+
isParenthesized,
9+
isOnSameLine,
10+
} = require('./utils/index.js');
11+
12+
const MESSAGE_ID_ERROR = 'no-negation-in-equality-check/error';
13+
const MESSAGE_ID_SUGGESTION = 'no-negation-in-equality-check/suggestion';
14+
const messages = {
15+
[MESSAGE_ID_ERROR]: 'Negated expression in not allowed in equality check.',
16+
[MESSAGE_ID_SUGGESTION]: 'Switch to \'{{operator}}\' check.',
17+
};
18+
19+
const EQUALITY_OPERATORS = new Set([
20+
'===',
21+
'!==',
22+
'==',
23+
'!=',
24+
]);
25+
26+
const isEqualityCheck = node => node.type === 'BinaryExpression' && EQUALITY_OPERATORS.has(node.operator);
27+
const isNegatedExpression = node => node.type === 'UnaryExpression' && node.prefix && node.operator === '!';
28+
29+
/** @param {import('eslint').Rule.RuleContext} context */
30+
const create = context => ({
31+
BinaryExpression(binaryExpression) {
32+
const {operator, left} = binaryExpression;
33+
34+
if (
35+
!isEqualityCheck(binaryExpression)
36+
|| !isNegatedExpression(left)
37+
) {
38+
return;
39+
}
40+
41+
const {sourceCode} = context;
42+
const bangToken = sourceCode.getFirstToken(left);
43+
const negatedOperator = `${operator.startsWith('!') ? '=' : '!'}${operator.slice(1)}`;
44+
45+
return {
46+
node: bangToken,
47+
messageId: MESSAGE_ID_ERROR,
48+
/** @param {import('eslint').Rule.RuleFixer} fixer */
49+
suggest: [
50+
{
51+
messageId: MESSAGE_ID_SUGGESTION,
52+
data: {
53+
operator: negatedOperator,
54+
},
55+
/** @param {import('eslint').Rule.RuleFixer} fixer */
56+
* fix(fixer) {
57+
yield * fixSpaceAroundKeyword(fixer, binaryExpression, sourceCode);
58+
59+
const tokenAfterBang = sourceCode.getTokenAfter(bangToken);
60+
61+
const {parent} = binaryExpression;
62+
if (
63+
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
64+
&& !isParenthesized(binaryExpression, sourceCode)
65+
) {
66+
const returnToken = sourceCode.getFirstToken(parent);
67+
if (!isOnSameLine(returnToken, tokenAfterBang)) {
68+
yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode);
69+
}
70+
}
71+
72+
yield fixer.remove(bangToken);
73+
74+
const previousToken = sourceCode.getTokenBefore(bangToken);
75+
if (needsSemicolon(previousToken, sourceCode, tokenAfterBang.value)) {
76+
yield fixer.insertTextAfter(bangToken, ';');
77+
}
78+
79+
const operatorToken = sourceCode.getTokenAfter(
80+
left,
81+
token => token.type === 'Punctuator' && token.value === operator,
82+
);
83+
yield fixer.replaceText(operatorToken, negatedOperator);
84+
},
85+
},
86+
],
87+
};
88+
},
89+
});
90+
91+
/** @type {import('eslint').Rule.RuleModule} */
92+
module.exports = {
93+
create,
94+
meta: {
95+
type: 'problem',
96+
docs: {
97+
description: 'Disallow negated expression in equality check.',
98+
recommended: true,
99+
},
100+
101+
hasSuggestions: true,
102+
messages,
103+
},
104+
};
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'!foo instanceof bar',
9+
'+foo === bar',
10+
'!(foo === bar)',
11+
// We are not checking right side
12+
'foo === !bar',
13+
],
14+
invalid: [
15+
'!foo === bar',
16+
'!foo !== bar',
17+
'!foo == bar',
18+
'!foo != bar',
19+
outdent`
20+
function x() {
21+
return!foo === bar;
22+
}
23+
`,
24+
outdent`
25+
function x() {
26+
return!
27+
foo === bar;
28+
throw!
29+
foo === bar;
30+
}
31+
`,
32+
outdent`
33+
foo
34+
!(a) === b
35+
`,
36+
outdent`
37+
foo
38+
![a, b].join('') === c
39+
`,
40+
outdent`
41+
foo
42+
! [a, b].join('') === c
43+
`,
44+
outdent`
45+
foo
46+
!/* comment */[a, b].join('') === c
47+
`,
48+
'!!foo === bar',
49+
],
50+
});

0 commit comments

Comments
 (0)