Skip to content

Commit

Permalink
Add no-negation-in-equality-check rule (#2353)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker authored May 24, 2024
1 parent 3a282ac commit 8957a03
Show file tree
Hide file tree
Showing 6 changed files with 453 additions and 0 deletions.
30 changes: 30 additions & 0 deletions docs/rules/no-negation-in-equality-check.md
Original file line number Diff line number Diff line change
@@ -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).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

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)) {}
```
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`. || 🔧 | 💡 |
Expand Down
104 changes: 104 additions & 0 deletions rules/no-negation-in-equality-check.js
Original file line number Diff line number Diff line change
@@ -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,
},
};
50 changes: 50 additions & 0 deletions test/no-negation-in-equality-check.mjs
Original file line number Diff line number Diff line change
@@ -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',
],
});
Loading

0 comments on commit 8957a03

Please sign in to comment.