diff --git a/crates/oxc_linter/src/rules/react/jsx_key.rs b/crates/oxc_linter/src/rules/react/jsx_key.rs index c63abc780..c746c7521 100644 --- a/crates/oxc_linter/src/rules/react/jsx_key.rs +++ b/crates/oxc_linter/src/rules/react/jsx_key.rs @@ -1,13 +1,16 @@ use oxc_ast::{ - ast::{Expression, JSXAttributeItem, JSXAttributeName, JSXElement, JSXFragment}, + ast::{ + Expression, JSXAttributeItem, JSXAttributeName, JSXElement, JSXFragment, MemberExpression, + }, AstKind, }; use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::Error, }; +use oxc_span::{Atom, GetSpan, Span}; + use oxc_macros::declare_oxc_lint; -use oxc_span::Span; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -18,8 +21,11 @@ enum JsxKeyDiagnostic { MissingKeyPropForElementInArray(#[label] Span), #[error("eslint-plugin-react(jsx-key): Missing \"key\" prop for element in iterator.")] - #[diagnostic(severity(warning))] - MissingKeyPropForElementInIterator(#[label] Span), + #[diagnostic(severity(warning), help("Add a \"key\" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key)."))] + MissingKeyPropForElementInIterator( + #[label("Iterator starts here")] Span, + #[label("Element generated here")] Span, + ), #[error( "eslint-plugin-react(jsx-key): \"key\" prop must be placed before any `{{...spread}}`" @@ -68,7 +74,7 @@ impl Rule for JsxKey { enum InsideArrayOrIterator { Array, - Iterator, + Iterator(Span), } fn is_in_array_or_iter<'a, 'b>( @@ -97,9 +103,9 @@ fn is_in_array_or_iter<'a, 'b>( let callee = &v.callee.without_parenthesized(); if let Expression::MemberExpression(v) = callee { - if let Some(static_property_name) = v.static_property_name() { - if TARGET_METHODS.contains(static_property_name) { - return Some(InsideArrayOrIterator::Iterator); + if let Some((x, span)) = get_member_expression_name_and_span(v.0) { + if TARGET_METHODS.contains(x.as_str()) { + return Some(InsideArrayOrIterator::Iterator(span)); } } } @@ -113,6 +119,28 @@ fn is_in_array_or_iter<'a, 'b>( } } +fn get_member_expression_name_and_span<'a>( + member_expr: &'a MemberExpression<'a>, +) -> Option<(&'a Atom, Span)> { + match member_expr { + MemberExpression::StaticMemberExpression(expr) => { + Some((&expr.property.name, expr.property.span)) + } + MemberExpression::ComputedMemberExpression(expr) => match &expr.expression { + Expression::StringLiteral(lit) => Some((&lit.value, lit.span)), + Expression::TemplateLiteral(lit) => { + if lit.expressions.is_empty() && lit.quasis.len() == 1 { + Some((&lit.quasis[0].value.raw, lit.quasis[0].span)) + } else { + None + } + } + _ => None, + }, + MemberExpression::PrivateFieldExpression(_) => None, + } +} + fn check_jsx_element<'a>(node: &AstNode<'a>, jsx_elem: &JSXElement<'a>, ctx: &LintContext<'a>) { if let Some(outer) = is_in_array_or_iter(node, ctx) { if !jsx_elem.opening_element.attributes.iter().any(|attr| { @@ -123,7 +151,7 @@ fn check_jsx_element<'a>(node: &AstNode<'a>, jsx_elem: &JSXElement<'a>, ctx: &Li }; attr_ident.name == "key" }) { - ctx.diagnostic(gen_diagnostic(jsx_elem.span, &outer)); + ctx.diagnostic(gen_diagnostic(jsx_elem.opening_element.name.span(), &outer)); } } } @@ -156,15 +184,15 @@ fn check_jsx_element_is_key_before_spread<'a>(jsx_elem: &JSXElement<'a>, ctx: &L fn check_jsx_fragment<'a>(node: &AstNode<'a>, fragment: &JSXFragment<'a>, ctx: &LintContext<'a>) { if let Some(outer) = is_in_array_or_iter(node, ctx) { - ctx.diagnostic(gen_diagnostic(fragment.span, &outer)); + ctx.diagnostic(gen_diagnostic(fragment.opening_fragment.span, &outer)); } } fn gen_diagnostic(span: Span, outer: &InsideArrayOrIterator) -> JsxKeyDiagnostic { match outer { InsideArrayOrIterator::Array => JsxKeyDiagnostic::MissingKeyPropForElementInArray(span), - InsideArrayOrIterator::Iterator => { - JsxKeyDiagnostic::MissingKeyPropForElementInIterator(span) + InsideArrayOrIterator::Iterator(v) => { + JsxKeyDiagnostic::MissingKeyPropForElementInIterator(*v, span) } } } @@ -415,6 +443,38 @@ fn test() { "#, None, ), + ( + r#" + const TestCase = () => { + const list = [1, 2, 3, 4, 5]; + + return ( +
+ {list.map(item => onClickHandler()} onPointerDown={() => onPointerDownHandler()} onMouseDown={() => onMouseDownHandler()} />)} +
+ ); + }; + "#, + None, + ), + ( + r#" + const TestCase = () => { + const list = [1, 2, 3, 4, 5]; + + return ( +
+ {list.map(item => (
+ onClickHandler()} onPointerDown={() => onPointerDownHandler()} onMouseDown={() => onMouseDownHandler()} /> +
) + + )} +
+ ); + }; + "#, + None, + ), ]; Tester::new(JsxKey::NAME, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/jsx_key.snap b/crates/oxc_linter/src/snapshots/jsx_key.snap index 3d814a8b8..9bf3a422f 100644 --- a/crates/oxc_linter/src/snapshots/jsx_key.snap +++ b/crates/oxc_linter/src/snapshots/jsx_key.snap @@ -5,109 +5,151 @@ expression: jsx_key ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in array. ╭─[jsx_key.tsx:1:1] 1 │ []; - · ─────── + · ─── ╰──── ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in array. ╭─[jsx_key.tsx:1:1] 1 │ []; - · ──────────────── + · ─── ╰──── ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in array. ╭─[jsx_key.tsx:1:1] 1 │ [, ]; - · ─────── + · ─── ╰──── ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. ╭─[jsx_key.tsx:1:1] 1 │ [1, 2 ,3].map(function(x) { return }); - · ─────── + · ─┬─ ─┬─ + · │ ╰── 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:1] 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:1] 1 │ [1, 2 ,3].map(x => 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:1] 1 │ [1, 2 ,3].map(x => 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:1] 1 │ [1, 2 ,3].map(x => 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:1] 1 │ [1, 2 ,3].map(x => { return }); - · ─────── + · ─┬─ ─┬─ + · │ ╰── 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:1] 1 │ Array.from([1, 2 ,3], function(x) { return }); - · ─────── + · ──┬─ ─┬─ + · │ ╰── 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:1] 1 │ Array.from([1, 2 ,3], (x => { return })); - · ─────── + · ──┬─ ─┬─ + · │ ╰── 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:1] 1 │ Array.from([1, 2 ,3], (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:1] 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:1] 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:1] 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:1] 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:1] 1 │ [1, 2, 3].map(x => <>{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 array. ╭─[jsx_key.tsx:1:1] 1 │ [<>]; - · ───── + · ── ╰──── ⚠ eslint-plugin-react(jsx-key): "key" prop must be placed before any `{...spread}` @@ -125,75 +167,163 @@ expression: jsx_key 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:8:1] + ╭─[jsx_key.tsx:6:1] + 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): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:11:1] - 11 │ - 12 │ return
; - · ─────── - 13 │ })} - ╰──── - - ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in iterator. - ╭─[jsx_key.tsx:8:1] + ╭─[jsx_key.tsx:6:1] + 6 │
+ 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here 8 │ if (item < 2) { 9 │ return
{item}
; - · ───────────────── - 10 │ } else if (item < 5) { + 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:10:1] + ╭─[jsx_key.tsx:6:1] + 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:6:1] + 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:12:1] + ╭─[jsx_key.tsx:6:1] + 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 │ } ╰──── + 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:6:1] + 6 │
+ 7 │ {list.map(item => { + · ─┬─ + · ╰── Iterator starts here + 8 │ if (item < 2) { + ╰──── ╭─[jsx_key.tsx:15:1] 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:1] + ╭─[jsx_key.tsx:6:1] + 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:8:1] + ╭─[jsx_key.tsx:6:1] + 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:9:1] + ╭─[jsx_key.tsx:6:1] + 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 │ })} ╰──── + 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:6:1] + 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:6:1] + 6 │
+ 7 │ {list.map(item => (
+ · ─┬─ ─┬─ + · │ ╰── Element generated here + · ╰── Iterator starts here + 8 │ onClickHandler()} onPointerDown={() => onPointerDownHandler()} onMouseDown={() => onMouseDownHandler()} /> + ╰──── + help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key).