11import * as AST from "@eslint-react/ast" ;
2- import { isFragmentElement } from "@eslint-react/core" ;
32import * as JSX from "@eslint-react/jsx" ;
43import type { RuleContext , RuleFeature } from "@eslint-react/types" ;
54import { AST_NODE_TYPES } from "@typescript-eslint/types" ;
65import type { TSESTree } from "@typescript-eslint/utils" ;
7- import { isMatching , P } from "ts-pattern" ;
86
97import { 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-
6476export 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 } ,
0 commit comments