eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
197 lines (170 loc) • 4.96 kB
JavaScript
/*
Based on ESLint builtin `no-negated-condition` rule
https://github.com/eslint/eslint/blob/5c39425fc55ecc0b97bbd07ac22654c0eb4f789c/lib/rules/no-negated-condition.js
*/
import {
removeParentheses,
fixSpaceAroundKeyword,
addParenthesesToReturnOrThrowExpression,
} from './fix/index.js';
import {
getParenthesizedRange,
isParenthesized,
isOnSameLine,
needsSemicolon,
isLogicalExpression,
getLogicalExpressionOperands,
shouldAddParenthesesToExpressionStatementExpression,
} from './utils/index.js';
const MESSAGE_ID = 'no-negated-condition';
const messages = {
[MESSAGE_ID]: 'Unexpected negated condition.',
};
function * convertNegatedCondition(fixer, test, context) {
const {sourceCode} = context;
if (isLogicalExpression(test)) {
const token = sourceCode.getTokenAfter(test.left, token => token.value === test.operator);
yield fixer.replaceText(token, test.operator === '&&' ? '||' : '&&');
yield convertNegatedCondition(fixer, test.left, context);
yield convertNegatedCondition(fixer, test.right, context);
return;
}
if (test.type === 'UnaryExpression') {
const token = sourceCode.getFirstToken(test);
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, context, abort) {
const {sourceCode} = context;
const isIfStatement = node.type === 'IfStatement';
const [consequent, alternate] = [
node.consequent,
node.alternate,
].map(branch => {
const range = getParenthesizedRange(branch, context);
if (sourceCode.getCommentsAfter({range}).length > 0) {
abort();
}
const firstComment = sourceCode.getCommentsBefore({range})[0];
if (firstComment) {
range[0] = sourceCode.getRange(firstComment)[0];
}
let text = sourceCode.text.slice(...range);
// `if (!a) b(); else c()` can't fix to `if (!a) c() else b();`
if (isIfStatement && branch.type !== 'BlockStatement') {
text = `{${text}}`;
}
return {
range,
text,
};
});
if (consequent.text === alternate.text) {
return;
}
yield fixer.replaceTextRange(sourceCode.getRange(consequent), alternate.text);
yield fixer.replaceTextRange(sourceCode.getRange(alternate), consequent.text);
}
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
context.on(['IfStatement', 'ConditionalExpression'], node => {
if (
node.type === 'IfStatement'
&& (
!node.alternate
|| node.alternate.type === 'IfStatement'
)
) {
return;
}
const {test} = node;
const operands = isLogicalExpression(test) ? getLogicalExpressionOperands(test, test.operator) : [test];
if (operands.some(operand =>
!((operand.type === 'UnaryExpression' && operand.operator === '!')
|| (operand.type === 'BinaryExpression' && (operand.operator === '!=' || operand.operator === '!=='))),
)) {
return;
}
return {
node: test,
messageId: MESSAGE_ID,
/**
@param {import('eslint').Rule.RuleFixer} fixer
*/
* fix(fixer, {abort}) {
if (
operands[0].type === 'UnaryExpression'
&& shouldAddParenthesesToExpressionStatementExpression(operands[0].argument, context)
) {
abort();
}
if (node.type === 'IfStatement' && test.type === 'UnaryExpression') {
yield removeParentheses(test.argument, fixer, context);
}
yield convertNegatedCondition(fixer, test, context);
yield swapConsequentAndAlternate(fixer, node, context, abort);
if (
node.type !== 'ConditionalExpression'
|| operands[0].type !== 'UnaryExpression'
) {
return;
}
yield fixSpaceAroundKeyword(fixer, node, context);
const {sourceCode} = context;
const {parent} = node;
const [firstToken, secondToken] = sourceCode.getFirstTokens(test, 2);
if (firstToken.value !== '!') {
return;
}
if (
(
parent.type === 'ReturnStatement'
|| parent.type === 'ThrowStatement'
|| (parent.type === 'YieldExpression' && !parent.delegate)
)
&& parent.argument === node
&& !isOnSameLine(firstToken, secondToken, context)
&& !isParenthesized(node, context)
&& !isParenthesized(test, context)
) {
if (parent.type === 'YieldExpression') {
abort();
}
yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
return;
}
const tokenBefore = sourceCode.getTokenBefore(node);
if (needsSemicolon(tokenBefore, context, secondToken.value)) {
yield fixer.insertTextBefore(node, ';');
}
},
};
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow negated conditions.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
languages: [
'js/js',
],
},
};
export default config;