diff --git a/.changeset/silent-fans-battle.md b/.changeset/silent-fans-battle.md new file mode 100644 index 000000000000..1aade3f831d4 --- /dev/null +++ b/.changeset/silent-fans-battle.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Updates the [`useJsxKeyInIterable`](https://biomejs.dev/linter/rules/use-jsx-key-in-iterable/) rule to more closely match the behavior of the ESLint plugin (e.g. mark the whole fragment as incorrect when no key is present). This also adds the option to check shorthand fragments (`<>`) diff --git a/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs b/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs index d447b0565260..694db20b16a9 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs @@ -3,6 +3,7 @@ use crate::services::semantic::Semantic; use biome_analyze::{Rule, RuleDiagnostic, RuleDomain, context::RuleContext, declare_lint_rule}; use biome_analyze::{RuleSource, RuleSourceKind}; use biome_console::markup; +use biome_deserialize_macros::Deserializable; use biome_diagnostics::Severity; use biome_js_semantic::SemanticModel; use biome_js_syntax::{ @@ -11,6 +12,7 @@ use biome_js_syntax::{ JsxAttributeList, JsxExpressionChild, JsxTagExpression, }; use biome_rowan::{AstNode, AstNodeList, AstSeparatedList, TextRange, declare_node_union}; +use serde::{Deserialize, Serialize}; declare_lint_rule! { /// Disallow missing key props in iterators/collection literals. @@ -36,6 +38,24 @@ declare_lint_rule! { /// data.map((x) => {x}); /// ``` /// + /// ## Options + /// + /// ### checkShorthandFragments + /// + /// React fragments can not only be created with ``, but also with shorthand + /// fragments (`<>`). To also check if those require a key, pass `true` to this option. + /// + /// ```json,options + /// { + /// "options": { + /// "checkShorthandFragments": true + /// } + /// } + /// ``` + /// ```jsx,expect_diagnostic,use_options + /// data.map((x) => <>{x}); + /// ``` + /// pub UseJsxKeyInIterable { version: "1.6.0", name: "useJsxKeyInIterable", @@ -56,19 +76,30 @@ declare_node_union! { pub ReactComponentExpression = JsxTagExpression | JsCallExpression } +#[derive(Debug, Default, Clone, Serialize, Deserialize, Deserializable, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct UseJsxKeyInIterableOptions { + /// Set to `true` to check shorthand fragments (`<>`) + check_shorthand_fragments: bool, +} + impl Rule for UseJsxKeyInIterable { type Query = Semantic; type State = TextRange; type Signals = Box<[Self::State]>; - type Options = (); + type Options = UseJsxKeyInIterableOptions; fn run(ctx: &RuleContext) -> Self::Signals { let node = ctx.query(); let model = ctx.model(); + let options = ctx.options(); match node { - UseJsxKeyInIterableQuery::JsArrayExpression(node) => handle_collections(node, model), + UseJsxKeyInIterableQuery::JsArrayExpression(node) => { + handle_collections(node, model, options) + } UseJsxKeyInIterableQuery::JsCallExpression(node) => { - handle_iterators(node, model).unwrap_or_default() + handle_iterators(node, model, options).unwrap_or_default() } } .into_boxed_slice() @@ -98,7 +129,11 @@ impl Rule for UseJsxKeyInIterable { /// ```jsx /// [

,

] /// ``` -fn handle_collections(node: &JsArrayExpression, model: &SemanticModel) -> Vec { +fn handle_collections( + node: &JsArrayExpression, + model: &SemanticModel, + options: &UseJsxKeyInIterableOptions, +) -> Vec { let is_inside_jsx = node.parent::().is_some(); node.elements() .iter() @@ -107,7 +142,7 @@ fn handle_collections(node: &JsArrayExpression, model: &SemanticModel) -> Vec Vec

