jsx-no-useless-fragment.js 5.9 KB

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