no-negated-condition.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /*
  2. Based on ESLint builtin `no-negated-condition` rule
  3. https://github.com/eslint/eslint/blob/5c39425fc55ecc0b97bbd07ac22654c0eb4f789c/lib/rules/no-negated-condition.js
  4. */
  5. 'use strict';
  6. const {matches} = require('./selectors/index.js');
  7. const {
  8. removeParentheses,
  9. fixSpaceAroundKeyword,
  10. addParenthesizesToReturnOrThrowExpression,
  11. } = require('./fix/index.js');
  12. const {
  13. getParenthesizedRange,
  14. isParenthesized,
  15. } = require('./utils/parentheses.js');
  16. const isOnSameLine = require('./utils/is-on-same-line.js');
  17. const needsSemicolon = require('./utils/needs-semicolon.js');
  18. const MESSAGE_ID = 'no-negated-condition';
  19. const messages = {
  20. [MESSAGE_ID]: 'Unexpected negated condition.',
  21. };
  22. const selector = [
  23. matches([
  24. 'IfStatement[alternate][alternate.type!="IfStatement"]',
  25. 'ConditionalExpression',
  26. ]),
  27. matches([
  28. '[test.type="UnaryExpression"][test.operator="!"]',
  29. '[test.type="BinaryExpression"][test.operator="!="]',
  30. '[test.type="BinaryExpression"][test.operator="!=="]',
  31. ]),
  32. ].join('');
  33. function * convertNegatedCondition(fixer, node, sourceCode) {
  34. const {test} = node;
  35. if (test.type === 'UnaryExpression') {
  36. const token = sourceCode.getFirstToken(test);
  37. if (node.type === 'IfStatement') {
  38. yield * removeParentheses(test.argument, fixer, sourceCode);
  39. }
  40. yield fixer.remove(token);
  41. return;
  42. }
  43. const token = sourceCode.getTokenAfter(
  44. test.left,
  45. token => token.type === 'Punctuator' && token.value === test.operator,
  46. );
  47. yield fixer.replaceText(token, '=' + token.value.slice(1));
  48. }
  49. function * swapConsequentAndAlternate(fixer, node, sourceCode) {
  50. const isIfStatement = node.type === 'IfStatement';
  51. const [consequent, alternate] = [
  52. node.consequent,
  53. node.alternate,
  54. ].map(node => {
  55. const range = getParenthesizedRange(node, sourceCode);
  56. let text = sourceCode.text.slice(...range);
  57. // `if (!a) b(); else c()` can't fix to `if (!a) c() else b();`
  58. if (isIfStatement && node.type !== 'BlockStatement') {
  59. text = `{${text}}`;
  60. }
  61. return {
  62. range,
  63. text,
  64. };
  65. });
  66. if (consequent.text === alternate.text) {
  67. return;
  68. }
  69. yield fixer.replaceTextRange(consequent.range, alternate.text);
  70. yield fixer.replaceTextRange(alternate.range, consequent.text);
  71. }
  72. /** @param {import('eslint').Rule.RuleContext} context */
  73. const create = context => ({
  74. [selector](node) {
  75. return {
  76. node: node.test,
  77. messageId: MESSAGE_ID,
  78. /** @param {import('eslint').Rule.RuleFixer} fixer */
  79. * fix(fixer) {
  80. const sourceCode = context.getSourceCode();
  81. yield * convertNegatedCondition(fixer, node, sourceCode);
  82. yield * swapConsequentAndAlternate(fixer, node, sourceCode);
  83. if (
  84. node.type !== 'ConditionalExpression'
  85. || node.test.type !== 'UnaryExpression'
  86. ) {
  87. return;
  88. }
  89. yield * fixSpaceAroundKeyword(fixer, node, sourceCode);
  90. const {test, parent} = node;
  91. const [firstToken, secondToken] = sourceCode.getFirstTokens(test, 2);
  92. if (
  93. (parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
  94. && parent.argument === node
  95. && !isOnSameLine(firstToken, secondToken)
  96. && !isParenthesized(node, sourceCode)
  97. && !isParenthesized(test, sourceCode)
  98. ) {
  99. yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode);
  100. return;
  101. }
  102. const tokenBefore = sourceCode.getTokenBefore(node);
  103. if (needsSemicolon(tokenBefore, sourceCode, secondToken.value)) {
  104. yield fixer.insertTextBefore(node, ';');
  105. }
  106. },
  107. };
  108. },
  109. });
  110. /** @type {import('eslint').Rule.RuleModule} */
  111. module.exports = {
  112. create,
  113. meta: {
  114. type: 'suggestion',
  115. docs: {
  116. description: 'Disallow negated conditions.',
  117. },
  118. fixable: 'code',
  119. messages,
  120. },
  121. };