{x}

) /// ``` -fn handle_iterators(node: &JsCallExpression, model: &SemanticModel) -> Option> { +fn handle_iterators( + node: &JsCallExpression, + model: &SemanticModel, + options: &UseJsxKeyInIterableOptions, +) -> Option> { let callee = node.callee().ok()?; let member_expression = AnyJsMemberExpression::cast(callee.into_syntax())?; let arguments = node.arguments().ok()?; @@ -164,16 +203,16 @@ fn handle_iterators(node: &JsCallExpression, model: &SemanticModel) -> Option { let body = callback.body().ok()?; - Some(handle_function_body(&body, model, is_inside_jsx)) + Some(handle_function_body(&body, model, is_inside_jsx, options)) } AnyJsExpression::JsArrowFunctionExpression(callback) => { let body = callback.body().ok()?; match body { AnyJsFunctionBody::AnyJsExpression(expr) => { - handle_potential_react_component(expr, model, is_inside_jsx) + handle_potential_react_component(expr, model, is_inside_jsx, options) } AnyJsFunctionBody::JsFunctionBody(body) => { - Some(handle_function_body(&body, model, is_inside_jsx)) + Some(handle_function_body(&body, model, is_inside_jsx, options)) } } } @@ -186,6 +225,7 @@ fn handle_function_body( node: &JsFunctionBody, model: &SemanticModel, is_inside_jsx: bool, + options: &UseJsxKeyInIterableOptions, ) -> Vec { // if the return statement definitely has a key prop, don't need to check the rest of the function let return_statement = node @@ -204,7 +244,7 @@ fn handle_function_body( .unwrap_or_default(); let ranges = return_statement.and_then(|ret| { let returned_value = ret.argument()?; - handle_potential_react_component(returned_value, model, is_inside_jsx) + handle_potential_react_component(returned_value, model, is_inside_jsx, options) }); if ranges.is_none() && is_return_component { return vec![]; @@ -222,14 +262,14 @@ fn handle_function_body( .filter_map(|declarator| { let decl = declarator.ok()?; let init = decl.initializer()?.expression().ok()?; - handle_potential_react_component(init, model, is_inside_jsx) + handle_potential_react_component(init, model, is_inside_jsx, options) }) .flatten() .collect(), ) } else if let Some(statement) = statement.as_js_return_statement() { let returned_value = statement.argument()?; - handle_potential_react_component(returned_value, model, is_inside_jsx) + handle_potential_react_component(returned_value, model, is_inside_jsx, options) } else { None } @@ -242,14 +282,19 @@ fn handle_potential_react_component( node: AnyJsExpression, model: &SemanticModel, is_inside_jsx: bool, + options: &UseJsxKeyInIterableOptions, ) -> Option> { let node = unwrap_parenthesis(node)?; if let AnyJsExpression::JsConditionalExpression(node) = node { - let consequent = - handle_potential_react_component(node.consequent().ok()?, model, is_inside_jsx); + let consequent = handle_potential_react_component( + node.consequent().ok()?, + model, + is_inside_jsx, + options, + ); let alternate = - handle_potential_react_component(node.alternate().ok()?, model, is_inside_jsx); + handle_potential_react_component(node.alternate().ok()?, model, is_inside_jsx, options); return match (consequent, alternate) { (Some(consequent), Some(alternate)) => Some([consequent, alternate].concat()), @@ -261,14 +306,17 @@ fn handle_potential_react_component( if is_inside_jsx { if let Some(node) = ReactComponentExpression::cast(node.into_syntax()) { - let range = handle_react_component(node, model)?; + let range = handle_react_component(node, model, options)?; Some(range) } else { None } } else { - let range = - handle_react_component(ReactComponentExpression::cast(node.into_syntax())?, model)?; + let range = handle_react_component( + ReactComponentExpression::cast(node.into_syntax())?, + model, + options, + )?; Some(range) } } @@ -276,9 +324,10 @@ fn handle_potential_react_component( fn handle_react_component( node: ReactComponentExpression, model: &SemanticModel, + options: &UseJsxKeyInIterableOptions, ) -> Option> { match node { - ReactComponentExpression::JsxTagExpression(node) => handle_jsx_tag(&node, model), + ReactComponentExpression::JsxTagExpression(node) => handle_jsx_tag(&node, model, options), ReactComponentExpression::JsCallExpression(node) => { handle_react_non_jsx(&node, model).map(|r| vec![r]) } @@ -292,13 +341,21 @@ fn handle_react_component( /// ```jsx /// /// ``` -fn handle_jsx_tag(node: &JsxTagExpression, model: &SemanticModel) -> Option> { +fn handle_jsx_tag( + node: &JsxTagExpression, + model: &SemanticModel, + options: &UseJsxKeyInIterableOptions, +) -> Option> { let tag = node.tag().ok()?; let tag = AnyJsxChild::cast(tag.into_syntax())?; - handle_jsx_child(&tag, model) + handle_jsx_child(&tag, model, options) } -fn handle_jsx_child(node: &AnyJsxChild, model: &SemanticModel) -> Option> { +fn handle_jsx_child( + node: &AnyJsxChild, + model: &SemanticModel, + options: &UseJsxKeyInIterableOptions, +) -> Option> { let mut stack: Vec = vec![node.clone()]; let mut ranges: Vec = vec![]; @@ -317,26 +374,16 @@ fn handle_jsx_child(node: &AnyJsxChild, model: &SemanticModel) -> Option { let expr = node.expression()?; - if let Some(child_ranges) = handle_potential_react_component(expr, model, true) { + if let Some(child_ranges) = + handle_potential_react_component(expr, model, true, options) + { ranges.extend(child_ranges); } } AnyJsxChild::JsxFragment(node) => { - let has_any_tags = node.children().iter().any(|child| match &child { - AnyJsxChild::JsxElement(_) | AnyJsxChild::JsxSelfClosingElement(_) => true, - // HACK: don't flag the entire fragment if there's a conditional expression - AnyJsxChild::JsxExpressionChild(node) => node - .expression() - .is_some_and(|n| n.as_js_conditional_expression().is_some()), - _ => false, - }); - - if !has_any_tags { - ranges.push(node.range()); - break; + if options.check_shorthand_fragments { + ranges.push(node.range()) } - - stack.extend(node.children()); } _ => {} } diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx index 2b58242283e6..66b3b7348e95 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx @@ -6,8 +6,6 @@ import React from "react"; [, xyz ? : , ]; -[<>, <>, <>]; - data.map(x => {x}); data.map(x => <>{x}); @@ -44,14 +42,6 @@ React.Children.map(c => React.cloneElement(c)); return item.condition ?
:
foo
; }); -[].map((item) => { - return <>
{item}
; -}); - -[].map((item) => { - return <>{item.condition ?
:
foo
}; -}); - [].map((item) => { const x = 5; const div =
{x}
; @@ -63,3 +53,5 @@ React.Children.map(c => React.cloneElement(c)); const div =
{x}
; return div; }); + +data.map((item) =>

{item}

) diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx.snap index 5eff875d69fb..5dd9f49c1bb1 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx.snap @@ -1,7 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs expression: invalid.jsx -snapshot_kind: text --- # Input ```jsx @@ -13,8 +12,6 @@ import React from "react"; [, xyz ? : , ]; -[<>, <>, <>]; - data.map(x => {x}); data.map(x => <>{x}); @@ -51,14 +48,6 @@ React.Children.map(c => React.cloneElement(c)); return item.condition ?
:
foo
; }); -[].map((item) => { - return <>
{item}
; -}); - -[].map((item) => { - return <>{item.condition ?
:
foo
}; -}); - [].map((item) => { const x = 5; const div =
{x}
; @@ -71,6 +60,8 @@ React.Children.map(c => React.cloneElement(c)); return div; }); +data.map((item) =>

{item}

) + ``` # Diagnostics @@ -198,7 +189,7 @@ invalid.jsx:7:2 lint/correctness/useJsxKeyInIterable ━━━━━━━━━ > 7 │ [, xyz ? : , ]; │ ^^^^^^^^^ 8 │ - 9 │ [<>, <>, <>]; + 9 │ data.map(x => {x}); i The order of the items may change, and having a key can help React identify which item was moved. @@ -217,7 +208,7 @@ invalid.jsx:7:19 lint/correctness/useJsxKeyInIterable ━━━━━━━━ > 7 │ [, xyz ? : , ]; │ ^^^^^^^^^ 8 │ - 9 │ [<>, <>, <>]; + 9 │ data.map(x => {x}); i The order of the items may change, and having a key can help React identify which item was moved. @@ -236,7 +227,7 @@ invalid.jsx:7:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━ > 7 │ [, xyz ? : , ]; │ ^^^^^^^^^ 8 │ - 9 │ [<>, <>, <>]; + 9 │ data.map(x => {x}); i The order of the items may change, and having a key can help React identify which item was moved. @@ -255,45 +246,7 @@ invalid.jsx:7:41 lint/correctness/useJsxKeyInIterable ━━━━━━━━ > 7 │ [, xyz ? : , ]; │ ^^^^^^^^^ 8 │ - 9 │ [<>, <>, <>]; - - i The order of the items may change, and having a key can help React identify which item was moved. - - i Check the React documentation. - - -``` - -``` -invalid.jsx:9:2 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Missing key property for this element in iterable. - - 7 │ [, xyz ? : , ]; - 8 │ - > 9 │ [<>, <>, <>]; - │ ^^^^^ - 10 │ - 11 │ data.map(x => {x}); - - i The order of the items may change, and having a key can help React identify which item was moved. - - i Check the React documentation. - - -``` - -``` -invalid.jsx:9:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Missing key property for this element in iterable. - - 7 │ [, xyz ? : , ]; - 8 │ - > 9 │ [<>, <>, <>]; - │ ^^^^^ - 10 │ - 11 │ data.map(x => {x}); + 9 │ data.map(x => {x}); i The order of the items may change, and having a key can help React identify which item was moved. @@ -303,35 +256,16 @@ invalid.jsx:9:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━ ``` ``` -invalid.jsx:9:16 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:9:15 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. 7 │ [, xyz ? : , ]; 8 │ - > 9 │ [<>, <>, <>]; - │ ^^^^^ - 10 │ - 11 │ data.map(x => {x}); - - i The order of the items may change, and having a key can help React identify which item was moved. - - i Check the React documentation. - - -``` - -``` -invalid.jsx:11:15 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Missing key property for this element in iterable. - - 9 │ [<>, <>, <>]; - 10 │ - > 11 │ data.map(x => {x}); + > 9 │ data.map(x => {x}); │ ^^^^^^^ - 12 │ - 13 │ data.map(x => <>{x}); + 10 │ + 11 │ data.map(x => <>{x}); i The order of the items may change, and having a key can help React identify which item was moved. @@ -341,16 +275,16 @@ invalid.jsx:11:15 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:13:15 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:15:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 11 │ data.map(x => {x}); - 12 │ - > 13 │ data.map(x => <>{x}); - │ ^^^^^^^^ + 13 │ data.forEach(x => data1.push(<>{x})); 14 │ - 15 │ data.forEach(x => data1.push(<>{x})); + > 15 │ Array.from([1, 2, 3], (x) => {x}); + │ ^^^^^^^ + 16 │ + 17 │ Array.from([1, 2, 3], (x) => { i The order of the items may change, and having a key can help React identify which item was moved. @@ -360,16 +294,15 @@ invalid.jsx:13:15 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:17:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:18:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 15 │ data.forEach(x => data1.push(<>{x})); - 16 │ - > 17 │ Array.from([1, 2, 3], (x) => {x}); - │ ^^^^^^^ - 18 │ - 19 │ Array.from([1, 2, 3], (x) => { + 17 │ Array.from([1, 2, 3], (x) => { + > 18 │ return {x} + │ ^^^^^^^ + 19 │ }); + 20 │ i The order of the items may change, and having a key can help React identify which item was moved. @@ -379,15 +312,16 @@ invalid.jsx:17:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:20:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:21:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 19 │ Array.from([1, 2, 3], (x) => { - > 20 │ return {x} - │ ^^^^^^^ - 21 │ }); + 19 │ }); + 20 │ + > 21 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + │ ^^^^^^ 22 │ + 23 │ data.map(c => React.createElement("h1")); i The order of the items may change, and having a key can help React identify which item was moved. @@ -397,16 +331,16 @@ invalid.jsx:20:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:23:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:21:48 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 21 │ }); + 19 │ }); + 20 │ + > 21 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + │ ^^^^^^ 22 │ - > 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; - │ ^^^^^^ - 24 │ - 25 │ data.map(c => React.createElement("h1")); + 23 │ data.map(c => React.createElement("h1")); i The order of the items may change, and having a key can help React identify which item was moved. @@ -416,16 +350,16 @@ invalid.jsx:23:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:23:48 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:21:75 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 21 │ }); + 19 │ }); + 20 │ + > 21 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + │ ^^^^^^ 22 │ - > 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; - │ ^^^^^^ - 24 │ - 25 │ data.map(c => React.createElement("h1")); + 23 │ data.map(c => React.createElement("h1")); i The order of the items may change, and having a key can help React identify which item was moved. @@ -435,16 +369,16 @@ invalid.jsx:23:48 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:23:75 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:23:34 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 21 │ }); + 21 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; 22 │ - > 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; - │ ^^^^^^ + > 23 │ data.map(c => React.createElement("h1")); + │ ^^^^^^ 24 │ - 25 │ data.map(c => React.createElement("h1")); + 25 │ React.Children.map(c => React.cloneElement(c)); i The order of the items may change, and having a key can help React identify which item was moved. @@ -454,16 +388,16 @@ invalid.jsx:23:75 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:25:34 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:25:43 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + 23 │ data.map(c => React.createElement("h1")); 24 │ - > 25 │ data.map(c => React.createElement("h1")); - │ ^^^^^^ + > 25 │ React.Children.map(c => React.cloneElement(c)); + │ ^^^ 26 │ - 27 │ React.Children.map(c => React.cloneElement(c)); + 27 │ (

{data.map(x =>

{x}

)}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -473,16 +407,15 @@ invalid.jsx:25:34 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:27:43 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:42:26 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 25 │ data.map(c => React.createElement("h1")); - 26 │ - > 27 │ React.Children.map(c => React.cloneElement(c)); - │ ^^^ - 28 │ - 29 │ (

{data.map(x =>

{x}

)}) + 41 │ [].map((item) => { + > 42 │ return item.condition ?
:
foo
; + │ ^^^^^^^ + 43 │ }); + 44 │ i The order of the items may change, and having a key can help React identify which item was moved. @@ -492,15 +425,15 @@ invalid.jsx:27:43 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:44:26 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:42:36 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 43 │ [].map((item) => { - > 44 │ return item.condition ?
:
foo
; - │ ^^^^^^^ - 45 │ }); - 46 │ + 41 │ [].map((item) => { + > 42 │ return item.condition ?
:
foo
; + │ ^^^^^ + 43 │ }); + 44 │ i The order of the items may change, and having a key can help React identify which item was moved. @@ -510,15 +443,16 @@ invalid.jsx:44:26 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:44:36 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:27:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 43 │ [].map((item) => { - > 44 │ return item.condition ?
:
foo
; - │ ^^^^^ - 45 │ }); - 46 │ + 25 │ React.Children.map(c => React.cloneElement(c)); + 26 │ + > 27 │ (

{data.map(x =>

{x}

)}) + │ ^^^^ + 28 │ + 29 │ (

{[

,

,

]}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -528,16 +462,16 @@ invalid.jsx:44:36 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:29:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:29:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 27 │ React.Children.map(c => React.cloneElement(c)); + 27 │ (

{data.map(x =>

{x}

)}) 28 │ - > 29 │ (

{data.map(x =>

{x}

)}) - │ ^^^^ + > 29 │ (

{[

,

,

]}) + │ ^^^^ 30 │ - 31 │ (

{[

,

,

]}) + 31 │ (

{[

, xyz, abc?

: bcd]}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -547,16 +481,16 @@ invalid.jsx:29:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:31:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:29:19 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 29 │ (

{data.map(x =>

{x}

)}) + 27 │ (

{data.map(x =>

{x}

)}) + 28 │ + > 29 │ (

{[

,

,

]}) + │ ^^^^ 30 │ - > 31 │ (

{[

,

,

]}) - │ ^^^^ - 32 │ - 33 │ (

{[

, xyz, abc?

: bcd]}) + 31 │ (

{[

, xyz, abc?

: bcd]}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -566,16 +500,16 @@ invalid.jsx:31:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:31:19 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:29:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 29 │ (

{data.map(x =>

{x}

)}) + 27 │ (

{data.map(x =>

{x}

)}) + 28 │ + > 29 │ (

{[

,

,

]}) + │ ^^^^ 30 │ - > 31 │ (

{[

,

,

]}) - │ ^^^^ - 32 │ - 33 │ (

{[

, xyz, abc?

: bcd]}) + 31 │ (

{[

, xyz, abc?

: bcd]}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -585,16 +519,16 @@ invalid.jsx:31:19 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:31:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:31:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 29 │ (

{data.map(x =>

{x}

)}) + 29 │ (

{[

,

,

]}) 30 │ - > 31 │ (

{[

,

,

]}) - │ ^^^^ + > 31 │ (

{[

, xyz, abc?

: bcd]}) + │ ^^^^ 32 │ - 33 │ (

{[

, xyz, abc?

: bcd]}) + 33 │ (

{data.map(c =>

)}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -604,16 +538,16 @@ invalid.jsx:31:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:33:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:31:29 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 31 │ (

{[

,

,

]}) + 29 │ (

{[

,

,

]}) + 30 │ + > 31 │ (

{[

, xyz, abc?

: bcd]}) + │ ^^^^ 32 │ - > 33 │ (

{[

, xyz, abc?

: bcd]}) - │ ^^^^ - 34 │ - 35 │ (

{data.map(c =>

)}) + 33 │ (

{data.map(c =>

)}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -623,16 +557,16 @@ invalid.jsx:33:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:33:29 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:33:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 31 │ (

{[

,

,

]}) + 31 │ (

{[

, xyz, abc?

: bcd]}) 32 │ - > 33 │ (

{[

, xyz, abc?

: bcd]}) - │ ^^^^ + > 33 │ (

{data.map(c =>

)}) + │ ^^^^ 34 │ - 35 │ (

{data.map(c =>

)}) + 35 │ (

{data.map(c => xyz)}

) i The order of the items may change, and having a key can help React identify which item was moved. @@ -642,35 +576,16 @@ invalid.jsx:33:29 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:35:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:37:22 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 33 │ (

{[

, xyz, abc?

: bcd]}) - 34 │ - > 35 │ (

{data.map(c =>

)}) - │ ^^^^ + 35 │ (

{data.map(c => xyz)}

) 36 │ - 37 │ (

{data.map(c => xyz)}

) - - i The order of the items may change, and having a key can help React identify which item was moved. - - i Check the React documentation. - - -``` - -``` -invalid.jsx:39:22 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Missing key property for this element in iterable. - - 37 │ (

{data.map(c => xyz)}

) - 38 │ - > 39 │ (

{data.map(c => (

))}) + > 37 │ (

{data.map(c => (

))}) │ ^^^^ - 40 │ - 41 │ (

{data.map(c => {return (

)})}) + 38 │ + 39 │ (

{data.map(c => {return (

)})}) i The order of the items may change, and having a key can help React identify which item was moved. @@ -680,34 +595,16 @@ invalid.jsx:39:22 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:41:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:39:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 39 │ (

{data.map(c => (

))}) - 40 │ - > 41 │ (

{data.map(c => {return (

)})}) + 37 │ (

{data.map(c => (

))}) + 38 │ + > 39 │ (

{data.map(c => {return (

)})}) │ ^^^^ - 42 │ - 43 │ [].map((item) => { - - i The order of the items may change, and having a key can help React identify which item was moved. - - i Check the React documentation. - - -``` - -``` -invalid.jsx:48:11 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Missing key property for this element in iterable. - - 47 │ [].map((item) => { - > 48 │ return <>
{item}
; - │ ^^^^^^^ - 49 │ }); - 50 │ + 40 │ + 41 │ [].map((item) => { i The order of the items may change, and having a key can help React identify which item was moved. @@ -717,15 +614,16 @@ invalid.jsx:48:11 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:48:18 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:47:14 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 47 │ [].map((item) => { - > 48 │ return <>
{item}
; - │ ^^^^^ + 45 │ [].map((item) => { + 46 │ const x = 5; + > 47 │ const div =
{x}
; + │ ^^^^^ + 48 │ return div; 49 │ }); - 50 │ i The order of the items may change, and having a key can help React identify which item was moved. @@ -735,52 +633,16 @@ invalid.jsx:48:18 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:52:29 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:53:14 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 51 │ [].map((item) => { - > 52 │ return <>{item.condition ?
:
foo
}; - │ ^^^^^^^ - 53 │ }); - 54 │ - - i The order of the items may change, and having a key can help React identify which item was moved. - - i Check the React documentation. - - -``` - -``` -invalid.jsx:52:39 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Missing key property for this element in iterable. - - 51 │ [].map((item) => { - > 52 │ return <>{item.condition ?
:
foo
}; - │ ^^^^^ - 53 │ }); - 54 │ - - i The order of the items may change, and having a key can help React identify which item was moved. - - i Check the React documentation. - - -``` - -``` -invalid.jsx:57:14 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Missing key property for this element in iterable. - - 55 │ [].map((item) => { - 56 │ const x = 5; - > 57 │ const div =
{x}
; + 51 │ [].map(function(item) { + 52 │ const x = 5; + > 53 │ const div =
{x}
; │ ^^^^^ - 58 │ return div; - 59 │ }); + 54 │ return div; + 55 │ }); i The order of the items may change, and having a key can help React identify which item was moved. @@ -790,16 +652,15 @@ invalid.jsx:57:14 lint/correctness/useJsxKeyInIterable ━━━━━━━━ ``` ``` -invalid.jsx:63:14 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:57:20 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Missing key property for this element in iterable. - 61 │ [].map(function(item) { - 62 │ const x = 5; - > 63 │ const div =
{x}
; - │ ^^^^^ - 64 │ return div; - 65 │ }); + 55 │ }); + 56 │ + > 57 │ data.map((item) =>

{item}

) + │ ^^^^^^^^^^^^^^^^ + 58 │ i The order of the items may change, and having a key can help React identify which item was moved. diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.jsx b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.jsx new file mode 100644 index 000000000000..617de4fa6cd5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.jsx @@ -0,0 +1,57 @@ +import React from "react"; + +[, , ]; + +[...[, ], ]; + +[, xyz ? : , ]; + +[<>, <>, <>]; + +data.map(x => {x}); + +data.map(x => <>{x}); + +data.forEach(x => data1.push(<>{x})); + +Array.from([1, 2, 3], (x) => {x}); + +Array.from([1, 2, 3], (x) => { + return {x} +}); + +[React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + +data.map(c => React.createElement("h1")); + +React.Children.map(c => React.cloneElement(c)); + +(

{data.map(x =>

{x}

)}) + +(

{[

,

,

]}) + +(

{[

, xyz, abc?

: bcd]}) + +(

{data.map(c =>

)}) + +(

{data.map(c => xyz)}

) + +(

{data.map(c => (

))}) + +(

{data.map(c => {return (

)})}) + +[].map((item) => { + return item.condition ?
:
foo
; +}); + +[].map((item) => { + return <>
{item}
; +}); + +[].map((item) => { + return <>{item.condition ?
:
foo
}; +}); + +[].map((item) => { + return <>
{item}
; +}); diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.jsx.snap new file mode 100644 index 000000000000..10abf1338b78 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.jsx.snap @@ -0,0 +1,744 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalidShorthand.jsx +--- +# Input +```jsx +import React from "react"; + +[, , ]; + +[...[, ], ]; + +[, xyz ? : , ]; + +[<>, <>, <>]; + +data.map(x => {x}); + +data.map(x => <>{x}); + +data.forEach(x => data1.push(<>{x})); + +Array.from([1, 2, 3], (x) => {x}); + +Array.from([1, 2, 3], (x) => { + return {x} +}); + +[React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + +data.map(c => React.createElement("h1")); + +React.Children.map(c => React.cloneElement(c)); + +(

{data.map(x =>

{x}

)}) + +(

{[

,

,

]}) + +(

{[

, xyz, abc?

: bcd]}) + +(

{data.map(c =>

)}) + +(

{data.map(c => xyz)}

) + +(

{data.map(c => (

))}) + +(

{data.map(c => {return (

)})}) + +[].map((item) => { + return item.condition ?
:
foo
; +}); + +[].map((item) => { + return <>
{item}
; +}); + +[].map((item) => { + return <>{item.condition ?
:
foo
}; +}); + +[].map((item) => { + return <>
{item}
; +}); + +``` + +# Diagnostics +``` +invalidShorthand.jsx:3:2 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 1 │ import React from "react"; + 2 │ + > 3 │ [, , ]; + │ ^^^^^^^^^ + 4 │ + 5 │ [...[, ], ]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:3:13 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 1 │ import React from "react"; + 2 │ + > 3 │ [, , ]; + │ ^^^^^^^^^ + 4 │ + 5 │ [...[, ], ]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:3:24 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 1 │ import React from "react"; + 2 │ + > 3 │ [, , ]; + │ ^^^^^^^^^ + 4 │ + 5 │ [...[, ], ]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:5:29 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 3 │ [, , ]; + 4 │ + > 5 │ [...[, ], ]; + │ ^^^^^^^^^ + 6 │ + 7 │ [, xyz ? : , ]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:5:6 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 3 │ [, , ]; + 4 │ + > 5 │ [...[, ], ]; + │ ^^^^^^^^^ + 6 │ + 7 │ [, xyz ? : , ]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:5:17 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 3 │ [, , ]; + 4 │ + > 5 │ [...[, ], ]; + │ ^^^^^^^^^ + 6 │ + 7 │ [, xyz ? : , ]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:7:2 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 5 │ [...[, ], ]; + 6 │ + > 7 │ [, xyz ? : , ]; + │ ^^^^^^^^^ + 8 │ + 9 │ [<>, <>, <>]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:7:19 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 5 │ [...[, ], ]; + 6 │ + > 7 │ [, xyz ? : , ]; + │ ^^^^^^^^^ + 8 │ + 9 │ [<>, <>, <>]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:7:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 5 │ [...[, ], ]; + 6 │ + > 7 │ [, xyz ? : , ]; + │ ^^^^^^^^^ + 8 │ + 9 │ [<>, <>, <>]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:7:41 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 5 │ [...[, ], ]; + 6 │ + > 7 │ [, xyz ? : , ]; + │ ^^^^^^^^^ + 8 │ + 9 │ [<>, <>, <>]; + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:9:2 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 7 │ [, xyz ? : , ]; + 8 │ + > 9 │ [<>, <>, <>]; + │ ^^^^^ + 10 │ + 11 │ data.map(x => {x}); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:9:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 7 │ [, xyz ? : , ]; + 8 │ + > 9 │ [<>, <>, <>]; + │ ^^^^^ + 10 │ + 11 │ data.map(x => {x}); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:9:16 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 7 │ [, xyz ? : , ]; + 8 │ + > 9 │ [<>, <>, <>]; + │ ^^^^^ + 10 │ + 11 │ data.map(x => {x}); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:11:15 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 9 │ [<>, <>, <>]; + 10 │ + > 11 │ data.map(x => {x}); + │ ^^^^^^^ + 12 │ + 13 │ data.map(x => <>{x}); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:13:15 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 11 │ data.map(x => {x}); + 12 │ + > 13 │ data.map(x => <>{x}); + │ ^^^^^^^^ + 14 │ + 15 │ data.forEach(x => data1.push(<>{x})); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:17:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 15 │ data.forEach(x => data1.push(<>{x})); + 16 │ + > 17 │ Array.from([1, 2, 3], (x) => {x}); + │ ^^^^^^^ + 18 │ + 19 │ Array.from([1, 2, 3], (x) => { + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:20:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 19 │ Array.from([1, 2, 3], (x) => { + > 20 │ return {x} + │ ^^^^^^^ + 21 │ }); + 22 │ + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:23:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 21 │ }); + 22 │ + > 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + │ ^^^^^^ + 24 │ + 25 │ data.map(c => React.createElement("h1")); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:23:48 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 21 │ }); + 22 │ + > 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + │ ^^^^^^ + 24 │ + 25 │ data.map(c => React.createElement("h1")); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:23:75 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 21 │ }); + 22 │ + > 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + │ ^^^^^^ + 24 │ + 25 │ data.map(c => React.createElement("h1")); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:25:34 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 23 │ [React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; + 24 │ + > 25 │ data.map(c => React.createElement("h1")); + │ ^^^^^^ + 26 │ + 27 │ React.Children.map(c => React.cloneElement(c)); + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:27:43 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 25 │ data.map(c => React.createElement("h1")); + 26 │ + > 27 │ React.Children.map(c => React.cloneElement(c)); + │ ^^^ + 28 │ + 29 │ (

{data.map(x =>

{x}

)}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:44:26 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 43 │ [].map((item) => { + > 44 │ return item.condition ?
:
foo
; + │ ^^^^^^^ + 45 │ }); + 46 │ + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:44:36 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 43 │ [].map((item) => { + > 44 │ return item.condition ?
:
foo
; + │ ^^^^^ + 45 │ }); + 46 │ + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:29:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 27 │ React.Children.map(c => React.cloneElement(c)); + 28 │ + > 29 │ (

{data.map(x =>

{x}

)}) + │ ^^^^ + 30 │ + 31 │ (

{[

,

,

]}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:31:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 29 │ (

{data.map(x =>

{x}

)}) + 30 │ + > 31 │ (

{[

,

,

]}) + │ ^^^^ + 32 │ + 33 │ (

{[

, xyz, abc?

: bcd]}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:31:19 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 29 │ (

{data.map(x =>

{x}

)}) + 30 │ + > 31 │ (

{[

,

,

]}) + │ ^^^^ + 32 │ + 33 │ (

{[

, xyz, abc?

: bcd]}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:31:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 29 │ (

{data.map(x =>

{x}

)}) + 30 │ + > 31 │ (

{[

,

,

]}) + │ ^^^^ + 32 │ + 33 │ (

{[

, xyz, abc?

: bcd]}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:33:8 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 31 │ (

{[

,

,

]}) + 32 │ + > 33 │ (

{[

, xyz, abc?

: bcd]}) + │ ^^^^ + 34 │ + 35 │ (

{data.map(c =>

)}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:33:29 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 31 │ (

{[

,

,

]}) + 32 │ + > 33 │ (

{[

, xyz, abc?

: bcd]}) + │ ^^^^ + 34 │ + 35 │ (

{data.map(c =>

)}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:35:21 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 33 │ (

{[

, xyz, abc?

: bcd]}) + 34 │ + > 35 │ (

{data.map(c =>

)}) + │ ^^^^ + 36 │ + 37 │ (

{data.map(c => xyz)}

) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:39:22 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 37 │ (

{data.map(c => xyz)}

) + 38 │ + > 39 │ (

{data.map(c => (

))}) + │ ^^^^ + 40 │ + 41 │ (

{data.map(c => {return (

)})}) + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:41:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 39 │ (

{data.map(c => (

))}) + 40 │ + > 41 │ (

{data.map(c => {return (

)})}) + │ ^^^^ + 42 │ + 43 │ [].map((item) => { + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:48:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 47 │ [].map((item) => { + > 48 │ return <>
{item}
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 49 │ }); + 50 │ + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:52:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 51 │ [].map((item) => { + > 52 │ return <>{item.condition ?
:
foo
}; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 53 │ }); + 54 │ + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` + +``` +invalidShorthand.jsx:56:9 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Missing key property for this element in iterable. + + 55 │ [].map((item) => { + > 56 │ return <>
{item}
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 57 │ }); + 58 │ + + i The order of the items may change, and having a key can help React identify which item was moved. + + i Check the React documentation. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.options.json b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.options.json new file mode 100644 index 000000000000..d70bb2a3b5a2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalidShorthand.options.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "enabled": true, + "rules": { + "correctness": { + "useJsxKeyInIterable": { + "level": "error", + "options": { + "checkShorthandFragments": true + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx index 5d00b470cdb7..4e103a18affc 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx @@ -53,34 +53,10 @@ React.Children.map(c => React.cloneElement(c, {key: c})); <>{data.reduce((a, b) => a > b ? a : b, 0)} -<>{data.map(a => a > 4 ?

{a}

:

{a}

)} - -[].map((item) => { - return <>
{item}
; -}); - -[].map((item) => { - const div =
; - return <>{div}
{item}
; -}); - -[].map((item) => { - return <>
foo
; -}); - [].map((item) => { return item.condition ?
:
foo
; }); -[].map((item) => { - return <>{item.condition ?
:
foo
}; -}); - -[].map((item) => { - const div =
; - return <>{item.condition ? div :
{div}
}; -}); - [].map(function(item) { const x = 5; return
{x}
; @@ -101,3 +77,6 @@ React.Children.map(c => React.cloneElement(c, {key: c})); const content =

Paragraph

return (
{content}
); }); + +// is only checked when options.checkShorthandFragment is set +data.map((x) => <>x) diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx.snap index ceb9f23e5640..34535ed11bf7 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx.snap @@ -59,34 +59,10 @@ React.Children.map(c => React.cloneElement(c, {key: c})); <>{data.reduce((a, b) => a > b ? a : b, 0)} -<>{data.map(a => a > 4 ?

{a}

:

{a}

)} - -[].map((item) => { - return <>
{item}
; -}); - -[].map((item) => { - const div =
; - return <>{div}
{item}
; -}); - -[].map((item) => { - return <>
foo
; -}); - [].map((item) => { return item.condition ?
:
foo
; }); -[].map((item) => { - return <>{item.condition ?
:
foo
}; -}); - -[].map((item) => { - const div =
; - return <>{item.condition ? div :
{div}
}; -}); - [].map(function(item) { const x = 5; return
{x}
; @@ -108,4 +84,7 @@ React.Children.map(c => React.cloneElement(c, {key: c})); return (
{content}
); }); +// is only checked when options.checkShorthandFragment is set +data.map((x) => <>x) + ``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index f328c41be64e..6c4f8d35407b 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1451,7 +1451,7 @@ export interface Correctness { /** * Disallow missing key props in iterators/collection literals. */ - useJsxKeyInIterable?: RuleConfiguration_for_Null; + useJsxKeyInIterable?: RuleConfiguration_for_UseJsxKeyInIterableOptions; /** * Enforce "for" loop update clause moving the counter in the right direction. */ @@ -2362,6 +2362,9 @@ export type RuleConfiguration_for_DeprecatedHooksOptions = export type RuleFixConfiguration_for_UseImportExtensionsOptions = | RulePlainConfiguration | RuleWithFixOptions_for_UseImportExtensionsOptions; +export type RuleConfiguration_for_UseJsxKeyInIterableOptions = + | RulePlainConfiguration + | RuleWithOptions_for_UseJsxKeyInIterableOptions; export type RuleConfiguration_for_NoBitwiseOperatorsOptions = | RulePlainConfiguration | RuleWithOptions_for_NoBitwiseOperatorsOptions; @@ -2576,6 +2579,16 @@ export interface RuleWithFixOptions_for_UseImportExtensionsOptions { */ options: UseImportExtensionsOptions; } +export interface RuleWithOptions_for_UseJsxKeyInIterableOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: UseJsxKeyInIterableOptions; +} export interface RuleWithOptions_for_NoBitwiseOperatorsOptions { /** * The severity of the emitted diagnostics by the rule @@ -2921,6 +2934,12 @@ export interface UseImportExtensionsOptions { */ forceJsExtensions?: boolean; } +export interface UseJsxKeyInIterableOptions { + /** + * Set to `true` to check shorthand fragments (`<>`) + */ + checkShorthandFragments?: boolean; +} /** * Rule's options */ diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 06e43ea16a5e..66dd6152b8e0 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1082,7 +1082,7 @@ "useJsxKeyInIterable": { "description": "Disallow missing key props in iterators/collection literals.", "anyOf": [ - { "$ref": "#/definitions/RuleConfiguration" }, + { "$ref": "#/definitions/UseJsxKeyInIterableConfiguration" }, { "type": "null" } ] }, @@ -4052,6 +4052,21 @@ }, "additionalProperties": false }, + "RuleWithUseJsxKeyInIterableOptions": { + "type": "object", + "required": ["level"], + "properties": { + "level": { + "description": "The severity of the emitted diagnostics by the rule", + "allOf": [{ "$ref": "#/definitions/RulePlainConfiguration" }] + }, + "options": { + "description": "Rule's options", + "allOf": [{ "$ref": "#/definitions/UseJsxKeyInIterableOptions" }] + } + }, + "additionalProperties": false + }, "RuleWithUseSelfClosingElementsOptions": { "type": "object", "required": ["level"], @@ -5357,6 +5372,23 @@ }, "additionalProperties": false }, + "UseJsxKeyInIterableConfiguration": { + "anyOf": [ + { "$ref": "#/definitions/RulePlainConfiguration" }, + { "$ref": "#/definitions/RuleWithUseJsxKeyInIterableOptions" } + ] + }, + "UseJsxKeyInIterableOptions": { + "type": "object", + "properties": { + "checkShorthandFragments": { + "description": "Set to `true` to check shorthand fragments (`<>`)", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, "UseSelfClosingElementsConfiguration": { "anyOf": [ { "$ref": "#/definitions/RulePlainConfiguration" },