@@ -3,6 +3,7 @@ use crate::services::semantic::Semantic;
33use biome_analyze:: { Rule , RuleDiagnostic , RuleDomain , context:: RuleContext , declare_lint_rule} ;
44use biome_analyze:: { RuleSource , RuleSourceKind } ;
55use biome_console:: markup;
6+ use biome_deserialize_macros:: Deserializable ;
67use biome_diagnostics:: Severity ;
78use biome_js_semantic:: SemanticModel ;
89use biome_js_syntax:: {
@@ -11,6 +12,7 @@ use biome_js_syntax::{
1112 JsxAttributeList , JsxExpressionChild , JsxTagExpression ,
1213} ;
1314use biome_rowan:: { AstNode , AstNodeList , AstSeparatedList , TextRange , declare_node_union} ;
15+ use serde:: { Deserialize , Serialize } ;
1416
1517declare_lint_rule ! {
1618 /// Disallow missing key props in iterators/collection literals.
@@ -36,6 +38,24 @@ declare_lint_rule! {
3638 /// data.map((x) => <Hello key={x.id}>{x}</Hello>);
3739 /// ```
3840 ///
41+ /// ## Options
42+ ///
43+ /// ### checkShorthandFragments
44+ ///
45+ /// React fragments can not only be created with `<React.Fragment>`, but also with shorthand
46+ /// fragments (`<></>`). To also check if those require a key, pass `true` to this option.
47+ ///
48+ /// ```json,options
49+ /// {
50+ /// "options": {
51+ /// "checkShorthandFragments": true
52+ /// }
53+ /// }
54+ /// ```
55+ /// ```jsx,expect_diagnostic,use_options
56+ /// data.map((x) => <>{x}</>);
57+ /// ```
58+ ///
3959 pub UseJsxKeyInIterable {
4060 version: "1.6.0" ,
4161 name: "useJsxKeyInIterable" ,
@@ -56,19 +76,30 @@ declare_node_union! {
5676 pub ReactComponentExpression = JsxTagExpression | JsCallExpression
5777}
5878
79+ #[ derive( Debug , Default , Clone , Serialize , Deserialize , Deserializable , Eq , PartialEq ) ]
80+ #[ cfg_attr( feature = "schemars" , derive( schemars:: JsonSchema ) ) ]
81+ #[ serde( rename_all = "camelCase" , deny_unknown_fields, default ) ]
82+ pub struct UseJsxKeyInIterableOptions {
83+ /// Set to `true` to check shorthand fragments (`<></>`)
84+ check_shorthand_fragments : bool ,
85+ }
86+
5987impl Rule for UseJsxKeyInIterable {
6088 type Query = Semantic < UseJsxKeyInIterableQuery > ;
6189 type State = TextRange ;
6290 type Signals = Box < [ Self :: State ] > ;
63- type Options = ( ) ;
91+ type Options = UseJsxKeyInIterableOptions ;
6492
6593 fn run ( ctx : & RuleContext < Self > ) -> Self :: Signals {
6694 let node = ctx. query ( ) ;
6795 let model = ctx. model ( ) ;
96+ let options = ctx. options ( ) ;
6897 match node {
69- UseJsxKeyInIterableQuery :: JsArrayExpression ( node) => handle_collections ( node, model) ,
98+ UseJsxKeyInIterableQuery :: JsArrayExpression ( node) => {
99+ handle_collections ( node, model, options)
100+ }
70101 UseJsxKeyInIterableQuery :: JsCallExpression ( node) => {
71- handle_iterators ( node, model) . unwrap_or_default ( )
102+ handle_iterators ( node, model, options ) . unwrap_or_default ( )
72103 }
73104 }
74105 . into_boxed_slice ( )
@@ -98,7 +129,11 @@ impl Rule for UseJsxKeyInIterable {
98129/// ```jsx
99130/// [<h1></h1>, <h1></h1>]
100131/// ```
101- fn handle_collections ( node : & JsArrayExpression , model : & SemanticModel ) -> Vec < TextRange > {
132+ fn handle_collections (
133+ node : & JsArrayExpression ,
134+ model : & SemanticModel ,
135+ options : & UseJsxKeyInIterableOptions ,
136+ ) -> Vec < TextRange > {
102137 let is_inside_jsx = node. parent :: < JsxExpressionChild > ( ) . is_some ( ) ;
103138 node. elements ( )
104139 . iter ( )
@@ -107,7 +142,7 @@ fn handle_collections(node: &JsArrayExpression, model: &SemanticModel) -> Vec<Te
107142 // no need to handle spread case, if the spread argument is itself a list it
108143 // will be handled during list declaration
109144 let node = AnyJsExpression :: cast ( node. into_syntax ( ) ) ?;
110- handle_potential_react_component ( node, model, is_inside_jsx)
145+ handle_potential_react_component ( node, model, is_inside_jsx, options )
111146 } )
112147 . flatten ( )
113148 . collect ( )
@@ -120,7 +155,11 @@ fn handle_collections(node: &JsArrayExpression, model: &SemanticModel) -> Vec<Te
120155/// ```jsx
121156/// data.map(x => <h1>{x}</h1>)
122157/// ```
123- fn handle_iterators ( node : & JsCallExpression , model : & SemanticModel ) -> Option < Vec < TextRange > > {
158+ fn handle_iterators (
159+ node : & JsCallExpression ,
160+ model : & SemanticModel ,
161+ options : & UseJsxKeyInIterableOptions ,
162+ ) -> Option < Vec < TextRange > > {
124163 let callee = node. callee ( ) . ok ( ) ?;
125164 let member_expression = AnyJsMemberExpression :: cast ( callee. into_syntax ( ) ) ?;
126165 let arguments = node. arguments ( ) . ok ( ) ?;
@@ -164,16 +203,16 @@ fn handle_iterators(node: &JsCallExpression, model: &SemanticModel) -> Option<Ve
164203 match callback_argument {
165204 AnyJsExpression :: JsFunctionExpression ( callback) => {
166205 let body = callback. body ( ) . ok ( ) ?;
167- Some ( handle_function_body ( & body, model, is_inside_jsx) )
206+ Some ( handle_function_body ( & body, model, is_inside_jsx, options ) )
168207 }
169208 AnyJsExpression :: JsArrowFunctionExpression ( callback) => {
170209 let body = callback. body ( ) . ok ( ) ?;
171210 match body {
172211 AnyJsFunctionBody :: AnyJsExpression ( expr) => {
173- handle_potential_react_component ( expr, model, is_inside_jsx)
212+ handle_potential_react_component ( expr, model, is_inside_jsx, options )
174213 }
175214 AnyJsFunctionBody :: JsFunctionBody ( body) => {
176- Some ( handle_function_body ( & body, model, is_inside_jsx) )
215+ Some ( handle_function_body ( & body, model, is_inside_jsx, options ) )
177216 }
178217 }
179218 }
@@ -186,6 +225,7 @@ fn handle_function_body(
186225 node : & JsFunctionBody ,
187226 model : & SemanticModel ,
188227 is_inside_jsx : bool ,
228+ options : & UseJsxKeyInIterableOptions ,
189229) -> Vec < TextRange > {
190230 // if the return statement definitely has a key prop, don't need to check the rest of the function
191231 let return_statement = node
@@ -204,7 +244,7 @@ fn handle_function_body(
204244 . unwrap_or_default ( ) ;
205245 let ranges = return_statement. and_then ( |ret| {
206246 let returned_value = ret. argument ( ) ?;
207- handle_potential_react_component ( returned_value, model, is_inside_jsx)
247+ handle_potential_react_component ( returned_value, model, is_inside_jsx, options )
208248 } ) ;
209249 if ranges. is_none ( ) && is_return_component {
210250 return vec ! [ ] ;
@@ -222,14 +262,14 @@ fn handle_function_body(
222262 . filter_map ( |declarator| {
223263 let decl = declarator. ok ( ) ?;
224264 let init = decl. initializer ( ) ?. expression ( ) . ok ( ) ?;
225- handle_potential_react_component ( init, model, is_inside_jsx)
265+ handle_potential_react_component ( init, model, is_inside_jsx, options )
226266 } )
227267 . flatten ( )
228268 . collect ( ) ,
229269 )
230270 } else if let Some ( statement) = statement. as_js_return_statement ( ) {
231271 let returned_value = statement. argument ( ) ?;
232- handle_potential_react_component ( returned_value, model, is_inside_jsx)
272+ handle_potential_react_component ( returned_value, model, is_inside_jsx, options )
233273 } else {
234274 None
235275 }
@@ -242,14 +282,19 @@ fn handle_potential_react_component(
242282 node : AnyJsExpression ,
243283 model : & SemanticModel ,
244284 is_inside_jsx : bool ,
285+ options : & UseJsxKeyInIterableOptions ,
245286) -> Option < Vec < TextRange > > {
246287 let node = unwrap_parenthesis ( node) ?;
247288
248289 if let AnyJsExpression :: JsConditionalExpression ( node) = node {
249- let consequent =
250- handle_potential_react_component ( node. consequent ( ) . ok ( ) ?, model, is_inside_jsx) ;
290+ let consequent = handle_potential_react_component (
291+ node. consequent ( ) . ok ( ) ?,
292+ model,
293+ is_inside_jsx,
294+ options,
295+ ) ;
251296 let alternate =
252- handle_potential_react_component ( node. alternate ( ) . ok ( ) ?, model, is_inside_jsx) ;
297+ handle_potential_react_component ( node. alternate ( ) . ok ( ) ?, model, is_inside_jsx, options ) ;
253298
254299 return match ( consequent, alternate) {
255300 ( Some ( consequent) , Some ( alternate) ) => Some ( [ consequent, alternate] . concat ( ) ) ,
@@ -261,24 +306,28 @@ fn handle_potential_react_component(
261306
262307 if is_inside_jsx {
263308 if let Some ( node) = ReactComponentExpression :: cast ( node. into_syntax ( ) ) {
264- let range = handle_react_component ( node, model) ?;
309+ let range = handle_react_component ( node, model, options ) ?;
265310 Some ( range)
266311 } else {
267312 None
268313 }
269314 } else {
270- let range =
271- handle_react_component ( ReactComponentExpression :: cast ( node. into_syntax ( ) ) ?, model) ?;
315+ let range = handle_react_component (
316+ ReactComponentExpression :: cast ( node. into_syntax ( ) ) ?,
317+ model,
318+ options,
319+ ) ?;
272320 Some ( range)
273321 }
274322}
275323
276324fn handle_react_component (
277325 node : ReactComponentExpression ,
278326 model : & SemanticModel ,
327+ options : & UseJsxKeyInIterableOptions ,
279328) -> Option < Vec < TextRange > > {
280329 match node {
281- ReactComponentExpression :: JsxTagExpression ( node) => handle_jsx_tag ( & node, model) ,
330+ ReactComponentExpression :: JsxTagExpression ( node) => handle_jsx_tag ( & node, model, options ) ,
282331 ReactComponentExpression :: JsCallExpression ( node) => {
283332 handle_react_non_jsx ( & node, model) . map ( |r| vec ! [ r] )
284333 }
@@ -292,13 +341,21 @@ fn handle_react_component(
292341/// ```jsx
293342/// <Hello></Hello>
294343/// ```
295- fn handle_jsx_tag ( node : & JsxTagExpression , model : & SemanticModel ) -> Option < Vec < TextRange > > {
344+ fn handle_jsx_tag (
345+ node : & JsxTagExpression ,
346+ model : & SemanticModel ,
347+ options : & UseJsxKeyInIterableOptions ,
348+ ) -> Option < Vec < TextRange > > {
296349 let tag = node. tag ( ) . ok ( ) ?;
297350 let tag = AnyJsxChild :: cast ( tag. into_syntax ( ) ) ?;
298- handle_jsx_child ( & tag, model)
351+ handle_jsx_child ( & tag, model, options )
299352}
300353
301- fn handle_jsx_child ( node : & AnyJsxChild , model : & SemanticModel ) -> Option < Vec < TextRange > > {
354+ fn handle_jsx_child (
355+ node : & AnyJsxChild ,
356+ model : & SemanticModel ,
357+ options : & UseJsxKeyInIterableOptions ,
358+ ) -> Option < Vec < TextRange > > {
302359 let mut stack: Vec < AnyJsxChild > = vec ! [ node. clone( ) ] ;
303360 let mut ranges: Vec < TextRange > = vec ! [ ] ;
304361
@@ -317,26 +374,16 @@ fn handle_jsx_child(node: &AnyJsxChild, model: &SemanticModel) -> Option<Vec<Tex
317374 }
318375 AnyJsxChild :: JsxExpressionChild ( node) => {
319376 let expr = node. expression ( ) ?;
320- if let Some ( child_ranges) = handle_potential_react_component ( expr, model, true ) {
377+ if let Some ( child_ranges) =
378+ handle_potential_react_component ( expr, model, true , options)
379+ {
321380 ranges. extend ( child_ranges) ;
322381 }
323382 }
324383 AnyJsxChild :: JsxFragment ( node) => {
325- let has_any_tags = node. children ( ) . iter ( ) . any ( |child| match & child {
326- AnyJsxChild :: JsxElement ( _) | AnyJsxChild :: JsxSelfClosingElement ( _) => true ,
327- // HACK: don't flag the entire fragment if there's a conditional expression
328- AnyJsxChild :: JsxExpressionChild ( node) => node
329- . expression ( )
330- . is_some_and ( |n| n. as_js_conditional_expression ( ) . is_some ( ) ) ,
331- _ => false ,
332- } ) ;
333-
334- if !has_any_tags {
335- ranges. push ( node. range ( ) ) ;
336- break ;
384+ if options. check_shorthand_fragments {
385+ ranges. push ( node. range ( ) )
337386 }
338-
339- stack. extend ( node. children ( ) ) ;
340387 }
341388 _ => { }
342389 }
0 commit comments