Skip to content

Commit c715881

Browse files
authored
refactor: JSX fragments related rules no longer rely on 'jsxPragma' and 'jsxPragmaFrag' in 'react-x' settings (#893)
1 parent 72d96f6 commit c715881

File tree

15 files changed

+414
-475
lines changed

15 files changed

+414
-475
lines changed

.markdownlint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"extends": "markdownlint/style/all",
33
"MD013": false,
4+
"MD024": false,
45
"MD033": false,
56
"MD041": false
67
}

CHANGELOG.md

Lines changed: 347 additions & 339 deletions
Large diffs are not rendered by default.

packages/core/docs/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
- [isDeclaredInRenderPropLoose](functions/isDeclaredInRenderPropLoose.md)
7676
- [isForwardRef](functions/isForwardRef.md)
7777
- [isForwardRefCall](functions/isForwardRefCall.md)
78-
- [isFragmentElement](functions/isFragmentElement.md)
7978
- [isFromReact](functions/isFromReact.md)
8079
- [isFunctionOfRenderMethod](functions/isFunctionOfRenderMethod.md)
8180
- [isInitializedFromReact](functions/isInitializedFromReact.md)

packages/core/docs/functions/isFragmentElement.md

Lines changed: 0 additions & 31 deletions
This file was deleted.

packages/core/docs/functions/isInitializedFromReact.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ The identifier that’s used for JSX Element creation.
166166

167167
`"createElement"`
168168

169+
**Deprecated**
170+
169171
#### jsxPragmaFrag
170172

