123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- /**
- * @fileoverview Disallow useless fragments
- */
- 'use strict';
- const arrayIncludes = require('array-includes');
- const pragmaUtil = require('../util/pragma');
- const jsxUtil = require('../util/jsx');
- const docsUrl = require('../util/docsUrl');
- function isJSXText(node) {
- return !!node && (node.type === 'JSXText' || node.type === 'Literal');
- }
- /**
- * @param {string} text
- * @returns {boolean}
- */
- function isOnlyWhitespace(text) {
- return text.trim().length === 0;
- }
- /**
- * @param {ASTNode} node
- * @returns {boolean}
- */
- function isNonspaceJSXTextOrJSXCurly(node) {
- return (isJSXText(node) && !isOnlyWhitespace(node.raw)) || node.type === 'JSXExpressionContainer';
- }
- /**
- * Somehow fragment like this is useful: <Foo content={<>ee eeee eeee ...</>} />
- * @param {ASTNode} node
- * @returns {boolean}
- */
- function isFragmentWithOnlyTextAndIsNotChild(node) {
- return node.children.length === 1
- && isJSXText(node.children[0])
- && !(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment');
- }
- /**
- * @param {string} text
- * @returns {string}
- */
- function trimLikeReact(text) {
- const leadingSpaces = /^\s*/.exec(text)[0];
- const trailingSpaces = /\s*$/.exec(text)[0];
- const start = arrayIncludes(leadingSpaces, '\n') ? leadingSpaces.length : 0;
- const end = arrayIncludes(trailingSpaces, '\n') ? text.length - trailingSpaces.length : text.length;
- return text.slice(start, end);
- }
- /**
- * Test if node is like `<Fragment key={_}>_</Fragment>`
- * @param {JSXElement} node
- * @returns {boolean}
- */
- function isKeyedElement(node) {
- return node.type === 'JSXElement'
- && node.openingElement.attributes
- && node.openingElement.attributes.some(jsxUtil.isJSXAttributeKey);
- }
- /**
- * @param {ASTNode} node
- * @returns {boolean}
- */
- function containsCallExpression(node) {
- return node
- && node.type === 'JSXExpressionContainer'
- && node.expression
- && node.expression.type === 'CallExpression';
- }
- module.exports = {
- meta: {
- type: 'suggestion',
- fixable: 'code',
- docs: {
- description: 'Disallow unnecessary fragments',
- category: 'Possible Errors',
- recommended: false,
- url: docsUrl('jsx-no-useless-fragment')
- },
- messages: {
- NeedsMoreChidren: 'Fragments should contain more than one child - otherwise, there‘s no need for a Fragment at all.',
- ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.'
- }
- },
- create(context) {
- const reactPragma = pragmaUtil.getFromContext(context);
- const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
- /**
- * Test whether a node is an padding spaces trimmed by react runtime.
- * @param {ASTNode} node
- * @returns {boolean}
- */
- function isPaddingSpaces(node) {
- return isJSXText(node)
- && isOnlyWhitespace(node.raw)
- && arrayIncludes(node.raw, '\n');
- }
- /**
- * Test whether a JSXElement has less than two children, excluding paddings spaces.
- * @param {JSXElement|JSXFragment} node
- * @returns {boolean}
- */
- function hasLessThanTwoChildren(node) {
- if (!node || !node.children) {
- return true;
- }
- /** @type {ASTNode[]} */
- const nonPaddingChildren = node.children.filter(
- (child) => !isPaddingSpaces(child)
- );
- if (nonPaddingChildren.length < 2) {
- return !containsCallExpression(nonPaddingChildren[0]);
- }
- }
- /**
- * @param {JSXElement|JSXFragment} node
- * @returns {boolean}
- */
- function isChildOfHtmlElement(node) {
- return node.parent.type === 'JSXElement'
- && node.parent.openingElement.name.type === 'JSXIdentifier'
- && /^[a-z]+$/.test(node.parent.openingElement.name.name);
- }
- /**
- * @param {JSXElement|JSXFragment} node
- * @return {boolean}
- */
- function isChildOfComponentElement(node) {
- return node.parent.type === 'JSXElement'
- && !isChildOfHtmlElement(node)
- && !jsxUtil.isFragment(node.parent, reactPragma, fragmentPragma);
- }
- /**
- * @param {ASTNode} node
- * @returns {boolean}
- */
- function canFix(node) {
- // Not safe to fix fragments without a jsx parent.
- if (!(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment')) {
- // const a = <></>
- if (node.children.length === 0) {
- return false;
- }
- // const a = <>cat {meow}</>
- if (node.children.some(isNonspaceJSXTextOrJSXCurly)) {
- return false;
- }
- }
- // Not safe to fix `<Eeee><>foo</></Eeee>` because `Eeee` might require its children be a ReactElement.
- if (isChildOfComponentElement(node)) {
- return false;
- }
- return true;
- }
- /**
- * @param {ASTNode} node
- * @returns {Function | undefined}
- */
- function getFix(node) {
- if (!canFix(node)) {
- return undefined;
- }
- return function fix(fixer) {
- const opener = node.type === 'JSXFragment' ? node.openingFragment : node.openingElement;
- const closer = node.type === 'JSXFragment' ? node.closingFragment : node.closingElement;
- const childrenText = opener.selfClosing ? '' : context.getSourceCode().getText().slice(opener.range[1], closer.range[0]);
- return fixer.replaceText(node, trimLikeReact(childrenText));
- };
- }
- function checkNode(node) {
- if (isKeyedElement(node)) {
- return;
- }
- if (hasLessThanTwoChildren(node) && !isFragmentWithOnlyTextAndIsNotChild(node)) {
- context.report({
- node,
- messageId: 'NeedsMoreChidren',
- fix: getFix(node)
- });
- }
- if (isChildOfHtmlElement(node)) {
- context.report({
- node,
- messageId: 'ChildOfHtmlElement',
- fix: getFix(node)
- });
- }
- }
- return {
- JSXElement(node) {
- if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) {
- checkNode(node);
- }
- },
- JSXFragment: checkNode
- };
- }
- };
|