@@ -5,29 +5,59 @@ use oxc_ast::{
55use oxc_diagnostics:: OxcDiagnostic ;
66use oxc_macros:: declare_oxc_lint;
77use oxc_span:: { GetSpan , Span } ;
8+ use serde_json:: Value ;
89
910use crate :: { AstNode , context:: LintContext , rule:: Rule } ;
1011
11- fn jsx_fragments_diagnostic ( span : Span ) -> OxcDiagnostic {
12- OxcDiagnostic :: warn ( "Shorthand form for React fragments is preferred" )
13- . with_help ( "Use <></> instead of <React.Fragment></React.Fragment>" )
14- . with_label ( span)
12+ fn jsx_fragments_diagnostic ( span : Span , mode : FragmentMode ) -> OxcDiagnostic {
13+ let msg = if mode == FragmentMode :: Element {
14+ "Standard form for React fragments is preferred"
15+ } else {
16+ "Shorthand form for React fragments is preferred"
17+ } ;
18+ let help = if mode == FragmentMode :: Element {
19+ "Use <React.Fragment></React.Fragment> instead of <></>"
20+ } else {
21+ "Use <></> instead of <React.Fragment></React.Fragment>"
22+ } ;
23+ OxcDiagnostic :: warn ( msg) . with_help ( help) . with_label ( span)
1524}
1625
1726#[ derive( Debug , Default , Clone ) ]
18- pub struct JsxFragments ;
27+ pub struct JsxFragments {
28+ mode : FragmentMode ,
29+ }
30+
31+ #[ derive( Debug , Default , Clone , PartialEq , Eq , Copy ) ]
32+ pub enum FragmentMode {
33+ #[ default]
34+ Syntax ,
35+ Element ,
36+ }
37+
38+ impl From < & str > for FragmentMode {
39+ fn from ( value : & str ) -> Self {
40+ if value == "element" { Self :: Element } else { Self :: Syntax }
41+ }
42+ }
1943
2044// See <https://github.com/oxc-project/oxc/issues/6050> for documentation details.
2145declare_oxc_lint ! (
2246 /// ### What it does
2347 ///
24- /// Enforces the shorthand form for React fragments
48+ /// Enforces the shorthand or standard form for React Fragments.
2549 ///
2650 /// ### Why is this bad?
2751 ///
28- /// Shorthand form is much more succinct and readable than the fully qualified element name.
52+ /// Makes code using fragments more consistent one way or the other.
53+ ///
54+ /// ### Options
55+ ///
56+ /// `{ "mode": "syntax" | "element" }`
2957 ///
30- /// ### Examples
58+ /// #### `syntax` mode
59+ /// This is the default mode. It will enforce the shorthand syntax for React fragments, with one exception.
60+ /// Keys or attributes are not supported by the shorthand syntax, so the rule will not warn on standard-form fragments that use those.
3161 ///
3262 /// Examples of **incorrect** code for this rule:
3363 /// ```jsx
@@ -42,26 +72,54 @@ declare_oxc_lint!(
4272 /// ```jsx
4373 /// <React.Fragment key="key"><Foo /></React.Fragment>
4474 /// ```
75+ ///
76+ /// #### `element` mode
77+ /// This mode enforces the standard form for React fragments.
78+ ///
79+ /// Examples of **incorrect** code for this rule:
80+ /// ```jsx
81+ /// <><Foo /></>
82+ /// ```
83+ ///
84+ /// Examples of **correct** code for this rule:
85+ /// ```jsx
86+ /// <React.Fragment><Foo /></React.Fragment>
87+ /// ```
88+ ///
89+ /// ```jsx
90+ /// <React.Fragment key="key"><Foo /></React.Fragment>
91+ /// ```
4592 JsxFragments ,
4693 react,
4794 style,
4895 fix
4996) ;
5097
5198impl Rule for JsxFragments {
99+ fn from_configuration ( value : Value ) -> Self {
100+ let obj = value. get ( 0 ) ;
101+ Self {
102+ mode : obj
103+ . and_then ( |v| v. get ( "mode" ) )
104+ . and_then ( Value :: as_str)
105+ . map ( FragmentMode :: from)
106+ . unwrap_or_default ( ) ,
107+ }
108+ }
109+
52110 fn run < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) {
53111 match node. kind ( ) {
54- AstKind :: JSXElement ( jsx_elem) => {
112+ AstKind :: JSXElement ( jsx_elem) if self . mode == FragmentMode :: Syntax => {
55113 let Some ( closing_element) = & jsx_elem. closing_element else {
56114 return ;
57115 } ;
58116 if !is_jsx_fragment ( & jsx_elem. opening_element )
59- || jsx_elem. opening_element . attributes . len ( ) > 0
117+ || ! jsx_elem. opening_element . attributes . is_empty ( )
60118 {
61119 return ;
62120 }
63121 ctx. diagnostic_with_fix (
64- jsx_fragments_diagnostic ( jsx_elem. opening_element . name . span ( ) ) ,
122+ jsx_fragments_diagnostic ( jsx_elem. opening_element . name . span ( ) , self . mode ) ,
65123 |fixer| {
66124 let before_opening_tag = ctx. source_range ( Span :: new (
67125 jsx_elem. span ( ) . start ,
@@ -76,15 +134,41 @@ impl Rule for JsxFragments {
76134 jsx_elem. span ( ) . end ,
77135 ) ) ;
78136 let mut replacement = String :: new ( ) ;
79- replacement. push_str ( & before_opening_tag) ;
137+ replacement. push_str ( before_opening_tag) ;
80138 replacement. push_str ( "<>" ) ;
81- replacement. push_str ( & between_opening_tag_and_closing_tag) ;
139+ replacement. push_str ( between_opening_tag_and_closing_tag) ;
82140 replacement. push_str ( "</>" ) ;
83- replacement. push_str ( & after_closing_tag) ;
141+ replacement. push_str ( after_closing_tag) ;
84142 fixer. replace ( jsx_elem. span ( ) , replacement)
85143 } ,
86144 ) ;
87145 }
146+ AstKind :: JSXFragment ( jsx_frag) if self . mode == FragmentMode :: Element => {
147+ ctx. diagnostic_with_fix (
148+ jsx_fragments_diagnostic ( jsx_frag. opening_fragment . span ( ) , self . mode ) ,
149+ |fixer| {
150+ let before_opening_tag = ctx. source_range ( Span :: new (
151+ jsx_frag. span ( ) . start ,
152+ jsx_frag. opening_fragment . span ( ) . start ,
153+ ) ) ;
154+ let between_opening_tag_and_closing_tag = ctx. source_range ( Span :: new (
155+ jsx_frag. opening_fragment . span ( ) . end ,
156+ jsx_frag. closing_fragment . span ( ) . start ,
157+ ) ) ;
158+ let after_closing_tag = ctx. source_range ( Span :: new (
159+ jsx_frag. closing_fragment . span ( ) . end ,
160+ jsx_frag. span ( ) . end ,
161+ ) ) ;
162+ let mut replacement = String :: new ( ) ;
163+ replacement. push_str ( before_opening_tag) ;
164+ replacement. push_str ( "<React.Fragment>" ) ;
165+ replacement. push_str ( between_opening_tag_and_closing_tag) ;
166+ replacement. push_str ( "</React.Fragment>" ) ;
167+ replacement. push_str ( after_closing_tag) ;
168+ fixer. replace ( jsx_frag. span ( ) , replacement)
169+ } ,
170+ ) ;
171+ }
88172 _ => { }
89173 }
90174 }
@@ -113,20 +197,31 @@ fn is_jsx_fragment(elem: &JSXOpeningElement) -> bool {
113197#[ test]
114198fn test ( ) {
115199 use crate :: tester:: Tester ;
200+ use serde_json:: json;
116201
117202 let pass = vec ! [
118- "<><Foo /></>" ,
119- "<Fragment key=\" key\" ><Foo /></Fragment>" ,
120- "<React.Fragment key=\" key\" ><Foo /></React.Fragment>" ,
121- "<Fragment />" ,
122- "<React.Fragment />" ,
203+ ( "<><Foo /></>" , None ) ,
204+ ( r#"<Fragment key="key"><Foo /></Fragment>"# , None ) ,
205+ ( r#"<React.Fragment key="key"><Foo /></React.Fragment>"# , None ) ,
206+ ( "<Fragment />" , None ) ,
207+ ( "<React.Fragment />" , None ) ,
208+ ( "<React.Fragment><Foo /></React.Fragment>" , Some ( json!( [ { "mode" : "element" } ] ) ) ) ,
123209 ] ;
124210
125- let fail = vec ! [ "<Fragment><Foo /></Fragment>" , "<React.Fragment><Foo /></React.Fragment>" ] ;
211+ let fail = vec ! [
212+ ( "<Fragment><Foo /></Fragment>" , None ) ,
213+ ( "<React.Fragment><Foo /></React.Fragment>" , None ) ,
214+ ( "<><Foo /></>" , Some ( json!( [ { "mode" : "element" } ] ) ) ) ,
215+ ] ;
126216
127217 let fix = vec ! [
128- ( "<Fragment><Foo /></Fragment>" , "<><Foo /></>" ) ,
129- ( "<React.Fragment><Foo /></React.Fragment>" , "<><Foo /></>" ) ,
218+ ( "<Fragment><Foo /></Fragment>" , "<><Foo /></>" , None ) ,
219+ ( "<React.Fragment><Foo /></React.Fragment>" , "<><Foo /></>" , None ) ,
220+ (
221+ "<><Foo /></>" ,
222+ "<React.Fragment><Foo /></React.Fragment>" ,
223+ Some ( json!( [ { "mode" : "element" } ] ) ) ,
224+ ) ,
130225 ] ;
131226 Tester :: new ( JsxFragments :: NAME , JsxFragments :: PLUGIN , pass, fail)
132227 . expect_fix ( fix)
0 commit comments