171173
`string`
@@ -180,6 +182,8 @@ This should not be a member expression (i.e. use "Fragment" instead of "React.Fr
180182

181183
`"Fragment"`
182184

185+
**Deprecated**
186+
183187
#### polymorphicPropName
184188

185189
`string`

packages/core/src/element/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from "./get-element-represent-name";
22
export * from "./hierarchy";
3-
export * from "./misc";

packages/core/src/element/misc.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ ruleTester.run(RULE_NAME, rule, {
197197
/* tsx */ `<>{cloneElement(children, { ref: childrenRef })}</>`,
198198
{
199199
code: /* tsx */ `
200-
<React.SomeFragment>
200+
<SomeReact.SomeFragment>
201201
{<Foo />}
202-
</React.SomeFragment>
202+
</SomeReact.SomeFragment>
203203
`,
204204
settings: {
205205
"react-x": {
@@ -208,14 +208,6 @@ ruleTester.run(RULE_NAME, rule, {
208208
},
209209
},
210210
},
211-
{
212-
code: /* tsx */ `<NotReact.Fragment />`,
213-
settings: {
214-
"react-x": {
215-
strictImportCheck: true,
216-
},
217-
},
218-
},
219211
{
220212
code: /* tsx */ `{foo}`,
221213
options: [{ allowExpressions: false }],

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

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import * as AST from "@eslint-react/ast";
2-
import { isFragmentElement } from "@eslint-react/core";
32
import * as JSX from "@eslint-react/jsx";
43
import type { RuleContext, RuleFeature } from "@eslint-react/types";
54
import { AST_NODE_TYPES } from "@typescript-eslint/types";
65
import type { TSESTree } from "@typescript-eslint/utils";
7-
import { isMatching, P } from "ts-pattern";
86

97
import { createRule } from "../utils";
108

@@ -19,48 +17,62 @@ export type MessageID =
1917
| "noUselessFragment"
2018
| "noUselessFragmentInBuiltIn";
2119

22-
// eslint-disable-next-line @typescript-eslint/consistent-return
23-
function check(
20+
type Options = [
21+
{
22+
allowExpressions: boolean;
23+
},
24+
];
25+
26+
const defaultOptions = [{
27+
allowExpressions: true,
28+
}] as const satisfies Options;
29+
30+
function checkAndReport(
2431
node: TSESTree.JSXElement | TSESTree.JSXFragment,
2532
context: RuleContext,
2633
allowExpressions: boolean,
2734
) {
2835
const initialScope = context.sourceCode.getScope(node);
36+
// return if the fragment is keyed (e.g. <Fragment key={key}>)
2937
if (JSX.isKeyedElement(node, initialScope)) return;
38+
// report if the fragment is placed inside a built-in component (e.g. <div><></></div>)
3039
if (JSX.isBuiltInElement(node.parent)) context.report({ messageId: "noUselessFragmentInBuiltIn", node });
40+
// report and return if the fragment has no children (e.g. <></>)
3141
if (node.children.length === 0) return context.report({ messageId: "noUselessFragment", node });
32-
const isChildren = AST.isOneOf([AST_NODE_TYPES.JSXElement, AST_NODE_TYPES.JSXFragment])(node.parent);
33-
const [firstChildren] = node.children;
34-
// <Foo content={<>ee eeee eeee ...</>} />
35-
if (allowExpressions && node.children.length === 1 && JSX.isLiteral(firstChildren) && !isChildren) return;
36-
if (!allowExpressions && isChildren) {
42+
const isChildElement = AST.isOneOf([AST_NODE_TYPES.JSXElement, AST_NODE_TYPES.JSXFragment])(node.parent);
43+
switch (true) {
44+
// <Foo content={<>ee eeee eeee ...</>} />
45+
case allowExpressions
46+
&& !isChildElement
47+
&& node.children.length === 1
48+
&& JSX.isLiteral(node.children.at(0)): {
49+
return;
50+
}
3751
// <Foo><>hello, world</></Foo>
38-
return context.report({ messageId: "noUselessFragment", node });
39-
} else if (!allowExpressions && !isChildren && node.children.length === 1) {
40-
// const foo = <>{children}</>;
41-
// return <>{children}</>;
42-
return context.report({ messageId: "noUselessFragment", node });
52+
case !allowExpressions
53+
&& isChildElement: {
54+
return context.report({ messageId: "noUselessFragment", node });
55+
}
56+
case !allowExpressions
57+
&& !isChildElement
58+
&& node.children.length === 1: {
59+
// const foo = <>{children}</>;
60+
// return <>{children}</>;
61+
return context.report({ messageId: "noUselessFragment", node });
62+
}
4363
}
4464
const nonPaddingChildren = node.children.filter((child) => !JSX.isPaddingSpaces(child));
45-
if (nonPaddingChildren.length > 1) return;
46-
if (nonPaddingChildren.length === 0) return context.report({ messageId: "noUselessFragment", node });
47-
const [first] = nonPaddingChildren;
48-
if (
49-
isMatching({ type: AST_NODE_TYPES.JSXExpressionContainer, expression: P.not(AST_NODE_TYPES.CallExpression) }, first)
50-
) return;
51-
context.report({ messageId: "noUselessFragment", node });
65+
const firstNonPaddingChild = nonPaddingChildren.at(0);
66+
switch (true) {
67+
case nonPaddingChildren.length === 0:
68+
case nonPaddingChildren.length === 1
69+
&& firstNonPaddingChild?.type !== AST_NODE_TYPES.JSXExpressionContainer: {
70+
return context.report({ messageId: "noUselessFragment", node });
71+
}
72+
}
73+
return;
5274
}
5375

54-
type Options = [
55-
{
56-
allowExpressions: boolean;
57-
},
58-
];
59-
60-
const defaultOptions = [{
61-
allowExpressions: true,
62-
}] as const satisfies Options;
63-
6476
export default createRule<Options, MessageID>({
6577
meta: {
6678
type: "problem",
@@ -88,11 +100,11 @@ export default createRule<Options, MessageID>({
88100
const { allowExpressions = true } = option;
89101
return {
90102
JSXElement(node) {
91-
if (!isFragmentElement(node, context)) return;
92-
check(node, context, allowExpressions);
103+
if (JSX.getElementName(node.openingElement).split(".").at(-1) !== "Fragment") return;
104+
checkAndReport(node, context, allowExpressions);
93105
},
94106
JSXFragment(node) {
95-
check(node, context, allowExpressions);
107+
checkAndReport(node, context, allowExpressions);
96108
},
97109
};
98110
},

packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-fragment.spec.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,5 @@ ruleTester.run(RULE_NAME, rule, {
3333
"<Fragment key={item.id}>{item.value}</Fragment>",
3434
"<Fooo content={<>eeee ee eeeeeee eeeeeeee</>} />",
3535
"<>{foos.map(foo => foo)}</>",
36-
{
37-
code: /* tsx */ `<NotReact.Fragment />`,
38-
settings: {
39-
"react-x": {
40-
strictImportCheck: true,
41-
},
42-
},
43-
},
4436
],
4537
});

0 commit comments

Comments
 (0)