diff --git a/CHANGELOG.md b/CHANGELOG.md
index 230b056302..715d1db33b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
## Unreleased
+### Fixed
+* [`jsx-key`]: fix detecting missing key in `Array.from`'s mapping function ([#3369][] @sjarva)
+
+[#3369]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3369
+
## [7.31.0] - 2022.08.24
### Added
diff --git a/docs/rules/jsx-key.md b/docs/rules/jsx-key.md
index a920173ab5..98a8b6b11a 100644
--- a/docs/rules/jsx-key.md
+++ b/docs/rules/jsx-key.md
@@ -21,6 +21,10 @@ data.map(x => {x});
```
+```jsx
+Array.from([1, 2, 3], (x) => {x});
+```
+
In the last example the key is being spread, which is currently possible, but discouraged in favor of the statically provided key.
Examples of **correct** code for this rule:
@@ -37,6 +41,10 @@ data.map((x) => {x});
```
+```jsx
+Array.from([1, 2, 3], (x) => {x});
+```
+
## Rule Options
```js
diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js
index 385ac6facb..6e38d74002 100644
--- a/lib/rules/jsx-key.js
+++ b/lib/rules/jsx-key.js
@@ -11,6 +11,7 @@ const values = require('object.values');
const docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');
const report = require('../util/report');
+const astUtil = require('../util/ast');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -124,6 +125,36 @@ module.exports = {
});
}
+ /**
+ * Checks if the given node is a function expression or arrow function,
+ * and checks if there is a missing key prop in return statement's arguments
+ * @param {ASTNode} node
+ */
+ function checkFunctionsBlockStatement(node) {
+ if (astUtil.isFunctionLikeExpression(node)) {
+ if (node.body.type === 'BlockStatement') {
+ getReturnStatements(node.body)
+ .filter((returnStatement) => returnStatement && returnStatement.argument)
+ .forEach((returnStatement) => {
+ checkIteratorElement(returnStatement.argument);
+ });
+ }
+ }
+ }
+
+ /**
+ * Checks if the given node is an arrow function that has an JSX Element or JSX Fragment in its body,
+ * and the JSX is missing a key prop
+ * @param {ASTNode} node
+ */
+ function checkArrowFunctionWithJSX(node) {
+ const isArrFn = node && node.type === 'ArrowFunctionExpression';
+
+ if (isArrFn && (node.body.type === 'JSXElement' || node.body.type === 'JSXFragment')) {
+ checkIteratorElement(node.body);
+ }
+ }
+
const seen = new WeakSet();
return {
@@ -196,26 +227,26 @@ module.exports = {
OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) {
const fn = node.arguments[0];
- const isFn = fn && fn.type === 'FunctionExpression';
- const isArrFn = fn && fn.type === 'ArrowFunctionExpression';
-
- if (!fn && !isFn && !isArrFn) {
+ if (!astUtil.isFunctionLikeExpression(fn)) {
return;
}
- if (isArrFn && (fn.body.type === 'JSXElement' || fn.body.type === 'JSXFragment')) {
- checkIteratorElement(fn.body);
- }
+ checkArrowFunctionWithJSX(fn);
- if (isFn || isArrFn) {
- if (fn.body.type === 'BlockStatement') {
- getReturnStatements(fn.body)
- .filter((returnStatement) => returnStatement && returnStatement.argument)
- .forEach((returnStatement) => {
- checkIteratorElement(returnStatement.argument);
- });
- }
+ checkFunctionsBlockStatement(fn);
+ },
+
+ // Array.from
+ 'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) {
+ const fn = node.arguments.length > 1 && node.arguments[1];
+
+ if (!astUtil.isFunctionLikeExpression(fn)) {
+ return;
}
+
+ checkArrowFunctionWithJSX(fn);
+
+ checkFunctionsBlockStatement(fn);
},
};
},
diff --git a/tests/lib/rules/jsx-key.js b/tests/lib/rules/jsx-key.js
index 6c007bb8bc..386befccc7 100644
--- a/tests/lib/rules/jsx-key.js
+++ b/tests/lib/rules/jsx-key.js
@@ -43,6 +43,11 @@ ruleTester.run('jsx-key', rule, {
{ code: '[1, 2, 3].map(function(x) { return });' },
{ code: '[1, 2, 3].map(x => );' },
{ code: '[1, 2, 3].map(x => { return });' },
+ { code: 'Array.from([1, 2, 3], function(x) { return });' },
+ { code: 'Array.from([1, 2, 3], (x => ));' },
+ { code: 'Array.from([1, 2, 3], (x => {return }));' },
+ { code: 'Array.from([1, 2, 3], someFn);' },
+ { code: 'Array.from([1, 2, 3]);' },
{ code: '[1, 2, 3].foo(x => );' },
{ code: 'var App = () =>
;' },
{ code: '[1, 2, 3].map(function(x) { return; });' },
@@ -174,6 +179,18 @@ ruleTester.run('jsx-key', rule, {
code: '[1, 2 ,3].map(x => { return });',
errors: [{ messageId: 'missingIterKey' }],
},
+ {
+ code: 'Array.from([1, 2 ,3], function(x) { return });',
+ errors: [{ messageId: 'missingIterKey' }],
+ },
+ {
+ code: 'Array.from([1, 2 ,3], (x => { return }));',
+ errors: [{ messageId: 'missingIterKey' }],
+ },
+ {
+ code: 'Array.from([1, 2 ,3], (x => ));',
+ errors: [{ messageId: 'missingIterKey' }],
+ },
{
code: '[1, 2, 3]?.map(x => )',
features: ['no-default'],