brace-style.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /**
  2. * @fileoverview Rule to flag block statements that do not use the one true brace style
  3. * @author Ian Christian Myers
  4. */
  5. "use strict";
  6. const astUtils = require("./utils/ast-utils");
  7. //------------------------------------------------------------------------------
  8. // Rule Definition
  9. //------------------------------------------------------------------------------
  10. /** @type {import('../shared/types').Rule} */
  11. module.exports = {
  12. meta: {
  13. type: "layout",
  14. docs: {
  15. description: "Enforce consistent brace style for blocks",
  16. recommended: false,
  17. url: "https://eslint.org/docs/rules/brace-style"
  18. },
  19. schema: [
  20. {
  21. enum: ["1tbs", "stroustrup", "allman"]
  22. },
  23. {
  24. type: "object",
  25. properties: {
  26. allowSingleLine: {
  27. type: "boolean",
  28. default: false
  29. }
  30. },
  31. additionalProperties: false
  32. }
  33. ],
  34. fixable: "whitespace",
  35. messages: {
  36. nextLineOpen: "Opening curly brace does not appear on the same line as controlling statement.",
  37. sameLineOpen: "Opening curly brace appears on the same line as controlling statement.",
  38. blockSameLine: "Statement inside of curly braces should be on next line.",
  39. nextLineClose: "Closing curly brace does not appear on the same line as the subsequent block.",
  40. singleLineClose: "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
  41. sameLineClose: "Closing curly brace appears on the same line as the subsequent block."
  42. }
  43. },
  44. create(context) {
  45. const style = context.options[0] || "1tbs",
  46. params = context.options[1] || {},
  47. sourceCode = context.getSourceCode();
  48. //--------------------------------------------------------------------------
  49. // Helpers
  50. //--------------------------------------------------------------------------
  51. /**
  52. * Fixes a place where a newline unexpectedly appears
  53. * @param {Token} firstToken The token before the unexpected newline
  54. * @param {Token} secondToken The token after the unexpected newline
  55. * @returns {Function} A fixer function to remove the newlines between the tokens
  56. */
  57. function removeNewlineBetween(firstToken, secondToken) {
  58. const textRange = [firstToken.range[1], secondToken.range[0]];
  59. const textBetween = sourceCode.text.slice(textRange[0], textRange[1]);
  60. // Don't do a fix if there is a comment between the tokens
  61. if (textBetween.trim()) {
  62. return null;
  63. }
  64. return fixer => fixer.replaceTextRange(textRange, " ");
  65. }
  66. /**
  67. * Validates a pair of curly brackets based on the user's config
  68. * @param {Token} openingCurly The opening curly bracket
  69. * @param {Token} closingCurly The closing curly bracket
  70. * @returns {void}
  71. */
  72. function validateCurlyPair(openingCurly, closingCurly) {
  73. const tokenBeforeOpeningCurly = sourceCode.getTokenBefore(openingCurly);
  74. const tokenAfterOpeningCurly = sourceCode.getTokenAfter(openingCurly);
  75. const tokenBeforeClosingCurly = sourceCode.getTokenBefore(closingCurly);
  76. const singleLineException = params.allowSingleLine && astUtils.isTokenOnSameLine(openingCurly, closingCurly);
  77. if (style !== "allman" && !astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly)) {
  78. context.report({
  79. node: openingCurly,
  80. messageId: "nextLineOpen",
  81. fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly)
  82. });
  83. }
  84. if (style === "allman" && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) {
  85. context.report({
  86. node: openingCurly,
  87. messageId: "sameLineOpen",
  88. fix: fixer => fixer.insertTextBefore(openingCurly, "\n")
  89. });
  90. }
  91. if (astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly) && tokenAfterOpeningCurly !== closingCurly && !singleLineException) {
  92. context.report({
  93. node: openingCurly,
  94. messageId: "blockSameLine",
  95. fix: fixer => fixer.insertTextAfter(openingCurly, "\n")
  96. });
  97. }
  98. if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly)) {
  99. context.report({
  100. node: closingCurly,
  101. messageId: "singleLineClose",
  102. fix: fixer => fixer.insertTextBefore(closingCurly, "\n")
  103. });
  104. }
  105. }
  106. /**
  107. * Validates the location of a token that appears before a keyword (e.g. a newline before `else`)
  108. * @param {Token} curlyToken The closing curly token. This is assumed to precede a keyword token (such as `else` or `finally`).
  109. * @returns {void}
  110. */
  111. function validateCurlyBeforeKeyword(curlyToken) {
  112. const keywordToken = sourceCode.getTokenAfter(curlyToken);
  113. if (style === "1tbs" && !astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
  114. context.report({
  115. node: curlyToken,
  116. messageId: "nextLineClose",
  117. fix: removeNewlineBetween(curlyToken, keywordToken)
  118. });
  119. }
  120. if (style !== "1tbs" && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
  121. context.report({
  122. node: curlyToken,
  123. messageId: "sameLineClose",
  124. fix: fixer => fixer.insertTextAfter(curlyToken, "\n")
  125. });
  126. }
  127. }
  128. //--------------------------------------------------------------------------
  129. // Public API
  130. //--------------------------------------------------------------------------
  131. return {
  132. BlockStatement(node) {
  133. if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
  134. validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
  135. }
  136. },
  137. StaticBlock(node) {
  138. validateCurlyPair(
  139. sourceCode.getFirstToken(node, { skip: 1 }), // skip the `static` token
  140. sourceCode.getLastToken(node)
  141. );
  142. },
  143. ClassBody(node) {
  144. validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
  145. },
  146. SwitchStatement(node) {
  147. const closingCurly = sourceCode.getLastToken(node);
  148. const openingCurly = sourceCode.getTokenBefore(node.cases.length ? node.cases[0] : closingCurly);
  149. validateCurlyPair(openingCurly, closingCurly);
  150. },
  151. IfStatement(node) {
  152. if (node.consequent.type === "BlockStatement" && node.alternate) {
  153. // Handle the keyword after the `if` block (before `else`)
  154. validateCurlyBeforeKeyword(sourceCode.getLastToken(node.consequent));
  155. }
  156. },
  157. TryStatement(node) {
  158. // Handle the keyword after the `try` block (before `catch` or `finally`)
  159. validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block));
  160. if (node.handler && node.finalizer) {
  161. // Handle the keyword after the `catch` block (before `finally`)
  162. validateCurlyBeforeKeyword(sourceCode.getLastToken(node.handler.body));
  163. }
  164. }
  165. };
  166. }
  167. };