/* Based on ESLint builtin `no-negated-condition` rule https://github.com/eslint/eslint/blob/5c39425fc55ecc0b97bbd07ac22654c0eb4f789c/lib/rules/no-negated-condition.js */ 'use strict'; const {matches} = require('./selectors/index.js'); const { removeParentheses, fixSpaceAroundKeyword, addParenthesizesToReturnOrThrowExpression, } = require('./fix/index.js'); const { getParenthesizedRange, isParenthesized, } = require('./utils/parentheses.js'); const isOnSameLine = require('./utils/is-on-same-line.js'); const needsSemicolon = require('./utils/needs-semicolon.js'); const MESSAGE_ID = 'no-negated-condition'; const messages = { [MESSAGE_ID]: 'Unexpected negated condition.', }; const selector = [ matches([ 'IfStatement[alternate][alternate.type!="IfStatement"]', 'ConditionalExpression', ]), matches([ '[test.type="UnaryExpression"][test.operator="!"]', '[test.type="BinaryExpression"][test.operator="!="]', '[test.type="BinaryExpression"][test.operator="!=="]', ]), ].join(''); function * convertNegatedCondition(fixer, node, sourceCode) { const {test} = node; if (test.type === 'UnaryExpression') { const token = sourceCode.getFirstToken(test); if (node.type === 'IfStatement') { yield * removeParentheses(test.argument, fixer, sourceCode); } yield fixer.remove(token); return; } const token = sourceCode.getTokenAfter( test.left, token => token.type === 'Punctuator' && token.value === test.operator, ); yield fixer.replaceText(token, '=' + token.value.slice(1)); } function * swapConsequentAndAlternate(fixer, node, sourceCode) { const isIfStatement = node.type === 'IfStatement'; const [consequent, alternate] = [ node.consequent, node.alternate, ].map(node => { const range = getParenthesizedRange(node, sourceCode); let text = sourceCode.text.slice(...range); // `if (!a) b(); else c()` can't fix to `if (!a) c() else b();` if (isIfStatement && node.type !== 'BlockStatement') { text = `{${text}}`; } return { range, text, }; }); if (consequent.text === alternate.text) { return; } yield fixer.replaceTextRange(consequent.range, alternate.text); yield fixer.replaceTextRange(alternate.range, consequent.text); } /** @param {import('eslint').Rule.RuleContext} context */ const create = context => ({ [selector](node) { return { node: node.test, messageId: MESSAGE_ID, /** @param {import('eslint').Rule.RuleFixer} fixer */ * fix(fixer) { const sourceCode = context.getSourceCode(); yield * convertNegatedCondition(fixer, node, sourceCode); yield * swapConsequentAndAlternate(fixer, node, sourceCode); if ( node.type !== 'ConditionalExpression' || node.test.type !== 'UnaryExpression' ) { return; } yield * fixSpaceAroundKeyword(fixer, node, sourceCode); const {test, parent} = node; const [firstToken, secondToken] = sourceCode.getFirstTokens(test, 2); if ( (parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement') && parent.argument === node && !isOnSameLine(firstToken, secondToken) && !isParenthesized(node, sourceCode) && !isParenthesized(test, sourceCode) ) { yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode); return; } const tokenBefore = sourceCode.getTokenBefore(node); if (needsSemicolon(tokenBefore, sourceCode, secondToken.value)) { yield fixer.insertTextBefore(node, ';'); } }, }; }, }); /** @type {import('eslint').Rule.RuleModule} */ module.exports = { create, meta: { type: 'suggestion', docs: { description: 'Disallow negated conditions.', }, fixable: 'code', messages, }, };