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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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. const jsxUtil = require('../util/jsx');
  8. const report = require('../util/report');
  9. // ------------------------------------------------------------------------------
  10. // Rule Definition
  11. // ------------------------------------------------------------------------------
  12. const optionDefaults = {
  13. allow: 'none',
  14. };
  15. const messages = {
  16. moveToNewLine: '`{{descriptor}}` must be placed on a new line',
  17. };
  18. module.exports = {
  19. meta: {
  20. docs: {
  21. description: 'Require one JSX element per line',
  22. category: 'Stylistic Issues',
  23. recommended: false,
  24. url: docsUrl('jsx-one-expression-per-line'),
  25. },
  26. fixable: 'whitespace',
  27. messages,
  28. schema: [
  29. {
  30. type: 'object',
  31. properties: {
  32. allow: {
  33. enum: ['none', 'literal', 'single-child'],
  34. },
  35. },
  36. default: optionDefaults,
  37. additionalProperties: false,
  38. },
  39. ],
  40. },
  41. create(context) {
  42. const options = Object.assign({}, optionDefaults, context.options[0]);
  43. function nodeKey(node) {
  44. return `${node.loc.start.line},${node.loc.start.column}`;
  45. }
  46. function nodeDescriptor(n) {
  47. return n.openingElement ? n.openingElement.name.name : context.getSourceCode().getText(n).replace(/\n/g, '');
  48. }
  49. function handleJSX(node) {
  50. const children = node.children;
  51. if (!children || !children.length) {
  52. return;
  53. }
  54. const openingElement = node.openingElement || node.openingFragment;
  55. const closingElement = node.closingElement || node.closingFragment;
  56. const openingElementStartLine = openingElement.loc.start.line;
  57. const openingElementEndLine = openingElement.loc.end.line;
  58. const closingElementStartLine = closingElement.loc.start.line;
  59. const closingElementEndLine = closingElement.loc.end.line;
  60. if (children.length === 1) {
  61. const child = children[0];
  62. if (
  63. openingElementStartLine === openingElementEndLine
  64. && openingElementEndLine === closingElementStartLine
  65. && closingElementStartLine === closingElementEndLine
  66. && closingElementEndLine === child.loc.start.line
  67. && child.loc.start.line === child.loc.end.line
  68. ) {
  69. if (
  70. options.allow === 'single-child'
  71. || (options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText'))
  72. ) {
  73. return;
  74. }
  75. }
  76. }
  77. const childrenGroupedByLine = {};
  78. const fixDetailsByNode = {};
  79. children.forEach((child) => {
  80. let countNewLinesBeforeContent = 0;
  81. let countNewLinesAfterContent = 0;
  82. if (child.type === 'Literal' || child.type === 'JSXText') {
  83. if (jsxUtil.isWhiteSpaces(child.raw)) {
  84. return;
  85. }
  86. countNewLinesBeforeContent = (child.raw.match(/^\s*\n/g) || []).length;
  87. countNewLinesAfterContent = (child.raw.match(/\n\s*$/g) || []).length;
  88. }
  89. const startLine = child.loc.start.line + countNewLinesBeforeContent;
  90. const endLine = child.loc.end.line - countNewLinesAfterContent;
  91. if (startLine === endLine) {
  92. if (!childrenGroupedByLine[startLine]) {
  93. childrenGroupedByLine[startLine] = [];
  94. }
  95. childrenGroupedByLine[startLine].push(child);
  96. } else {
  97. if (!childrenGroupedByLine[startLine]) {
  98. childrenGroupedByLine[startLine] = [];
  99. }
  100. childrenGroupedByLine[startLine].push(child);
  101. if (!childrenGroupedByLine[endLine]) {
  102. childrenGroupedByLine[endLine] = [];
  103. }
  104. childrenGroupedByLine[endLine].push(child);
  105. }
  106. });
  107. Object.keys(childrenGroupedByLine).forEach((_line) => {
  108. const line = parseInt(_line, 10);
  109. const firstIndex = 0;
  110. const lastIndex = childrenGroupedByLine[line].length - 1;
  111. childrenGroupedByLine[line].forEach((child, i) => {
  112. let prevChild;
  113. let nextChild;
  114. if (i === firstIndex) {
  115. if (line === openingElementEndLine) {
  116. prevChild = openingElement;
  117. }
  118. } else {
  119. prevChild = childrenGroupedByLine[line][i - 1];
  120. }
  121. if (i === lastIndex) {
  122. if (line === closingElementStartLine) {
  123. nextChild = closingElement;
  124. }
  125. } else {
  126. // We don't need to append a trailing because the next child will prepend a leading.
  127. // nextChild = childrenGroupedByLine[line][i + 1];
  128. }
  129. function spaceBetweenPrev() {
  130. return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw))
  131. || ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw))
  132. || context.getSourceCode().isSpaceBetweenTokens(prevChild, child);
  133. }
  134. function spaceBetweenNext() {
  135. return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw))
  136. || ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw))
  137. || context.getSourceCode().isSpaceBetweenTokens(child, nextChild);
  138. }
  139. if (!prevChild && !nextChild) {
  140. return;
  141. }
  142. const source = context.getSourceCode().getText(child);
  143. const leadingSpace = !!(prevChild && spaceBetweenPrev());
  144. const trailingSpace = !!(nextChild && spaceBetweenNext());
  145. const leadingNewLine = !!prevChild;
  146. const trailingNewLine = !!nextChild;
  147. const key = nodeKey(child);
  148. if (!fixDetailsByNode[key]) {
  149. fixDetailsByNode[key] = {
  150. node: child,
  151. source,
  152. descriptor: nodeDescriptor(child),
  153. };
  154. }
  155. if (leadingSpace) {
  156. fixDetailsByNode[key].leadingSpace = true;
  157. }
  158. if (leadingNewLine) {
  159. fixDetailsByNode[key].leadingNewLine = true;
  160. }
  161. if (trailingNewLine) {
  162. fixDetailsByNode[key].trailingNewLine = true;
  163. }
  164. if (trailingSpace) {
  165. fixDetailsByNode[key].trailingSpace = true;
  166. }
  167. });
  168. });
  169. Object.keys(fixDetailsByNode).forEach((key) => {
  170. const details = fixDetailsByNode[key];
  171. const nodeToReport = details.node;
  172. const descriptor = details.descriptor;
  173. const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
  174. const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
  175. const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
  176. const leadingNewLineString = details.leadingNewLine ? '\n' : '';
  177. const trailingNewLineString = details.trailingNewLine ? '\n' : '';
  178. const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
  179. report(context, messages.moveToNewLine, 'moveToNewLine', {
  180. node: nodeToReport,
  181. data: {
  182. descriptor,
  183. },
  184. fix(fixer) {
  185. return fixer.replaceText(nodeToReport, replaceText);
  186. },
  187. });
  188. });
  189. }
  190. return {
  191. JSXElement: handleJSX,
  192. JSXFragment: handleJSX,
  193. };
  194. },
  195. };