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

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