jsx-one-expression-per-line.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /**
  2. * @fileoverview Limit to one expression per line in JSX
  3. * @author Mark Ivan Allen <Vydia.com>
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. // ------------------------------------------------------------------------------
  8. // Rule Definition
  9. // ------------------------------------------------------------------------------
  10. module.exports = {
  11. meta: {
  12. docs: {
  13. description: 'Limit to one expression per line in JSX',
  14. category: 'Stylistic Issues',
  15. recommended: false,
  16. url: docsUrl('jsx-one-expression-per-line')
  17. },
  18. fixable: 'whitespace',
  19. schema: []
  20. },
  21. create: function (context) {
  22. const sourceCode = context.getSourceCode();
  23. function nodeKey (node) {
  24. return `${node.loc.start.line},${node.loc.start.column}`;
  25. }
  26. function nodeDescriptor (n) {
  27. return n.openingElement ? n.openingElement.name.name : sourceCode.getText(n).replace(/\n/g, '');
  28. }
  29. return {
  30. JSXElement: function (node) {
  31. const children = node.children;
  32. if (!children || !children.length) {
  33. return;
  34. }
  35. const openingElement = node.openingElement;
  36. const closingElement = node.closingElement;
  37. const openingElementEndLine = openingElement.loc.end.line;
  38. const closingElementStartLine = closingElement.loc.start.line;
  39. const childrenGroupedByLine = {};
  40. const fixDetailsByNode = {};
  41. children.forEach(child => {
  42. let countNewLinesBeforeContent = 0;
  43. let countNewLinesAfterContent = 0;
  44. if (child.type === 'Literal' || child.type === 'JSXText') {
  45. if (/^\s*$/.test(child.raw)) {
  46. return;
  47. }
  48. countNewLinesBeforeContent = (child.raw.match(/^ *\n/g) || []).length;
  49. countNewLinesAfterContent = (child.raw.match(/\n *$/g) || []).length;
  50. }
  51. const startLine = child.loc.start.line + countNewLinesBeforeContent;
  52. const endLine = child.loc.end.line - countNewLinesAfterContent;
  53. if (startLine === endLine) {
  54. if (!childrenGroupedByLine[startLine]) {
  55. childrenGroupedByLine[startLine] = [];
  56. }
  57. childrenGroupedByLine[startLine].push(child);
  58. } else {
  59. if (!childrenGroupedByLine[startLine]) {
  60. childrenGroupedByLine[startLine] = [];
  61. }
  62. childrenGroupedByLine[startLine].push(child);
  63. if (!childrenGroupedByLine[endLine]) {
  64. childrenGroupedByLine[endLine] = [];
  65. }
  66. childrenGroupedByLine[endLine].push(child);
  67. }
  68. });
  69. Object.keys(childrenGroupedByLine).forEach(_line => {
  70. const line = parseInt(_line, 10);
  71. const firstIndex = 0;
  72. const lastIndex = childrenGroupedByLine[line].length - 1;
  73. childrenGroupedByLine[line].forEach((child, i) => {
  74. let prevChild;
  75. let nextChild;
  76. if (i === firstIndex) {
  77. if (line === openingElementEndLine) {
  78. prevChild = openingElement;
  79. }
  80. } else {
  81. prevChild = childrenGroupedByLine[line][i - 1];
  82. }
  83. if (i === lastIndex) {
  84. if (line === closingElementStartLine) {
  85. nextChild = closingElement;
  86. }
  87. } else {
  88. // We don't need to append a trailing because the next child will prepend a leading.
  89. // nextChild = childrenGroupedByLine[line][i + 1];
  90. }
  91. function spaceBetweenPrev () {
  92. return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw)) ||
  93. ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw)) ||
  94. sourceCode.isSpaceBetweenTokens(prevChild, child);
  95. }
  96. function spaceBetweenNext () {
  97. return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw)) ||
  98. ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw)) ||
  99. sourceCode.isSpaceBetweenTokens(child, nextChild);
  100. }
  101. if (!prevChild && !nextChild) {
  102. return;
  103. }
  104. const source = sourceCode.getText(child);
  105. const leadingSpace = !!(prevChild && spaceBetweenPrev());
  106. const trailingSpace = !!(nextChild && spaceBetweenNext());
  107. const leadingNewLine = !!prevChild;
  108. const trailingNewLine = !!nextChild;
  109. const key = nodeKey(child);
  110. if (!fixDetailsByNode[key]) {
  111. fixDetailsByNode[key] = {
  112. node: child,
  113. source: source,
  114. descriptor: nodeDescriptor(child)
  115. };
  116. }
  117. if (leadingSpace) {
  118. fixDetailsByNode[key].leadingSpace = true;
  119. }
  120. if (leadingNewLine) {
  121. fixDetailsByNode[key].leadingNewLine = true;
  122. }
  123. if (trailingNewLine) {
  124. fixDetailsByNode[key].trailingNewLine = true;
  125. }
  126. if (trailingSpace) {
  127. fixDetailsByNode[key].trailingSpace = true;
  128. }
  129. });
  130. });
  131. Object.keys(fixDetailsByNode).forEach(key => {
  132. const details = fixDetailsByNode[key];
  133. const nodeToReport = details.node;
  134. const descriptor = details.descriptor;
  135. const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
  136. const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
  137. const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
  138. const leadingNewLineString = details.leadingNewLine ? '\n' : '';
  139. const trailingNewLineString = details.trailingNewLine ? '\n' : '';
  140. const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
  141. context.report({
  142. node: nodeToReport,
  143. message: `\`${descriptor}\` must be placed on a new line`,
  144. fix: function (fixer) {
  145. return fixer.replaceText(nodeToReport, replaceText);
  146. }
  147. });
  148. });
  149. }
  150. };
  151. }
  152. };