jsx-curly-newline.js 6.0 KB

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