diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 73de0e8ca3834..cc26afb444cc1 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2222,8 +2222,11 @@ impl RuleRunner for crate::rules::react::jsx_handler_names::JsxHandlerNames { } impl RuleRunner for crate::rules::react::jsx_key::JsxKey { - const NODE_TYPES: Option<&AstTypesBitset> = - Some(&AstTypesBitset::from_types(&[AstType::JSXElement, AstType::JSXFragment])); + const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ + AstType::ArrayExpression, + AstType::JSXElement, + AstType::JSXFragment, + ])); const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } diff --git a/crates/oxc_linter/src/rules/react/jsx_key.rs b/crates/oxc_linter/src/rules/react/jsx_key.rs index a352de31e5995..9cb27632ae5cc 100644 --- a/crates/oxc_linter/src/rules/react/jsx_key.rs +++ b/crates/oxc_linter/src/rules/react/jsx_key.rs @@ -1,9 +1,16 @@ +use std::ops::Deref; + use cow_utils::CowUtils; +use rustc_hash::FxHashSet; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + use oxc_ast::{ AstKind, ast::{ - CallExpression, Expression, JSXAttributeItem, JSXAttributeName, JSXElement, JSXFragment, - Statement, + ArrayExpression, ArrayExpressionElement, CallExpression, Expression, IdentifierReference, + JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXChild, JSXElement, JSXExpression, + JSXFragment, Statement, }, }; use oxc_diagnostics::OxcDiagnostic; @@ -14,7 +21,8 @@ use crate::{ AstNode, ast_util::is_node_within_call_argument, context::{ContextHost, LintContext}, - rule::Rule, + rule::{DefaultRuleConfig, Rule}, + utils::default_true, }; const TARGET_METHODS: [&str; 3] = ["flatMap", "from", "map"]; @@ -38,8 +46,38 @@ fn key_prop_must_be_placed_before_spread(span: Span) -> OxcDiagnostic { .with_label(span) } -#[derive(Debug, Default, Clone)] -pub struct JsxKey; +fn duplicate_key_prop(key_value: &str, span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Duplicate key '{key_value}' found in JSX elements")) + .with_help("Each child in a list should have a unique 'key' prop") + .with_label(span) +} + +#[derive(Debug, Default, Clone, JsonSchema)] +#[schemars(transparent)] +pub struct JsxKey(Box); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", default)] +#[derive(Default)] +pub struct JsxKeyConfig { + /// When true, require key prop to be placed before any spread props + #[serde(default = "default_true")] + pub check_key_must_before_spread: bool, + /// When true, warn on duplicate key values + #[serde(default = "default_true")] + pub warn_on_duplicates: bool, + /// When true, check fragment shorthand `<>` for keys + #[serde(default = "default_true")] + pub check_fragment_shorthand: bool, +} + +impl Deref for JsxKey { + type Target = JsxKeyConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} declare_oxc_lint!( /// ### What it does @@ -65,18 +103,38 @@ declare_oxc_lint!( /// ``` JsxKey, react, - correctness + correctness, + config = JsxKey, ); impl Rule for JsxKey { + fn from_configuration(value: serde_json::Value) -> Self { + let config = serde_json::from_value::>(value) + .unwrap_or_default() + .into_inner(); + Self(Box::new(config)) + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { match node.kind() { AstKind::JSXElement(jsx_elem) => { check_jsx_element(node, jsx_elem, ctx); - check_jsx_element_is_key_before_spread(jsx_elem, ctx); + if self.check_key_must_before_spread { + check_jsx_element_is_key_before_spread(jsx_elem, ctx); + } + if self.warn_on_duplicates { + check_duplicate_keys_in_children(jsx_elem, ctx); + } } AstKind::JSXFragment(jsx_frag) => { - check_jsx_fragment(node, jsx_frag, ctx); + if self.check_fragment_shorthand { + check_jsx_fragment(node, jsx_frag, ctx); + } + } + AstKind::ArrayExpression(array_expr) => { + if self.warn_on_duplicates { + check_duplicate_keys_in_array(array_expr, ctx); + } } _ => {} @@ -119,6 +177,43 @@ pub fn is_import<'a>( import_matcher(ctx, actual_local_name, expected_module_name) } +fn is_children_from_react<'a>(ident: &IdentifierReference<'a>, ctx: &LintContext<'a>) -> bool { + const REACT_MODULE: &str = "react"; + const CHILDREN: &str = "Children"; + + let name = ident.name.as_str(); + + // Check if directly imported: import { Children } from 'react' + if import_matcher(ctx, name, REACT_MODULE) { + return true; + } + + // Check if it's a local variable that might be destructured from React + // e.g., const { Children } = React; or const { Children } = Act; + if name == CHILDREN { + // Get the symbol ID from the reference + if let Some(reference_id) = ident.reference_id.get() { + let reference = ctx.scoping().get_reference(reference_id); + if let Some(symbol_id) = reference.symbol_id() { + // Get the declaration node + let decl_id = ctx.scoping().symbol_declaration(symbol_id); + let decl_node = ctx.nodes().get_node(decl_id); + + // Check if this is a VariableDeclarator with ObjectPattern + if let AstKind::VariableDeclarator(var_decl) = decl_node.kind() { + // Check if init is an identifier imported from React + if let Some(Expression::Identifier(init_ident)) = var_decl.init.as_ref() { + // Check if the init identifier is imported from 'react' module + return import_matcher(ctx, init_ident.name.as_str(), REACT_MODULE); + } + } + } + } + } + + false +} + pub fn is_children<'a, 'b>(call: &'b CallExpression<'a>, ctx: &'b LintContext<'a>) -> bool { const REACT: &str = "React"; const CHILDREN: &str = "Children"; @@ -126,7 +221,7 @@ pub fn is_children<'a, 'b>(call: &'b CallExpression<'a>, ctx: &'b LintContext<'a let Some(member) = call.callee.as_member_expression() else { return false }; if let Expression::Identifier(ident) = member.object() { - return is_import(ctx, ident.name.as_str(), CHILDREN, REACT); + return is_children_from_react(ident, ctx); } let Some(inner_member) = member.object().get_inner_expression().as_member_expression() else { @@ -300,326 +395,353 @@ fn gen_diagnostic(span: Span, outer: &InsideArrayOrIterator) -> OxcDiagnostic { } } -#[test] -fn test() { - use crate::tester::Tester; - - let pass = vec![ - r"fn()", - r"[1, 2, 3].map(function () {})", - r";", - r"[, ];", - r"[1, 2, 3].map(function(x) { return });", - r"[1, 2, 3].map(x => );", - r"[1, 2 ,3].map(x => x && );", - r#"[1, 2 ,3].map(x => x ? : );"#, - r"[1, 2, 3].map(x => { return });", - r"Array.from([1, 2, 3], function(x) { return });", - r"Array.from([1, 2, 3], (x => ));", - r"Array.from([1, 2, 3], (x => {return }));", - r"Array.from([1, 2, 3], someFn);", - r"Array.from([1, 2, 3]);", - r"[1, 2, 3].foo(x => );", - r"var App = () =>
;", - r"[1, 2, 3].map(function(x) { return; });", - r"foo(() =>
);", - r"foo(() => <>);", - r"<>;", - r";", - r#";"#, - r#"
;"#, - r#"const spans = [,];"#, - r#" - function Component(props) { - return hasPayment ? ( -
- - {props.modal && props.calculatedPrice && ( - - )} -
- ) : null; +fn get_jsx_element_key_value(jsx_elem: &JSXElement) -> Option<(String, Span)> { + for attr in &jsx_elem.opening_element.attributes { + if let JSXAttributeItem::Attribute(attr) = attr + && let JSXAttributeName::Identifier(ident) = &attr.name + && ident.name == "key" + { + // Extract the key value + if let Some(value) = &attr.value { + match value { + JSXAttributeValue::StringLiteral(lit) => { + return Some((lit.value.to_string(), attr.span)); + } + JSXAttributeValue::ExpressionContainer(container) => { + // JSXExpression inherits from Expression, so we match the Expression variants directly + match &container.expression { + JSXExpression::StringLiteral(lit) => { + return Some((lit.value.to_string(), attr.span)); + } + JSXExpression::NumericLiteral(lit) => { + return Some((lit.value.to_string(), attr.span)); + } + JSXExpression::TemplateLiteral(lit) + if lit.expressions.is_empty() && lit.quasis.len() == 1 => + { + return Some((lit.quasis[0].value.raw.to_string(), attr.span)); + } + _ => {} + } + } + _ => {} + } } - "#, - r#" - import React, { FC, useRef, useState } from 'react'; + } + } + None +} - import './ResourceVideo.sass'; - import VimeoVideoPlayInModal from '../vimeoVideoPlayInModal/VimeoVideoPlayInModal'; +fn check_duplicate_keys_in_array<'a>(array_expr: &ArrayExpression<'a>, ctx: &LintContext<'a>) { + let mut seen_keys: FxHashSet = FxHashSet::default(); - type Props = { - videoUrl: string; - videoTitle: string; - }; + for element in &array_expr.elements { + // ArrayExpressionElement also inherits from Expression + if let ArrayExpressionElement::JSXElement(jsx_elem) = element + && let Some((key_value, span)) = get_jsx_element_key_value(jsx_elem) + && !seen_keys.insert(key_value.clone()) + { + ctx.diagnostic(duplicate_key_prop(&key_value, span)); + } + } +} - const ResourceVideo: FC = ({ - videoUrl, - videoTitle, - }: Props): JSX.Element => { - return ( -
- -

{videoTitle}

-
- ); - }; +fn check_duplicate_keys_in_children<'a>(jsx_elem: &JSXElement<'a>, ctx: &LintContext<'a>) { + let mut seen_keys: FxHashSet = FxHashSet::default(); - export default ResourceVideo; - "#, - r" - // testrule.jsx - const trackLink = () => {}; - const getAnalyticsUiElement = () => {}; - - const onTextButtonClick = (e, item) => trackLink([, getAnalyticsUiElement(item), item.name], e); - ", - r#" - function Component({ allRatings }) { - return ( - - {Object.entries(allRatings)?.map(([key, value], index) => { - const rate = value?.split(/(?=[%, /])/); - - if (!rate) return null; - - return ( -
  • - - {rate?.[0]} - {rate?.[1]} -
  • - ); - })} -
    - ); - } - "#, - r" - const baz = foo?.bar?.()?.[1] ?? 'qux'; - - qux()?.map() - - const directiveRanges = comments?.map(tryParseTSDirective) - ", - r#" - const foo: (JSX.Element | string)[] = [ - "text", - hello worldsuperscript, - ]; - "#, - r#" - import { observable } from "mobx"; - - export interface ClusterFrameInfo { - frameId: number; - processId: number; - } + for child in &jsx_elem.children { + if let JSXChild::Element(child_elem) = child + && let Some((key_value, span)) = get_jsx_element_key_value(child_elem) + && !seen_keys.insert(key_value.clone()) + { + ctx.diagnostic(duplicate_key_prop(&key_value, span)); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; - export const clusterFrameMap = observable.map(); - "#, - r#" - const columns: ColumnDef[] = [{ - accessorKey: 'lastName', - header: ({ column }) => , - cell: ({ row }) =>
    {row.getValue('lastName')}
    , - enableSorting: true, - enableHiding: false, - }] - "#, - r#" - const columns: ColumnDef[] = [{ - accessorKey: 'lastName', - header: function ({ column }) { return }, - cell: ({ row }) =>
    {row.getValue('lastName')}
    , - enableSorting: true, - enableHiding: false, - }] - "#, - r#" - const router = createBrowserRouter([ - { - path: "/", - element: , - children: [ - { - path: "team", - element: , - }, - ], - }, - ]); - "#, - r#" - function App() { - return ( -
    - {[1, 2, 3, 4, 5].map((val) => { - const text = () => {val}; - return null - })} -
    - ); - }"#, - r#" - function App() { - return ( -
    - {[1, 2, 3, 4, 5].map((val) => { - const text = {val}; - return ; - })} -
    - ); - }"#, - r" - MyStory.decorators = [ - (Component) =>
    - ]; - ", - r" - MyStory.decorators = [ - (Component) => { - const store = useMyStore(); - return ; - } - ]; - ", - r"{React.Children.toArray(items.map((item) => { - return ( -
    - {item} -
    - );}))} - ", - r#"import { Children } from "react"; - Children.toArray([1, 2 ,3].map(x => )); - "#, - r#"import React from "react"; - React.Children.toArray([1, 2 ,3].map(x => )); - "#, - r"React.Children.toArray([1, 2 ,3].map(x => ));", - r"{React.Children.toArray(items.map((item) => { - return ( - <> - {item} - - ); - }))} - ", - r"const DummyComponent: FC<{ children: ReactNode }> = ({ children }) => { const wrappedChildren = Children.map(children, (child) => { return
    {child}
    ; }); return
    {wrappedChildren}
    ; };", + let pass = vec![ + ("fn()", None, None), + (r"[1, 2, 3].map(function () {})", None, None), + (r";", None, None), + (r"[, ];", None, None), + (r"[1, 2, 3].map(function(x) { return });", None, None), + (r"[1, 2, 3].map(x => );", None, None), + (r"[1, 2 ,3].map(x => x && );", None, None), + (r#"[1, 2 ,3].map(x => x ? : );"#, None, None), + (r"[1, 2, 3].map(x => { return });", None, None), + (r"Array.from([1, 2, 3], function(x) { return });", None, None), + (r"Array.from([1, 2, 3], (x => ));", None, None), + (r"Array.from([1, 2, 3], (x => {return }));", None, None), + (r"Array.from([1, 2, 3], someFn);", None, None), + (r"Array.from([1, 2, 3]);", None, None), + (r"[1, 2, 3].foo(x => );", None, None), + (r"var App = () =>
    ;", None, None), + (r"[1, 2, 3].map(function(x) { return; });", None, None), + (r"foo(() =>
    );", None, None), + (r"foo(() => <>);", None, None), + (r"<>;", None, None), + (r";", None, None), +(r#";"#, Some(serde_json::json!([{ "checkKeyMustBeforeSpread": true }])), None), +(r#"
    ;"#, Some(serde_json::json!([{ "checkKeyMustBeforeSpread": true }])), None), +(r#" + const spans = [ + , + , + ]; + "#, None, None), +(r#" + function Component(props) { + return hasPayment ? ( +
    + + {props.modal && props.calculatedPrice && ( + + )} +
    + ) : null; + } + "#, None, None), +(r#" + import React, { FC, useRef, useState } from 'react'; + + import './ResourceVideo.sass'; + import VimeoVideoPlayInModal from '../vimeoVideoPlayInModal/VimeoVideoPlayInModal'; + + type Props = { + videoUrl: string; + videoTitle: string; + }; + const ResourceVideo: FC = ({ + videoUrl, + videoTitle, + }: Props): JSX.Element => { + return ( +
    + +

    {videoTitle}

    +
    + ); + }; + + export default ResourceVideo; + "#, None, None), +(" + // testrule.jsx + const trackLink = () => {}; + const getAnalyticsUiElement = () => {}; + + const onTextButtonClick = (e, item) => trackLink([, getAnalyticsUiElement(item), item.name], e); + ", None, None), +(r#" + function Component({ allRatings }) { + return ( + + {Object.entries(allRatings)?.map(([key, value], index) => { + const rate = value?.split(/(?=[%, /])/); + + if (!rate) return null; + + return ( +
  • + + {rate?.[0]} + {rate?.[1]} +
  • + ); + })} +
    + ); + } + "#, None, None), +(" + const baz = foo?.bar?.()?.[1] ?? 'qux'; + + qux()?.map() + + const directiveRanges = comments?.map(tryParseTSDirective) + ", None, None), +(r#" + import { observable } from "mobx"; + + export interface ClusterFrameInfo { + frameId: number; + processId: number; + } + + export const clusterFrameMap = observable.map(); + "#, None, None), +("React.Children.toArray([1, 2 ,3].map(x => ));", None, None), +(r#" + import { Children } from "react"; + Children.toArray([1, 2 ,3].map(x => )); + "#, None, None), +(" + import Act from 'react'; + import { Children as ReactChildren } from 'react'; + + const { Children } = Act; + const { toArray } = Children; + + Act.Children.toArray([1, 2 ,3].map(x => )); + Act.Children.toArray(Array.from([1, 2 ,3], x => )); + Children.toArray([1, 2 ,3].map(x => )); + Children.toArray(Array.from([1, 2 ,3], x => )); + // ReactChildren.toArray([1, 2 ,3].map(x => )); + // ReactChildren.toArray(Array.from([1, 2 ,3], x => )); + // toArray([1, 2 ,3].map(x => )); + // toArray(Array.from([1, 2 ,3], x => )); + ", None, Some(serde_json::json!({ "settings": { "react": { "pragma": "Act", "fragment": "Frag" } }}))) ]; let fail = vec![ - r"[];", - r"[];", - r"[, ];", - r"[1, 2 ,3].map(function(x) { return });", - r"[1, 2 ,3].map(x => );", - r"[1, 2 ,3].map(x => x && );", - r#"[1, 2 ,3].map(x => x ? : );"#, - r#"[1, 2 ,3].map(x => x ? : );"#, - r"[1, 2 ,3].map(x => { return });", - r"Array.from([1, 2 ,3], function(x) { return });", - r"Array.from([1, 2 ,3], (x => { return }));", - r"Array.from([1, 2 ,3], (x => ));", - r"[1, 2, 3]?.map(x => )", - r"[1, 2, 3]?.map(x => )", - r"[1, 2, 3]?.map(x => <>)", - "[1, 2, 3].map(x => <>{x});", - "[<>];", - r#"[];"#, - r#"[
    ];"#, - r" - const Test = () => { - const list = [1, 2, 3, 4, 5]; - - return ( -
    - {list.map(item => { - if (item < 2) { - return
    {item}
    ; - } - - return
    ; - })} -
    - ); - }; - ", - r" - const TestO = () => { - const list = [1, 2, 3, 4, 5]; - - return ( -
    - {list.map(item => { - if (item < 2) { - return
    {item}
    ; - } else if (item < 5) { - return
    - } else { - return
    - } - - return
    ; - })} -
    - ); - }; - ", - r" - const TestCase = () => { - const list = [1, 2, 3, 4, 5]; - - return ( -
    - {list.map(item => { - if (item < 2) return
    {item}
    ; - else if (item < 5) return
    ; - else return
    ; - })} -
    - ); - }; - ", - r" - const TestCase = () => { - const list = [1, 2, 3, 4, 5]; - - return ( -
    - {list.map(item => onClickHandler()} onPointerDown={() => onPointerDownHandler()} onMouseDown={() => onMouseDownHandler()} />)} -
    - ); - }; - ", - r" - const TestCase = () => { - const list = [1, 2, 3, 4, 5]; - - return ( -
    - {list.map(item => (
    - onClickHandler()} onPointerDown={() => onPointerDownHandler()} onMouseDown={() => onMouseDownHandler()} /> -
    ) - )} -
    - ); - }; - ", - r"foo.Children.toArray([1, 2 ,3].map(x => ));", - r" - import Act from 'react'; - import { Children as ReactChildren } from 'react'; - - const { Children } = Act; - const { toArray } = Children; - - Act.Children.toArray([1, 2 ,3].map(x => )); - Act.Children.toArray(Array.from([1, 2 ,3], x => )); - Children.toArray([1, 2 ,3].map(x => )); - Children.toArray(Array.from([1, 2 ,3], x => )); - ", + ("[];", None, None), + ("[];", None, None), + ("[, ];", None, None), + ("[1, 2 ,3].map(function(x) { return });", None, None), + ("[1, 2 ,3].map(x => );", None, None), + ("[1, 2 ,3].map(x => x && );", None, None), + (r#"[1, 2 ,3].map(x => x ? : );"#, None, None), + (r#"[1, 2 ,3].map(x => x ? : );"#, None, None), + ("[1, 2 ,3].map(x => { return });", None, None), + ("Array.from([1, 2 ,3], function(x) { return });", None, None), + ("Array.from([1, 2 ,3], (x => { return }));", None, None), + ("Array.from([1, 2 ,3], (x => ));", None, None), + ("[1, 2, 3]?.map(x => )", None, None), + ("[1, 2, 3]?.map(x => )", None, None), + ( + "[1, 2, 3].map(x => <>{x});", + Some(serde_json::json!([{ "checkFragmentShorthand": true }])), + Some( + serde_json::json!({ "settings": { "react": { "pragma": "Act", "fragment": "Frag" } }}), + ), + ), + ( + "[<>];", + Some(serde_json::json!([{ "checkFragmentShorthand": true }])), + Some( + serde_json::json!({ "settings": { "react": { "pragma": "Act", "fragment": "Frag" } }}), + ), + ), + ( + r#"[];"#, + Some(serde_json::json!([{ "checkKeyMustBeforeSpread": true }])), + Some( + serde_json::json!({ "settings": { "react": { "pragma": "Act", "fragment": "Frag" } }}), + ), + ), + ( + r#"[
    ];"#, + Some(serde_json::json!([{ "checkKeyMustBeforeSpread": true }])), + Some( + serde_json::json!({ "settings": { "react": { "pragma": "Act", "fragment": "Frag" } }}), + ), + ), + ( + r#" + const spans = [ + , + , + ]; + "#, + Some(serde_json::json!([{ "warnOnDuplicates": true }])), + None, + ), + ( + r#" + const div = ( +
    + + +
    + ); + "#, + Some(serde_json::json!([{ "warnOnDuplicates": true }])), + None, + ), + ( + " + const Test = () => { + const list = [1, 2, 3, 4, 5]; + + return ( +
    + {list.map(item => { + if (item < 2) { + return
    {item}
    ; + } + + return
    ; + })} +
    + ); + }; + ", + None, + None, + ), + ( + " + const TestO = () => { + const list = [1, 2, 3, 4, 5]; + + return ( +
    + {list.map(item => { + if (item < 2) { + return
    {item}
    ; + } else if (item < 5) { + return
    + } else { + return
    + } + + return
    ; + })} +
    + ); + }; + ", + None, + None, + ), + ( + " + const TestCase = () => { + const list = [1, 2, 3, 4, 5]; + + return ( +
    + {list.map(item => { + if (item < 2) return
    {item}
    ; + else if (item < 5) return
    ; + else return
    ; + })} +
    + ); + }; + ", + None, + None, + ), + ( + " + const TestCase = () => { + const list = [1, 2, 3, 4, 5]; + + return ( +
    + {list.map(x =>
    )} +
    + ); + }; + ", + Some(serde_json::json!([{ "checkKeyMustBeforeSpread": true }])), + None, + ), ]; Tester::new(JsxKey::NAME, JsxKey::PLUGIN, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/react_jsx_key.snap b/crates/oxc_linter/src/snapshots/react_jsx_key.snap index 62f40cb899dca..6b67fe5dbe4e9 100644 --- a/crates/oxc_linter/src/snapshots/react_jsx_key.snap +++ b/crates/oxc_linter/src/snapshots/react_jsx_key.snap @@ -118,15 +118,6 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:1:12] - 1 │ [1, 2, 3]?.map(x => <>) - · ─┬─ ─┬ - · │ ╰── Element generated here. - · ╰── Iterator starts here. - ╰──── - help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. ╭─[jsx_key.tsx:1:11] 1 │ [1, 2, 3].map(x => <>{x}); @@ -156,193 +147,167 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: To avoid conflicting with React's new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) { - 9 │ return
    {item}
    ; - · ─┬─ - · ╰── Element generated here. - 10 │ } - ╰──── - help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). + ⚠ eslint-plugin-react(jsx-key): Duplicate key 'notunique' found in JSX elements + ╭─[jsx_key.tsx:4:20] + 3 │ , + 4 │ , + · ─────────────── + 5 │ ]; + ╰──── + help: Each child in a list should have a unique 'key' prop + + ⚠ eslint-plugin-react(jsx-key): Duplicate key 'notunique' found in JSX elements + ╭─[jsx_key.tsx:5:22] + 4 │ + 5 │ + · ─────────────── + 6 │
    + ╰──── + help: Each child in a list should have a unique 'key' prop ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) { - 9 │ return
    {item}
    ; - 10 │ } - 11 │ - 12 │ return
    ; + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { · ─┬─ - · ╰── Element generated here. - 13 │ })} + · ╰── Iterator starts here. + 8 │ if (item < 2) { + 9 │ return
    {item}
    ; + · ─┬─ + · ╰── Element generated here. + 10 │ } ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) { - 9 │ return
    {item}
    ; - · ─┬─ - · ╰── Element generated here. - 10 │ } else if (item < 5) { + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here. + 8 │ if (item < 2) { + 9 │ return
    {item}
    ; + 10 │ } + 11 │ + 12 │ return
    ; + · ─┬─ + · ╰── Element generated here. + 13 │ })} ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) { - 9 │ return
    {item}
    ; - 10 │ } else if (item < 5) { - 11 │ return
    - · ─┬─ - · ╰── Element generated here. - 12 │ } else { + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here. + 8 │ if (item < 2) { + 9 │ return
    {item}
    ; + · ─┬─ + · ╰── Element generated here. + 10 │ } else if (item < 5) { ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) { - 9 │ return
    {item}
    ; - 10 │ } else if (item < 5) { - 11 │ return
    - 12 │ } else { - 13 │ return
    - · ─┬─ - · ╰── Element generated here. - 14 │ } + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here. + 8 │ if (item < 2) { + 9 │ return
    {item}
    ; + 10 │ } else if (item < 5) { + 11 │ return
    + · ─┬─ + · ╰── Element generated here. + 12 │ } else { ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) { - ╰──── - ╭─[jsx_key.tsx:16:33] - 15 │ - 16 │ return
    ; + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { · ─┬─ - · ╰── Element generated here. - 17 │ })} + · ╰── Iterator starts here. + 8 │ if (item < 2) { + 9 │ return
    {item}
    ; + 10 │ } else if (item < 5) { + 11 │ return
    + 12 │ } else { + 13 │ return
    + · ─┬─ + · ╰── Element generated here. + 14 │ } ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) return
    {item}
    ; - · ─┬─ - · ╰── Element generated here. - 9 │ else if (item < 5) return
    ; + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here. + 8 │ if (item < 2) { ╰──── - help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). - - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) return
    {item}
    ; - 9 │ else if (item < 5) return
    ; - · ─┬─ - · ╰── Element generated here. - 10 │ else return
    ; - ╰──── - help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). - - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => { - · ─┬─ - · ╰── Iterator starts here. - 8 │ if (item < 2) return
    {item}
    ; - 9 │ else if (item < 5) return
    ; - 10 │ else return
    ; - · ─┬─ - · ╰── Element generated here. - 11 │ })} + ╭─[jsx_key.tsx:16:28] + 15 │ + 16 │ return
    ; + · ─┬─ + · ╰── Element generated here. + 17 │ })} ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => onClickHandler()} onPointerDown={() => onPointerDownHandler()} onMouseDown={() => onMouseDownHandler()} />)} - · ─┬─ ──┬─ - · │ ╰── Element generated here. - · ╰── Iterator starts here. - 8 │
    - ╰──── - help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). - - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:7:29] - 6 │
    - 7 │ {list.map(item => (
    - · ─┬─ ─┬─ - · │ ╰── Element generated here. - · ╰── Iterator starts here. - 8 │ onClickHandler()} onPointerDown={() => onPointerDownHandler()} onMouseDown={() => onMouseDownHandler()} /> + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here. + 8 │ if (item < 2) return
    {item}
    ; + · ─┬─ + · ╰── Element generated here. + 9 │ else if (item < 5) return
    ; ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:1:32] - 1 │ foo.Children.toArray([1, 2 ,3].map(x => )); - · ─┬─ ─┬─ - · │ ╰── Element generated here. - · ╰── Iterator starts here. - ╰──── - help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). - - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:10:36] - 9 │ Act.Children.toArray(Array.from([1, 2 ,3], x => )); - 10 │ Children.toArray([1, 2 ,3].map(x => )); - · ─┬─ ─┬─ - · │ ╰── Element generated here. - · ╰── Iterator starts here. - 11 │ Children.toArray(Array.from([1, 2 ,3], x => )); + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here. + 8 │ if (item < 2) return
    {item}
    ; + 9 │ else if (item < 5) return
    ; + · ─┬─ + · ╰── Element generated here. + 10 │ else return
    ; ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:11:32] - 10 │ Children.toArray([1, 2 ,3].map(x => )); - 11 │ Children.toArray(Array.from([1, 2 ,3], x => )); - · ──┬─ ─┬─ - · │ ╰── Element generated here. + ╭─[jsx_key.tsx:7:24] + 6 │
    + 7 │ {list.map(item => { + · ─┬─ · ╰── Iterator starts here. - 12 │ + 8 │ if (item < 2) return
    {item}
    ; + 9 │ else if (item < 5) return
    ; + 10 │ else return
    ; + · ─┬─ + · ╰── Element generated here. + 11 │ })} ╰──── help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key). + + ⚠ eslint-plugin-react(jsx-key): "key" prop must be placed before any `{...spread}` + ╭─[jsx_key.tsx:7:50] + 6 │
    + 7 │ {list.map(x =>
    )} + · ─── + 8 │
    + ╰──── + help: To avoid conflicting with React's new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html