no-lonely-if.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. 'use strict';
  2. const {isParenthesized, isNotSemicolonToken} = require('@eslint-community/eslint-utils');
  3. const needsSemicolon = require('./utils/needs-semicolon.js');
  4. const {removeSpacesAfter} = require('./fix/index.js');
  5. const {matches} = require('./selectors/index.js');
  6. const MESSAGE_ID = 'no-lonely-if';
  7. const messages = {
  8. [MESSAGE_ID]: 'Unexpected `if` as the only statement in a `if` block without `else`.',
  9. };
  10. const ifStatementWithoutAlternate = 'IfStatement:not([alternate])';
  11. const selector = matches([
  12. // `if (a) { if (b) {} }`
  13. [
  14. ifStatementWithoutAlternate,
  15. ' > ',
  16. 'BlockStatement.consequent',
  17. '[body.length=1]',
  18. ' > ',
  19. `${ifStatementWithoutAlternate}.body`,
  20. ].join(''),
  21. // `if (a) if (b) {}`
  22. `${ifStatementWithoutAlternate} > ${ifStatementWithoutAlternate}.consequent`,
  23. ]);
  24. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
  25. // Lower precedence than `&&`
  26. const needParenthesis = node => (
  27. (node.type === 'LogicalExpression' && (node.operator === '||' || node.operator === '??'))
  28. || node.type === 'ConditionalExpression'
  29. || node.type === 'AssignmentExpression'
  30. || node.type === 'YieldExpression'
  31. || node.type === 'SequenceExpression'
  32. );
  33. function getIfStatementTokens(node, sourceCode) {
  34. const tokens = {};
  35. tokens.ifToken = sourceCode.getFirstToken(node);
  36. tokens.openingParenthesisToken = sourceCode.getFirstToken(node, 1);
  37. const {consequent} = node;
  38. tokens.closingParenthesisToken = sourceCode.getTokenBefore(consequent);
  39. if (consequent.type === 'BlockStatement') {
  40. tokens.openingBraceToken = sourceCode.getFirstToken(consequent);
  41. tokens.closingBraceToken = sourceCode.getLastToken(consequent);
  42. }
  43. return tokens;
  44. }
  45. function fix(innerIfStatement, sourceCode) {
  46. return function * (fixer) {
  47. const outerIfStatement = (
  48. innerIfStatement.parent.type === 'BlockStatement'
  49. ? innerIfStatement.parent
  50. : innerIfStatement
  51. ).parent;
  52. const outer = {
  53. ...outerIfStatement,
  54. ...getIfStatementTokens(outerIfStatement, sourceCode),
  55. };
  56. const inner = {
  57. ...innerIfStatement,
  58. ...getIfStatementTokens(innerIfStatement, sourceCode),
  59. };
  60. // Remove inner `if` token
  61. yield fixer.remove(inner.ifToken);
  62. yield removeSpacesAfter(inner.ifToken, sourceCode, fixer);
  63. // Remove outer `{}`
  64. if (outer.openingBraceToken) {
  65. yield fixer.remove(outer.openingBraceToken);
  66. yield removeSpacesAfter(outer.openingBraceToken, sourceCode, fixer);
  67. yield fixer.remove(outer.closingBraceToken);
  68. const tokenBefore = sourceCode.getTokenBefore(outer.closingBraceToken, {includeComments: true});
  69. yield removeSpacesAfter(tokenBefore, sourceCode, fixer);
  70. }
  71. // Add new `()`
  72. yield fixer.insertTextBefore(outer.openingParenthesisToken, '(');
  73. yield fixer.insertTextAfter(
  74. inner.closingParenthesisToken,
  75. `)${inner.consequent.type === 'EmptyStatement' ? '' : ' '}`,
  76. );
  77. // Add ` && `
  78. yield fixer.insertTextAfter(outer.closingParenthesisToken, ' && ');
  79. // Remove `()` if `test` don't need it
  80. for (const {test, openingParenthesisToken, closingParenthesisToken} of [outer, inner]) {
  81. if (
  82. isParenthesized(test, sourceCode)
  83. || !needParenthesis(test)
  84. ) {
  85. yield fixer.remove(openingParenthesisToken);
  86. yield fixer.remove(closingParenthesisToken);
  87. }
  88. yield removeSpacesAfter(closingParenthesisToken, sourceCode, fixer);
  89. }
  90. // If the `if` statement has no block, and is not followed by a semicolon,
  91. // make sure that fixing the issue would not change semantics due to ASI.
  92. // Similar logic https://github.com/eslint/eslint/blob/2124e1b5dad30a905dc26bde9da472bf622d3f50/lib/rules/no-lonely-if.js#L61-L77
  93. if (inner.consequent.type !== 'BlockStatement') {
  94. const lastToken = sourceCode.getLastToken(inner.consequent);
  95. if (isNotSemicolonToken(lastToken)) {
  96. const nextToken = sourceCode.getTokenAfter(outer);
  97. if (needsSemicolon(lastToken, sourceCode, nextToken.value)) {
  98. yield fixer.insertTextBefore(nextToken, ';');
  99. }
  100. }
  101. }
  102. };
  103. }
  104. /** @param {import('eslint').Rule.RuleContext} context */
  105. const create = context => {
  106. const sourceCode = context.getSourceCode();
  107. return {
  108. [selector](node) {
  109. return {
  110. node,
  111. messageId: MESSAGE_ID,
  112. fix: fix(node, sourceCode),
  113. };
  114. },
  115. };
  116. };
  117. /** @type {import('eslint').Rule.RuleModule} */
  118. module.exports = {
  119. create,
  120. meta: {
  121. type: 'suggestion',
  122. docs: {
  123. description: 'Disallow `if` statements as the only statement in `if` blocks without `else`.',
  124. },
  125. fixable: 'code',
  126. messages,
  127. },
  128. };