Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[New] add
jsx-no-useless-fragment
rule
Co-Authored-By: Jordan Harband <ljharb@gmail.com>
- Loading branch information
Showing
7 changed files
with
518 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# Disallow unnecessary fragments (react/jsx-no-useless-fragment) | ||
|
||
A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a [keyed fragment](https://reactjs.org/docs/fragments.html#keyed-fragments). | ||
|
||
**Fixable:** This rule is sometimes automatically fixable using the `--fix` flag on the command line. | ||
|
||
## Rule Details | ||
|
||
The following patterns are considered warnings: | ||
|
||
```jsx | ||
<>{foo}</> | ||
|
||
<><Foo /></> | ||
|
||
<p><>foo</></p> | ||
|
||
<></> | ||
|
||
<Fragment>foo</Fragment> | ||
|
||
<React.Fragment>foo</React.Fragment> | ||
|
||
<section> | ||
<> | ||
<div /> | ||
<div /> | ||
</> | ||
</section> | ||
``` | ||
|
||
The following patterns are **not** considered warnings: | ||
|
||
```jsx | ||
<> | ||
<Foo /> | ||
<Bar /> | ||
</> | ||
|
||
<>foo {bar}</> | ||
|
||
<> {foo}</> | ||
|
||
const cat = <>meow</> | ||
|
||
<SomeComponent> | ||
<> | ||
<div /> | ||
<div /> | ||
</> | ||
</SomeComponent> | ||
|
||
<Fragment key={item.id}>{item.value}</Fragment> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
/** | ||
* @fileoverview Disallow useless fragments | ||
*/ | ||
|
||
'use strict'; | ||
|
||
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 = leadingSpaces.includes('\n') ? leadingSpaces.length : 0; | ||
const end = trailingSpaces.includes('\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); | ||
} | ||
|
||
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) && | ||
node.raw.includes('\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 || node.children.length < 2) { | ||
return true; | ||
} | ||
|
||
return ( | ||
node.children.length - | ||
(+isPaddingSpaces(node.children[0])) - | ||
(+isPaddingSpaces(node.children[node.children.length - 1])) | ||
) < 2; | ||
} | ||
|
||
/** | ||
* @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 = 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 | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.