no-lonely-if.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. /**
  2. * @fileoverview Rule to disallow if as the only statement in an else block
  3. * @author Brandon Mills
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../types').Rule.RuleModule} */
  14. module.exports = {
  15. meta: {
  16. type: "suggestion",
  17. docs: {
  18. description:
  19. "Disallow `if` statements as the only statement in `else` blocks",
  20. recommended: false,
  21. frozen: true,
  22. url: "https://eslint.org/docs/latest/rules/no-lonely-if",
  23. },
  24. schema: [],
  25. fixable: "code",
  26. messages: {
  27. unexpectedLonelyIf:
  28. "Unexpected if as the only statement in an else block.",
  29. },
  30. },
  31. create(context) {
  32. const sourceCode = context.sourceCode;
  33. return {
  34. IfStatement(node) {
  35. const parent = node.parent,
  36. grandparent = parent.parent;
  37. if (
  38. parent &&
  39. parent.type === "BlockStatement" &&
  40. parent.body.length === 1 &&
  41. !astUtils.areBracesNecessary(parent, sourceCode) &&
  42. grandparent &&
  43. grandparent.type === "IfStatement" &&
  44. parent === grandparent.alternate
  45. ) {
  46. context.report({
  47. node,
  48. messageId: "unexpectedLonelyIf",
  49. fix(fixer) {
  50. const openingElseCurly =
  51. sourceCode.getFirstToken(parent);
  52. const closingElseCurly =
  53. sourceCode.getLastToken(parent);
  54. const elseKeyword =
  55. sourceCode.getTokenBefore(openingElseCurly);
  56. const tokenAfterElseBlock =
  57. sourceCode.getTokenAfter(closingElseCurly);
  58. const lastIfToken = sourceCode.getLastToken(
  59. node.consequent,
  60. );
  61. const sourceText = sourceCode.getText();
  62. if (
  63. sourceText
  64. .slice(
  65. openingElseCurly.range[1],
  66. node.range[0],
  67. )
  68. .trim() ||
  69. sourceText
  70. .slice(
  71. node.range[1],
  72. closingElseCurly.range[0],
  73. )
  74. .trim()
  75. ) {
  76. // Don't fix if there are any non-whitespace characters interfering (e.g. comments)
  77. return null;
  78. }
  79. if (
  80. node.consequent.type !== "BlockStatement" &&
  81. lastIfToken.value !== ";" &&
  82. tokenAfterElseBlock &&
  83. (node.consequent.loc.end.line ===
  84. tokenAfterElseBlock.loc.start.line ||
  85. /^[([/+`-]/u.test(
  86. tokenAfterElseBlock.value,
  87. ) ||
  88. lastIfToken.value === "++" ||
  89. lastIfToken.value === "--")
  90. ) {
  91. /*
  92. * If the `if` statement has no block, and is not followed by a semicolon, make sure that fixing
  93. * the issue would not change semantics due to ASI. If this would happen, don't do a fix.
  94. */
  95. return null;
  96. }
  97. return fixer.replaceTextRange(
  98. [
  99. openingElseCurly.range[0],
  100. closingElseCurly.range[1],
  101. ],
  102. (elseKeyword.range[1] ===
  103. openingElseCurly.range[0]
  104. ? " "
  105. : "") + sourceCode.getText(node),
  106. );
  107. },
  108. });
  109. }
  110. },
  111. };
  112. },
  113. };