PromoCursed/node_modules/eslint-plugin-react/lib/rules/jsx-no-useless-fragment.js
2024-08-20 23:25:37 +04:00

260 lines
6.8 KiB
JavaScript
Raw 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');
const report = require('../util/report');
const getText = require('../util/eslint').getText;
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';
}
const messages = {
NeedsMoreChildren: '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.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
fixable: 'code',
docs: {
description: 'Disallow unnecessary fragments',
category: 'Possible Errors',
recommended: false,
url: docsUrl('jsx-no-useless-fragment'),
},
messages,
schema: [{
type: 'object',
properties: {
allowExpressions: {
type: 'boolean',
},
},
}],
},
create(context) {
const config = context.options[0] || {};
const allowExpressions = config.allowExpressions || false;
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');
}
function isFragmentWithSingleExpression(node) {
const children = node && node.children.filter((child) => !isPaddingSpaces(child));
return (
children
&& children.length === 1
&& children[0].type === 'JSXExpressionContainer'
);
}
/**
* 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;
}
// old TS parser can't handle this one
if (node.type === 'JSXFragment' && (!node.openingFragment || !node.closingFragment)) {
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 ? '' : getText(context).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)
&& !(allowExpressions && isFragmentWithSingleExpression(node))
) {
report(context, messages.NeedsMoreChildren, 'NeedsMoreChildren', {
node,
fix: getFix(node),
});
}
if (isChildOfHtmlElement(node)) {
report(context, messages.ChildOfHtmlElement, 'ChildOfHtmlElement', {
node,
fix: getFix(node),
});
}
}
return {
JSXElement(node) {
if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) {
checkNode(node);
}
},
JSXFragment: checkNode,
};
},
};