jsx-newline.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. /**
  2. * @fileoverview Require or prevent a new line after jsx elements and expressions.
  3. * @author Johnny Zabala
  4. * @author Joseph Stiles
  5. */
  6. 'use strict';
  7. const docsUrl = require('../util/docsUrl');
  8. const getText = require('../util/eslint').getText;
  9. const report = require('../util/report');
  10. // ------------------------------------------------------------------------------
  11. // Rule Definition
  12. // ------------------------------------------------------------------------------
  13. const messages = {
  14. require: 'JSX element should start in a new line',
  15. prevent: 'JSX element should not start in a new line',
  16. allowMultilines: 'Multiline JSX elements should start in a new line',
  17. };
  18. function isMultilined(node) {
  19. return node && node.loc.start.line !== node.loc.end.line;
  20. }
  21. /** @type {import('eslint').Rule.RuleModule} */
  22. module.exports = {
  23. meta: {
  24. docs: {
  25. description: 'Require or prevent a new line after jsx elements and expressions.',
  26. category: 'Stylistic Issues',
  27. recommended: false,
  28. url: docsUrl('jsx-newline'),
  29. },
  30. fixable: 'code',
  31. messages,
  32. schema: [
  33. {
  34. type: 'object',
  35. properties: {
  36. prevent: {
  37. default: false,
  38. type: 'boolean',
  39. },
  40. allowMultilines: {
  41. default: false,
  42. type: 'boolean',
  43. },
  44. },
  45. additionalProperties: false,
  46. if: {
  47. properties: {
  48. allowMultilines: {
  49. const: true,
  50. },
  51. },
  52. },
  53. then: {
  54. properties: {
  55. prevent: {
  56. const: true,
  57. },
  58. },
  59. required: [
  60. 'prevent',
  61. ],
  62. },
  63. },
  64. ],
  65. },
  66. create(context) {
  67. const jsxElementParents = new Set();
  68. function isBlockCommentInCurlyBraces(element) {
  69. const elementRawValue = getText(context, element);
  70. return /^\s*{\/\*/.test(elementRawValue);
  71. }
  72. function isNonBlockComment(element) {
  73. return !isBlockCommentInCurlyBraces(element) && (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer');
  74. }
  75. return {
  76. 'Program:exit'() {
  77. jsxElementParents.forEach((parent) => {
  78. parent.children.forEach((element, index, elements) => {
  79. if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
  80. const configuration = context.options[0] || {};
  81. const prevent = configuration.prevent || false;
  82. const allowMultilines = configuration.allowMultilines || false;
  83. const firstAdjacentSibling = elements[index + 1];
  84. const secondAdjacentSibling = elements[index + 2];
  85. const hasSibling = firstAdjacentSibling
  86. && secondAdjacentSibling
  87. && (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText');
  88. if (!hasSibling) return;
  89. // Check adjacent sibling has the proper amount of newlines
  90. const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value);
  91. if (isBlockCommentInCurlyBraces(element)) return;
  92. if (
  93. allowMultilines
  94. && (
  95. isMultilined(element)
  96. || isMultilined(elements.slice(index + 2).find(isNonBlockComment))
  97. )
  98. ) {
  99. if (!isWithoutNewLine) return;
  100. const regex = /(\n)(?!.*\1)/g;
  101. const replacement = '\n\n';
  102. const messageId = 'allowMultilines';
  103. report(context, messages[messageId], messageId, {
  104. node: secondAdjacentSibling,
  105. fix(fixer) {
  106. return fixer.replaceText(
  107. firstAdjacentSibling,
  108. getText(context, firstAdjacentSibling).replace(regex, replacement)
  109. );
  110. },
  111. });
  112. return;
  113. }
  114. if (isWithoutNewLine === prevent) return;
  115. const messageId = prevent
  116. ? 'prevent'
  117. : 'require';
  118. const regex = prevent
  119. ? /(\n\n)(?!.*\1)/g
  120. : /(\n)(?!.*\1)/g;
  121. const replacement = prevent
  122. ? '\n'
  123. : '\n\n';
  124. report(context, messages[messageId], messageId, {
  125. node: secondAdjacentSibling,
  126. fix(fixer) {
  127. return fixer.replaceText(
  128. firstAdjacentSibling,
  129. // double or remove the last newline
  130. getText(context, firstAdjacentSibling).replace(regex, replacement)
  131. );
  132. },
  133. });
  134. }
  135. });
  136. });
  137. },
  138. ':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => {
  139. jsxElementParents.add(node.parent);
  140. },
  141. };
  142. },
  143. };