GoScrobble/web/node_modules/eslint-plugin-react/lib/rules/jsx-no-useless-fragment.js

228 lines
5.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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, theres 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
};
}
};