jsx-curly-newline.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /**
  2. * @fileoverview enforce consistent line breaks inside jsx curly
  3. */
  4. 'use strict';
  5. const docsUrl = require('../util/docsUrl');
  6. const report = require('../util/report');
  7. // ------------------------------------------------------------------------------
  8. // Rule Definition
  9. // ------------------------------------------------------------------------------
  10. function getNormalizedOption(context) {
  11. const rawOption = context.options[0] || 'consistent';
  12. if (rawOption === 'consistent') {
  13. return {
  14. multiline: 'consistent',
  15. singleline: 'consistent',
  16. };
  17. }
  18. if (rawOption === 'never') {
  19. return {
  20. multiline: 'forbid',
  21. singleline: 'forbid',
  22. };
  23. }
  24. return {
  25. multiline: rawOption.multiline || 'consistent',
  26. singleline: rawOption.singleline || 'consistent',
  27. };
  28. }
  29. const messages = {
  30. expectedBefore: 'Expected newline before \'}\'.',
  31. expectedAfter: 'Expected newline after \'{\'.',
  32. unexpectedBefore: 'Unexpected newline before \'}\'.',
  33. unexpectedAfter: 'Unexpected newline after \'{\'.',
  34. };
  35. module.exports = {
  36. meta: {
  37. type: 'layout',
  38. docs: {
  39. description: 'Enforce consistent linebreaks in curly braces in JSX attributes and expressions',
  40. category: 'Stylistic Issues',
  41. recommended: false,
  42. url: docsUrl('jsx-curly-newline'),
  43. },
  44. fixable: 'whitespace',
  45. schema: [
  46. {
  47. anyOf: [
  48. {
  49. enum: ['consistent', 'never'],
  50. },
  51. {
  52. type: 'object',
  53. properties: {
  54. singleline: { enum: ['consistent', 'require', 'forbid'] },
  55. multiline: { enum: ['consistent', 'require', 'forbid'] },
  56. },
  57. additionalProperties: false,
  58. },
  59. ],
  60. },
  61. ],
  62. messages,
  63. },
  64. create(context) {
  65. const sourceCode = context.getSourceCode();
  66. const option = getNormalizedOption(context);
  67. // ----------------------------------------------------------------------
  68. // Helpers
  69. // ----------------------------------------------------------------------
  70. /**
  71. * Determines whether two adjacent tokens are on the same line.
  72. * @param {Object} left - The left token object.
  73. * @param {Object} right - The right token object.
  74. * @returns {boolean} Whether or not the tokens are on the same line.
  75. */
  76. function isTokenOnSameLine(left, right) {
  77. return left.loc.end.line === right.loc.start.line;
  78. }
  79. /**
  80. * Determines whether there should be newlines inside curlys
  81. * @param {ASTNode} expression The expression contained in the curlys
  82. * @param {boolean} hasLeftNewline `true` if the left curly has a newline in the current code.
  83. * @returns {boolean} `true` if there should be newlines inside the function curlys
  84. */
  85. function shouldHaveNewlines(expression, hasLeftNewline) {
  86. const isMultiline = expression.loc.start.line !== expression.loc.end.line;
  87. switch (isMultiline ? option.multiline : option.singleline) {
  88. case 'forbid': return false;
  89. case 'require': return true;
  90. case 'consistent':
  91. default: return hasLeftNewline;
  92. }
  93. }
  94. /**
  95. * Validates curlys
  96. * @param {Object} curlys An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
  97. * @param {ASTNode} expression The expression inside the curly
  98. * @returns {void}
  99. */
  100. function validateCurlys(curlys, expression) {
  101. const leftCurly = curlys.leftCurly;
  102. const rightCurly = curlys.rightCurly;
  103. const tokenAfterLeftCurly = sourceCode.getTokenAfter(leftCurly);
  104. const tokenBeforeRightCurly = sourceCode.getTokenBefore(rightCurly);
  105. const hasLeftNewline = !isTokenOnSameLine(leftCurly, tokenAfterLeftCurly);
  106. const hasRightNewline = !isTokenOnSameLine(tokenBeforeRightCurly, rightCurly);
  107. const needsNewlines = shouldHaveNewlines(expression, hasLeftNewline);
  108. if (hasLeftNewline && !needsNewlines) {
  109. report(context, messages.unexpectedAfter, 'unexpectedAfter', {
  110. node: leftCurly,
  111. fix(fixer) {
  112. return sourceCode
  113. .getText()
  114. .slice(leftCurly.range[1], tokenAfterLeftCurly.range[0])
  115. .trim()
  116. ? null // If there is a comment between the { and the first element, don't do a fix.
  117. : fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]);
  118. },
  119. });
  120. } else if (!hasLeftNewline && needsNewlines) {
  121. report(context, messages.expectedAfter, 'expectedAfter', {
  122. node: leftCurly,
  123. fix: (fixer) => fixer.insertTextAfter(leftCurly, '\n'),
  124. });
  125. }
  126. if (hasRightNewline && !needsNewlines) {
  127. report(context, messages.unexpectedBefore, 'unexpectedBefore', {
  128. node: rightCurly,
  129. fix(fixer) {
  130. return sourceCode
  131. .getText()
  132. .slice(tokenBeforeRightCurly.range[1], rightCurly.range[0])
  133. .trim()
  134. ? null // If there is a comment between the last element and the }, don't do a fix.
  135. : fixer.removeRange([
  136. tokenBeforeRightCurly.range[1],
  137. rightCurly.range[0],
  138. ]);
  139. },
  140. });
  141. } else if (!hasRightNewline && needsNewlines) {
  142. report(context, messages.expectedBefore, 'expectedBefore', {
  143. node: rightCurly,
  144. fix: (fixer) => fixer.insertTextBefore(rightCurly, '\n'),
  145. });
  146. }
  147. }
  148. // ----------------------------------------------------------------------
  149. // Public
  150. // ----------------------------------------------------------------------
  151. return {
  152. JSXExpressionContainer(node) {
  153. const curlyTokens = {
  154. leftCurly: sourceCode.getFirstToken(node),
  155. rightCurly: sourceCode.getLastToken(node),
  156. };
  157. validateCurlys(curlyTokens, node.expression);
  158. },
  159. };
  160. },
  161. };