Skip to content

Commit c3df913

Browse files
golopotljharb
andcommitted
[New] add jsx-no-useless-fragment rule
Co-Authored-By: Jordan Harband <[email protected]>
1 parent 49343d4 commit c3df913

File tree

6 files changed

+390
-0
lines changed

6 files changed

+390
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ Enable the rules that you would like to use.
170170
* [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings
171171
* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'`
172172
* [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
173+
* [react/jsx-no-useless-fragment](docs/rules/jsx-no-useless-fragment.md): Disallow unnescessary fragments (fixable)
173174
* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX
174175
* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
175176
* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments

docs/rules/jsx-no-useless-fragment.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Disallow unnecessary fragments (react/jsx-no-useless-fragment)
2+
3+
A fragment is redundant if it contains only one child, or if it is the child of a html element.
4+
5+
**Fixable:** This rule is sometimes automatically fixable using the `--fix` flag on the command line.
6+
7+
## Rule Details
8+
9+
The following patterns are considered warnings:
10+
11+
```jsx
12+
<>foo</>
13+
14+
<>{foo}</>
15+
16+
<><Foo /></>
17+
18+
<></>
19+
20+
<Fragment>foo</Fragment>
21+
22+
<React.Fragment>foo</React.Fragment>
23+
24+
<section>
25+
<>
26+
<div />
27+
<div />
28+
</>
29+
</section>
30+
```
31+
32+
The following patterns are **not** considered warnings:
33+
34+
```jsx
35+
<>
36+
<Foo />
37+
<Bar />
38+
</>
39+
40+
<>foo {bar}</>
41+
42+
<> {foo}</>
43+
44+
<SomeComponent>
45+
<>
46+
<div />
47+
<div />
48+
</>
49+
</SomeComponent>
50+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const allRules = {
3434
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
3535
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
3636
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
37+
'jsx-no-useless-fragment': require('./lib/rules/jsx-no-useless-fragment'),
3738
'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
3839
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
3940
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),

lib/rules/jsx-no-useless-fragment.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* @fileoverview Disallow useless fragments
3+
*/
4+
5+
'use strict';
6+
7+
const pragmaUtil = require('../util/pragma');
8+
const docsUrl = require('../util/docsUrl');
9+
10+
11+
module.exports = {
12+
meta: {
13+
type: 'suggestion',
14+
fixable: 'code',
15+
docs: {
16+
description: 'Disallow unnecessary fragments',
17+
category: 'Possible Errors',
18+
recommended: false,
19+
url: docsUrl('jsx-no-useless-fragment')
20+
},
21+
messages: {
22+
NeedsMoreChidren: 'Fragments should contain more than one child - otherwise, there‘s no need for a Fragment at all.',
23+
ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.'
24+
}
25+
},
26+
27+
create(context) {
28+
const reactPragma = pragmaUtil.getFromContext(context);
29+
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
30+
31+
/**
32+
* Test whether a JSXElement is a fragment
33+
* @param {JSXElement} node
34+
* @returns {boolean}
35+
*/
36+
function isFragment(node) {
37+
const name = node.openingElement.name;
38+
39+
// <Fragment>
40+
if (
41+
name.type === 'JSXIdentifier' &&
42+
name.name === fragmentPragma
43+
) {
44+
return true;
45+
}
46+
47+
// <React.Fragment>
48+
if (
49+
name.type === 'JSXMemberExpression' &&
50+
name.object.type === 'JSXIdentifier' &&
51+
name.object.name === reactPragma &&
52+
name.property.type === 'JSXIdentifier' &&
53+
name.property.name === fragmentPragma
54+
) {
55+
return true;
56+
}
57+
58+
return false;
59+
}
60+
61+
/**
62+
* Test whether a node is an padding spaces trimmed by react runtime.
63+
* @param {ASTNode} node
64+
* @returns {boolean}
65+
*/
66+
function isPaddingSpaces(node) {
67+
return (node.type === 'JSXText' || node.type === 'Literal') &&
68+
/^\s*$/.test(node.raw) &&
69+
node.raw.includes('\n');
70+
}
71+
72+
/**
73+
* Test whether a JSXElement has less than two children, excluding paddings spaces.
74+
* @param {JSXElement|JSXFragment} node
75+
*/
76+
function hasLessThanTwoChildren(node) {
77+
if (node.children.length < 2) {
78+
return true;
79+
}
80+
81+
return (
82+
node.children.length -
83+
Number(isPaddingSpaces(node.children[0])) -
84+
Number(isPaddingSpaces(node.children[node.children.length - 1]))
85+
) < 2;
86+
}
87+
88+
/**
89+
* @param {JSXElement|JSXFragment} node
90+
* @returns {boolean}
91+
*/
92+
function isChildOfHtmlElement(node) {
93+
return node.parent.type === 'JSXElement' &&
94+
node.parent.openingElement.name.type === 'JSXIdentifier' &&
95+
/^[a-z]+$/.test(node.parent.openingElement.name.name);
96+
}
97+
98+
function isJSXText(node) {
99+
return !!node && (node.type === 'JSXText' || node.type === 'Literal');
100+
}
101+
102+
/**
103+
* Avoid fixing case like:
104+
* ```jsx
105+
* <div>
106+
* pine<>
107+
* apple
108+
* </>
109+
* </div>
110+
* ```
111+
* @param {JSXElement|JSXFragment} node
112+
* @returns {boolean}
113+
*/
114+
function isSafeToFix(node) {
115+
if (!node.parent.children) {
116+
return false;
117+
}
118+
119+
const i = node.parent.children.indexOf(node);
120+
const previousChild = node.parent.children[i - 1];
121+
const nextChild = node.parent.children[i + 1];
122+
123+
return (
124+
(!isJSXText(previousChild) || /\s$/.test(previousChild.value)) &&
125+
(!isJSXText(nextChild) || /^\s/.test(nextChild.value))
126+
);
127+
}
128+
129+
function fix(node, fixer) {
130+
return node.type === 'JSXFragment' ?
131+
[
132+
fixer.remove(node.openingFragment),
133+
fixer.remove(node.closingFragment)
134+
] :
135+
[
136+
fixer.remove(node.openingElement),
137+
fixer.remove(node.closingElement)
138+
];
139+
}
140+
141+
function checkNode(node) {
142+
if (hasLessThanTwoChildren(node)) {
143+
context.report({
144+
node,
145+
messageId: 'NeedsMoreChidren',
146+
fix: isSafeToFix(node) ? (fixer => fix(node, fixer)) : null
147+
});
148+
}
149+
150+
if (isChildOfHtmlElement(node)) {
151+
context.report({
152+
node,
153+
messageId: 'ChildOfHtmlElement',
154+
fix: isSafeToFix(node) ? (fixer => fix(node, fixer)) : null
155+
});
156+
}
157+
}
158+
159+
return {
160+
JSXElement(node) {
161+
if (isFragment(node)) {
162+
checkNode(node);
163+
}
164+
},
165+
JSXFragment: checkNode
166+
};
167+
}
168+
};

lib/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ declare global {
99
type Token = eslint.AST.Token;
1010
type Fixer = eslint.Rule.RuleFixer;
1111
type JSXAttribute = ASTNode;
12+
type JSXElement = ASTNode;
13+
type JSXFragment = ASTNode;
1214
type JSXSpreadAttribute = ASTNode;
1315

1416
interface Context extends eslint.SourceCode {

0 commit comments

Comments
 (0)