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

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