diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca44284b72..6fa165d487 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,12 +12,16 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* [`jsx-no-literals`]: properly error on children with noAttributeStrings: true ([#3317][] @TildaDares)
* [`jsx-key`]: catch key errors inside conditional statements ([#3320][] @TildaDares)
* [`display-name`]: Accept forwardRef and Memo nesting in newer React versions ([#3321][] @TildaDares)
+* [`jsx-key`]: avoid a crash from optional chaining from [#3320][] ([#3327][] @ljharb)
+* [`jsx-key`]: avoid a crash on a non-array node.body from [#3320][] ([#3328][] @ljharb)
### Changed
* [Refactor] [`jsx-indent-props`]: improved readability of the checkNodesIndent function ([#3315][] @caroline223)
* [Tests] [`jsx-indent`], [`jsx-one-expression-per-line`]: add passing test cases ([#3314][] @ROSSROSALES)
* [Refactor] `boolean-prop-naming`, `jsx-indent`: avoid assigning to arguments ([#3316][] @caroline223)
+[#3328]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3328
+[#3327]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3327
[#3321]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3321
[#3320]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3320
[#3317]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3317
diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js
index 2b61cc20a1..e3731945a8 100644
--- a/lib/rules/jsx-key.js
+++ b/lib/rules/jsx-key.js
@@ -95,7 +95,7 @@ module.exports = {
if (node.alternate) {
getReturnStatements(node.alternate, returnStatements);
}
- } else {
+ } else if (Array.isArray(node.body)) {
node.body.forEach((item) => {
if (item.type === 'IfStatement') {
getReturnStatements(item, returnStatements);
diff --git a/tests/helpers/parsers.js b/tests/helpers/parsers.js
index 12a4df78b0..b0e0a7af9c 100644
--- a/tests/helpers/parsers.js
+++ b/tests/helpers/parsers.js
@@ -57,7 +57,8 @@ const parsers = {
}
const features = new Set([].concat(test.features || []));
delete test.features;
- const es = test.parserOptions && test.parserOptions.ecmaVersion;
+
+ const es = features.has('class fields') ? 2022 : (features.has('optional chaining') ? 2020 : (test.parserOptions && test.parserOptions.ecmaVersion) || undefined); // eslint-disable-line no-nested-ternary
function addComment(testObject, parser) {
const extras = [].concat(
@@ -132,9 +133,9 @@ const parsers = {
return [].concat(
skipBase ? [] : addComment(
- Object.assign({}, test, features.has('class fields') && {
+ Object.assign({}, test, typeof es !== 'undefined' && {
parserOptions: Object.assign({}, test.parserOptions, {
- ecmaVersion: Math.max((test.parserOptions && test.parserOptions.ecmaVersion) || 0, 2022),
+ ecmaVersion: Math.max((test.parserOptions && test.parserOptions.ecmaVersion) || 0, es),
}),
}),
'default'
diff --git a/tests/lib/rules/jsx-key.js b/tests/lib/rules/jsx-key.js
index 4dae8973f3..6c007bb8bc 100644
--- a/tests/lib/rules/jsx-key.js
+++ b/tests/lib/rules/jsx-key.js
@@ -124,6 +124,30 @@ ruleTester.run('jsx-key', rule, {
const onTextButtonClick = (e, item) => trackLink([, getAnalyticsUiElement(item), item.name], e);
`,
},
+ {
+ code: `
+ function Component({ allRatings }) {
+ return (
+
+ {Object.entries(allRatings)?.map(([key, value], index) => {
+ const rate = value?.split(/(?=[%, /])/);
+
+ if (!rate) return null;
+
+ return (
+
+
+ {rate?.[0]}
+ {rate?.[1]}
+
+ );
+ })}
+
+ );
+ }
+ `,
+ features: ['optional chaining'],
+ },
]),
invalid: parsers.all([
{
diff --git a/tests/lib/rules/jsx-no-constructed-context-values.js b/tests/lib/rules/jsx-no-constructed-context-values.js
index 7a59da820c..1ec0592382 100644
--- a/tests/lib/rules/jsx-no-constructed-context-values.js
+++ b/tests/lib/rules/jsx-no-constructed-context-values.js
@@ -63,9 +63,6 @@ ruleTester.run('react-no-constructed-context-values', rule, {
}
`,
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
},
{
code: `
diff --git a/tests/lib/rules/no-array-index-key.js b/tests/lib/rules/no-array-index-key.js
index 1faa12b1eb..e765d001ab 100644
--- a/tests/lib/rules/no-array-index-key.js
+++ b/tests/lib/rules/no-array-index-key.js
@@ -136,9 +136,6 @@ ruleTester.run('no-array-index-key', rule, {
{
code: 'foo?.map(child => )',
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
}
),
@@ -349,9 +346,6 @@ ruleTester.run('no-array-index-key', rule, {
code: 'foo?.map((child, i) => )',
errors: [{ messageId: 'noArrayIndex' }],
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
},
{
code: `
diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js
index e70717427a..80f6405d32 100644
--- a/tests/lib/rules/no-unused-prop-types.js
+++ b/tests/lib/rules/no-unused-prop-types.js
@@ -807,9 +807,6 @@ ruleTester.run('no-unused-prop-types', rule, {
};
`,
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
},
{
code: `
@@ -839,9 +836,6 @@ ruleTester.run('no-unused-prop-types', rule, {
module.exports = HelloComponent();
`,
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
},
{
code: `
@@ -871,9 +865,6 @@ ruleTester.run('no-unused-prop-types', rule, {
module.exports = HelloComponent();
`,
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
},
{
code: `
@@ -926,9 +917,6 @@ ruleTester.run('no-unused-prop-types', rule, {
};
`,
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
},
{
code: `
diff --git a/tests/lib/rules/no-unused-state.js b/tests/lib/rules/no-unused-state.js
index 380296ec4e..c37145a047 100644
--- a/tests/lib/rules/no-unused-state.js
+++ b/tests/lib/rules/no-unused-state.js
@@ -353,9 +353,6 @@ eslintTester.run('no-unused-state', rule, {
}
`,
features: ['optional chaining'],
- parserOptions: {
- ecmaVersion: 2020,
- },
},
{
code: `