139 lines
3.6 KiB
JavaScript
139 lines
3.6 KiB
JavaScript
/*
|
|
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,
|
|
},
|
|
};
|