Skip to content

Commit 1120109

Browse files
imjordanxdRel1cx
andauthored
feat: add support for allowExpressions in no-useless-fragment (#836)
Co-authored-by: Eva1ent <[email protected]>
1 parent 13d1948 commit 1120109

File tree

3 files changed

+124
-9
lines changed

3 files changed

+124
-9
lines changed

packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts

+36
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,34 @@ ruleTester.run(RULE_NAME, rule, {
118118
code: /* tsx */ `<><Foo>{moo}</Foo></>`,
119119
errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
120120
},
121+
{
122+
code: /* tsx */ `<>{moo}</>`,
123+
errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
124+
options: [{ allowExpressions: false }],
125+
},
126+
{
127+
code: /* tsx */ `<Foo><>{moo}</></Foo>`,
128+
errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
129+
options: [{ allowExpressions: false }],
130+
},
131+
{
132+
code: /* tsx */ `<React.Fragment><>{moo}</></React.Fragment>`,
133+
errors: [{ type: AST_NODE_TYPES.JSXElement, messageId: "noUselessFragment" }, {
134+
type: AST_NODE_TYPES.JSXFragment,
135+
messageId: "noUselessFragment",
136+
}],
137+
options: [{ allowExpressions: false }],
138+
},
139+
{
140+
code: /* tsx */ `<Foo bar={<>baz</>}/>`,
141+
errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
142+
options: [{ allowExpressions: false }],
143+
},
144+
{
145+
code: /* tsx */ `<Foo><><Bar/><Baz/></></Foo>`,
146+
errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
147+
options: [{ allowExpressions: false }],
148+
},
121149
],
122150
valid: [
123151
...allValid,
@@ -178,5 +206,13 @@ ruleTester.run(RULE_NAME, rule, {
178206
},
179207
},
180208
},
209+
{
210+
code: /* tsx */ `{foo}`,
211+
options: [{ allowExpressions: false }],
212+
},
213+
{
214+
code: /* tsx */ `<Foo bar={<><Bar/><Baz/></>} />`,
215+
options: [{ allowExpressions: false }],
216+
},
181217
],
182218
});

packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts

+33-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type MessageID =
1818
function check(
1919
node: TSESTree.JSXElement | TSESTree.JSXFragment,
2020
context: RuleContext,
21+
allowExpressions: boolean,
2122
) {
2223
const initialScope = context.sourceCode.getScope(node);
2324
if (JSX.isKeyedElement(node, initialScope)) return;
@@ -26,7 +27,15 @@ function check(
2627
const isChildren = AST.isOneOf([AST_NODE_TYPES.JSXElement, AST_NODE_TYPES.JSXFragment])(node.parent);
2728
const [firstChildren] = node.children;
2829
// <Foo content={<>ee eeee eeee ...</>} />
29-
if (node.children.length === 1 && JSX.isLiteral(firstChildren) && !isChildren) return;
30+
if (allowExpressions && node.children.length === 1 && JSX.isLiteral(firstChildren) && !isChildren) return;
31+
if (!allowExpressions && isChildren) {
32+
// <Foo><>hello, world</></Foo>
33+
return context.report({ messageId: "noUselessFragment", node });
34+
} else if (!allowExpressions && !isChildren && node.children.length === 1) {
35+
// const foo = <>{children}</>;
36+
// return <>{children}</>;
37+
return context.report({ messageId: "noUselessFragment", node });
38+
}
3039
const nonPaddingChildren = node.children.filter((child) => !JSX.isPaddingSpaces(child));
3140
if (nonPaddingChildren.length > 1) return;
3241
if (nonPaddingChildren.length === 0) return context.report({ messageId: "noUselessFragment", node });
@@ -37,7 +46,13 @@ function check(
3746
context.report({ messageId: "noUselessFragment", node });
3847
}
3948

40-
export default createRule<[], MessageID>({
49+
type Options = [
50+
{
51+
allowExpressions: boolean;
52+
},
53+
];
54+
55+
export default createRule<Options, MessageID>({
4156
meta: {
4257
type: "problem",
4358
docs: {
@@ -47,19 +62,30 @@ export default createRule<[], MessageID>({
4762
noUselessFragment: "A fragment contains less than two children is unnecessary.",
4863
noUselessFragmentInBuiltIn: "A fragment placed inside a built-in component is unnecessary.",
4964
},
50-
schema: [],
65+
schema: [{
66+
type: "object",
67+
additionalProperties: false,
68+
properties: {
69+
allowExpressions: {
70+
type: "boolean",
71+
},
72+
},
73+
}],
5174
},
5275
name: RULE_NAME,
53-
create(context) {
76+
create(context, [option]) {
77+
const { allowExpressions = true } = option;
5478
return {
5579
JSXElement(node) {
5680
if (!isFragmentElement(node, context)) return;
57-
check(node, context);
81+
check(node, context, allowExpressions);
5882
},
5983
JSXFragment(node) {
60-
check(node, context);
84+
check(node, context, allowExpressions);
6185
},
6286
};
6387
},
64-
defaultOptions: [],
88+
defaultOptions: [{
89+
allowExpressions: true,
90+
}],
6591
});

website/pages/docs/rules/no-useless-fragment.mdx

+55-2
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ const cat = <>meow</>
6767

6868
## Note
6969

70-
[This rule always allows single expressions in a fragment](https://github.com/Rel1cx/eslint-react/pull/188). This is useful in
70+
[By default, this rule always allows single expressions in a fragment](https://github.com/Rel1cx/eslint-react/pull/188). This is useful in
7171
places like Typescript where `string` does not satisfy the expected return type
7272
of `JSX.Element`. A common workaround is to wrap the variable holding a string
73-
in a fragment and expression.
73+
in a fragment and expression. To change this behaviour, use the `allowExpressions` option.
7474

7575
### Examples of correct code for single expressions in fragments:
7676

@@ -80,6 +80,59 @@ in a fragment and expression.
8080
<Fragment>{foo}</Fragment>
8181
```
8282

83+
## Examples with `allowExpressions: false`
84+
85+
### Failing
86+
87+
```tsx
88+
<><Foo /></>
89+
90+
<p><>foo</></p>
91+
92+
<></>
93+
94+
<Foo bar={<>baz</>} />
95+
96+
<section>
97+
<>
98+
<div />
99+
<div />
100+
</>
101+
</section>
102+
103+
const cat = <>meow</>
104+
105+
<>{children}</>
106+
107+
<>{props.children}</>
108+
109+
<> {foo}</>
110+
111+
<SomeComponent>
112+
<>
113+
<div />
114+
<div />
115+
</>
116+
</SomeComponent>
117+
```
118+
119+
### Passing
120+
121+
```tsx
122+
{foo}
123+
124+
<Foo />
125+
126+
<>
127+
<Foo />
128+
<Bar />
129+
</>
130+
131+
<>foo {bar}</>
132+
133+
<Fragment key={item.id}>{item.value}</Fragment>
134+
```
135+
83136
## Further Reading
84137

85138
- [React: Fragment](https://react.dev/reference/react/Fragment)

0 commit comments

Comments
 (0)