jsx-no-useless-fragment.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. /**
  2. * @fileoverview Disallow useless fragments
  3. */
  4. 'use strict';
  5. const arrayIncludes = require('array-includes');
  6. const pragmaUtil = require('../util/pragma');
  7. const jsxUtil = require('../util/jsx');
  8. const docsUrl = require('../util/docsUrl');
  9. const report = require('../util/report');
  10. function isJSXText(node) {
  11. return !!node && (node.type === 'JSXText' || node.type === 'Literal');
  12. }
  13. /**
  14. * @param {string} text
  15. * @returns {boolean}
  16. */
  17. function isOnlyWhitespace(text) {
  18. return text.trim().length === 0;
  19. }
  20. /**
  21. * @param {ASTNode} node
  22. * @returns {boolean}
  23. */
  24. function isNonspaceJSXTextOrJSXCurly(node) {
  25. return (isJSXText(node) && !isOnlyWhitespace(node.raw)) || node.type === 'JSXExpressionContainer';
  26. }
  27. /**
  28. * Somehow fragment like this is useful: <Foo content={<>ee eeee eeee ...</>} />
  29. * @param {ASTNode} node
  30. * @returns {boolean}
  31. */
  32. function isFragmentWithOnlyTextAndIsNotChild(node) {
  33. return node.children.length === 1
  34. && isJSXText(node.children[0])
  35. && !(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment');
  36. }
  37. /**
  38. * @param {string} text
  39. * @returns {string}
  40. */
  41. function trimLikeReact(text) {
  42. const leadingSpaces = /^\s*/.exec(text)[0];
  43. const trailingSpaces = /\s*$/.exec(text)[0];
  44. const start = arrayIncludes(leadingSpaces, '\n') ? leadingSpaces.length : 0;
  45. const end = arrayIncludes(trailingSpaces, '\n') ? text.length - trailingSpaces.length : text.length;
  46. return text.slice(start, end);
  47. }
  48. /**
  49. * Test if node is like `<Fragment key={_}>_</Fragment>`
  50. * @param {JSXElement} node
  51. * @returns {boolean}
  52. */
  53. function isKeyedElement(node) {
  54. return node.type === 'JSXElement'
  55. && node.openingElement.attributes
  56. && node.openingElement.attributes.some(jsxUtil.isJSXAttributeKey);
  57. }
  58. /**
  59. * @param {ASTNode} node
  60. * @returns {boolean}
  61. */
  62. function containsCallExpression(node) {
  63. return node
  64. && node.type === 'JSXExpressionContainer'
  65. && node.expression
  66. && node.expression.type === 'CallExpression';
  67. }
  68. const messages = {
  69. NeedsMoreChildren: 'Fragments should contain more than one child - otherwise, there’s no need for a Fragment at all.',
  70. ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.',
  71. };
  72. module.exports = {
  73. meta: {
  74. type: 'suggestion',
  75. fixable: 'code',
  76. docs: {
  77. description: 'Disallow unnecessary fragments',
  78. category: 'Possible Errors',
  79. recommended: false,
  80. url: docsUrl('jsx-no-useless-fragment'),
  81. },
  82. messages,
  83. },
  84. create(context) {
  85. const config = context.options[0] || {};
  86. const allowExpressions = config.allowExpressions || false;
  87. const reactPragma = pragmaUtil.getFromContext(context);
  88. const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
  89. /**
  90. * Test whether a node is an padding spaces trimmed by react runtime.
  91. * @param {ASTNode} node
  92. * @returns {boolean}
  93. */
  94. function isPaddingSpaces(node) {
  95. return isJSXText(node)
  96. && isOnlyWhitespace(node.raw)
  97. && arrayIncludes(node.raw, '\n');
  98. }
  99. function isFragmentWithSingleExpression(node) {
  100. const children = node && node.children.filter((child) => !isPaddingSpaces(child));
  101. return (
  102. children
  103. && children.length === 1
  104. && children[0].type === 'JSXExpressionContainer'
  105. );
  106. }
  107. /**
  108. * Test whether a JSXElement has less than two children, excluding paddings spaces.
  109. * @param {JSXElement|JSXFragment} node
  110. * @returns {boolean}
  111. */
  112. function hasLessThanTwoChildren(node) {
  113. if (!node || !node.children) {
  114. return true;
  115. }
  116. /** @type {ASTNode[]} */
  117. const nonPaddingChildren = node.children.filter(
  118. (child) => !isPaddingSpaces(child)
  119. );
  120. if (nonPaddingChildren.length < 2) {
  121. return !containsCallExpression(nonPaddingChildren[0]);
  122. }
  123. }
  124. /**
  125. * @param {JSXElement|JSXFragment} node
  126. * @returns {boolean}
  127. */
  128. function isChildOfHtmlElement(node) {
  129. return node.parent.type === 'JSXElement'
  130. && node.parent.openingElement.name.type === 'JSXIdentifier'
  131. && /^[a-z]+$/.test(node.parent.openingElement.name.name);
  132. }
  133. /**
  134. * @param {JSXElement|JSXFragment} node
  135. * @return {boolean}
  136. */
  137. function isChildOfComponentElement(node) {
  138. return node.parent.type === 'JSXElement'
  139. && !isChildOfHtmlElement(node)
  140. && !jsxUtil.isFragment(node.parent, reactPragma, fragmentPragma);
  141. }
  142. /**
  143. * @param {ASTNode} node
  144. * @returns {boolean}
  145. */
  146. function canFix(node) {
  147. // Not safe to fix fragments without a jsx parent.
  148. if (!(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment')) {
  149. // const a = <></>
  150. if (node.children.length === 0) {
  151. return false;
  152. }
  153. // const a = <>cat {meow}</>
  154. if (node.children.some(isNonspaceJSXTextOrJSXCurly)) {
  155. return false;
  156. }
  157. }
  158. // Not safe to fix `<Eeee><>foo</></Eeee>` because `Eeee` might require its children be a ReactElement.
  159. if (isChildOfComponentElement(node)) {
  160. return false;
  161. }
  162. // old TS parser can't handle this one
  163. if (node.type === 'JSXFragment' && (!node.openingFragment || !node.closingFragment)) {
  164. return false;
  165. }
  166. return true;
  167. }
  168. /**
  169. * @param {ASTNode} node
  170. * @returns {Function | undefined}
  171. */
  172. function getFix(node) {
  173. if (!canFix(node)) {
  174. return undefined;
  175. }
  176. return function fix(fixer) {
  177. const opener = node.type === 'JSXFragment' ? node.openingFragment : node.openingElement;
  178. const closer = node.type === 'JSXFragment' ? node.closingFragment : node.closingElement;
  179. const childrenText = opener.selfClosing ? '' : context.getSourceCode().getText().slice(opener.range[1], closer.range[0]);
  180. return fixer.replaceText(node, trimLikeReact(childrenText));
  181. };
  182. }
  183. function checkNode(node) {
  184. if (isKeyedElement(node)) {
  185. return;
  186. }
  187. if (
  188. hasLessThanTwoChildren(node)
  189. && !isFragmentWithOnlyTextAndIsNotChild(node)
  190. && !(allowExpressions && isFragmentWithSingleExpression(node))
  191. ) {
  192. report(context, messages.NeedsMoreChildren, 'NeedsMoreChildren', {
  193. node,
  194. fix: getFix(node),
  195. });
  196. }
  197. if (isChildOfHtmlElement(node)) {
  198. report(context, messages.ChildOfHtmlElement, 'ChildOfHtmlElement', {
  199. node,
  200. fix: getFix(node),
  201. });
  202. }
  203. }
  204. return {
  205. JSXElement(node) {
  206. if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) {
  207. checkNode(node);
  208. }
  209. },
  210. JSXFragment: checkNode,
  211. };
  212. },
  213. };