Repository: jsx-eslint/eslint-plugin-react (js)

Issue:
react/jsx-key doesn't detect Array.from
[`react/jsx-key`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-key.md) doesn't detect a specific case where an iterable is [mapped using `Array.from`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#parameters):

```
const mySet = new Set()

// ...

// Should warn for missing key property, but doesn't
Array.from(mySet, () => <div />)
```

Pull request description:
[Fix] `jsx-key` not detecting missing keys in `Array.from`'s mapping function
Closes #3368 

Now the `jsx-key` rule will catch these invalid cases:

```
Array.from([1, 2 ,3], function(x) { return <App /> });
Array.from([1, 2 ,3], (x => { return <App /> }));
Array.from([1, 2 ,3], (x => <App />));
```

and will allow these valid cases:

```
Array.from([1, 2, 3], function(x) { return <App key={x} /> });
Array.from([1, 2, 3], (x => <App key={x} />));
Array.from([1, 2, 3], (x => {return <App key={x} />}));
Array.from([1, 2, 3], someFn);
Array.from([1, 2, 3]);
```

In addition, I did some refactoring to keep the code DRY (the checking for keys in `Array.prototype.map` and `Array.from` had many repetitions).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 230b0563..715d1db3 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 a920173a..98a8b6b1 100644
--- a/docs/rules/jsx-key.md
+++ b/docs/rules/jsx-key.md
@@ -21,6 +21,10 @@ data.map(x => <Hello>{x}</Hello>);
 <Hello {...{ key: id, id, caption }} />
 ```
 
+```jsx
+Array.from([1, 2, 3], (x) => <Hello>{x}</Hello>);
+```
+
 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) => <Hello key={x.id}>{x}</Hello>);
 <Hello key={id} {...{ id, caption }} />
 ```
 
+```jsx
+Array.from([1, 2, 3], (x) => <Hello key={x.id}>{x}</Hello>);
+```
+
 ## Rule Options
 
 ```js
diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js
index 385ac6fa..6e38d740 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);
       },
     };
   },


Test patch:
diff --git a/tests/lib/rules/jsx-key.js b/tests/lib/rules/jsx-key.js
index 6c007bb8..386befcc 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 <App key={x} /> });' },
     { code: '[1, 2, 3].map(x => <App key={x} />);' },
     { code: '[1, 2, 3].map(x => { return <App key={x} /> });' },
+    { code: 'Array.from([1, 2, 3], function(x) { return <App key={x} /> });' },
+    { code: 'Array.from([1, 2, 3], (x => <App key={x} />));' },
+    { code: 'Array.from([1, 2, 3], (x => {return <App key={x} />}));' },
+    { code: 'Array.from([1, 2, 3], someFn);' },
+    { code: 'Array.from([1, 2, 3]);' },
     { code: '[1, 2, 3].foo(x => <App />);' },
     { code: 'var App = () => <div />;' },
     { code: '[1, 2, 3].map(function(x) { return; });' },
@@ -174,6 +179,18 @@ ruleTester.run('jsx-key', rule, {
       code: '[1, 2 ,3].map(x => { return <App /> });',
       errors: [{ messageId: 'missingIterKey' }],
     },
+    {
+      code: 'Array.from([1, 2 ,3], function(x) { return <App /> });',
+      errors: [{ messageId: 'missingIterKey' }],
+    },
+    {
+      code: 'Array.from([1, 2 ,3], (x => { return <App /> }));',
+      errors: [{ messageId: 'missingIterKey' }],
+    },
+    {
+      code: 'Array.from([1, 2 ,3], (x => <App />));',
+      errors: [{ messageId: 'missingIterKey' }],
+    },
     {
       code: '[1, 2, 3]?.map(x => <BabelEslintApp />)',
       features: ['no-default'],