Skip to content

Commit bb4d1b3

Browse files
sjarvaljharb
authored andcommitted
[Fix] jsx-key: detect keys in Array.from's mapping function
1 parent b0d0ca1 commit bb4d1b3

File tree

4 files changed

+76
-15
lines changed

4 files changed

+76
-15
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
55

66
## Unreleased
77

8+
### Fixed
9+
* [`jsx-key`]: fix detecting missing key in `Array.from`'s mapping function ([#3369][] @sjarva)
10+
11+
[#3369]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3369
12+
813
## [7.31.0] - 2022.08.24
914

1015
### Added

docs/rules/jsx-key.md

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ data.map(x => <Hello>{x}</Hello>);
2121
<Hello {...{ key: id, id, caption }} />
2222
```
2323

24+
```jsx
25+
Array.from([1, 2, 3], (x) => <Hello>{x}</Hello>);
26+
```
27+
2428
In the last example the key is being spread, which is currently possible, but discouraged in favor of the statically provided key.
2529

2630
Examples of **correct** code for this rule:
@@ -37,6 +41,10 @@ data.map((x) => <Hello key={x.id}>{x}</Hello>);
3741
<Hello key={id} {...{ id, caption }} />
3842
```
3943

44+
```jsx
45+
Array.from([1, 2, 3], (x) => <Hello key={x.id}>{x}</Hello>);
46+
```
47+
4048
## Rule Options
4149

4250
```js

lib/rules/jsx-key.js

+46-15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const values = require('object.values');
1111
const docsUrl = require('../util/docsUrl');
1212
const pragmaUtil = require('../util/pragma');
1313
const report = require('../util/report');
14+
const astUtil = require('../util/ast');
1415

1516
// ------------------------------------------------------------------------------
1617
// Rule Definition
@@ -124,6 +125,36 @@ module.exports = {
124125
});
125126
}
126127

128+
/**
129+
* Checks if the given node is a function expression or arrow function,
130+
* and checks if there is a missing key prop in return statement's arguments
131+
* @param {ASTNode} node
132+
*/
133+
function checkFunctionsBlockStatement(node) {
134+
if (astUtil.isFunctionLikeExpression(node)) {
135+
if (node.body.type === 'BlockStatement') {
136+
getReturnStatements(node.body)
137+
.filter((returnStatement) => returnStatement && returnStatement.argument)
138+
.forEach((returnStatement) => {
139+
checkIteratorElement(returnStatement.argument);
140+
});
141+
}
142+
}
143+
}
144+
145+
/**
146+
* Checks if the given node is an arrow function that has an JSX Element or JSX Fragment in its body,
147+
* and the JSX is missing a key prop
148+
* @param {ASTNode} node
149+
*/
150+
function checkArrowFunctionWithJSX(node) {
151+
const isArrFn = node && node.type === 'ArrowFunctionExpression';
152+
153+
if (isArrFn && (node.body.type === 'JSXElement' || node.body.type === 'JSXFragment')) {
154+
checkIteratorElement(node.body);
155+
}
156+
}
157+
127158
const seen = new WeakSet();
128159

129160
return {
@@ -196,26 +227,26 @@ module.exports = {
196227
OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
197228
OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) {
198229
const fn = node.arguments[0];
199-
const isFn = fn && fn.type === 'FunctionExpression';
200-
const isArrFn = fn && fn.type === 'ArrowFunctionExpression';
201-
202-
if (!fn && !isFn && !isArrFn) {
230+
if (!astUtil.isFunctionLikeExpression(fn)) {
203231
return;
204232
}
205233

206-
if (isArrFn && (fn.body.type === 'JSXElement' || fn.body.type === 'JSXFragment')) {
207-
checkIteratorElement(fn.body);
208-
}
234+
checkArrowFunctionWithJSX(fn);
209235

210-
if (isFn || isArrFn) {
211-
if (fn.body.type === 'BlockStatement') {
212-
getReturnStatements(fn.body)
213-
.filter((returnStatement) => returnStatement && returnStatement.argument)
214-
.forEach((returnStatement) => {
215-
checkIteratorElement(returnStatement.argument);
216-
});
217-
}
236+
checkFunctionsBlockStatement(fn);
237+
},
238+
239+
// Array.from
240+
'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) {
241+
const fn = node.arguments.length > 1 && node.arguments[1];
242+
243+
if (!astUtil.isFunctionLikeExpression(fn)) {
244+
return;
218245
}
246+
247+
checkArrowFunctionWithJSX(fn);
248+
249+
checkFunctionsBlockStatement(fn);
219250
},
220251
};
221252
},

tests/lib/rules/jsx-key.js

+17
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ ruleTester.run('jsx-key', rule, {
4343
{ code: '[1, 2, 3].map(function(x) { return <App key={x} /> });' },
4444
{ code: '[1, 2, 3].map(x => <App key={x} />);' },
4545
{ code: '[1, 2, 3].map(x => { return <App key={x} /> });' },
46+
{ code: 'Array.from([1, 2, 3], function(x) { return <App key={x} /> });' },
47+
{ code: 'Array.from([1, 2, 3], (x => <App key={x} />));' },
48+
{ code: 'Array.from([1, 2, 3], (x => {return <App key={x} />}));' },
49+
{ code: 'Array.from([1, 2, 3], someFn);' },
50+
{ code: 'Array.from([1, 2, 3]);' },
4651
{ code: '[1, 2, 3].foo(x => <App />);' },
4752
{ code: 'var App = () => <div />;' },
4853
{ code: '[1, 2, 3].map(function(x) { return; });' },
@@ -174,6 +179,18 @@ ruleTester.run('jsx-key', rule, {
174179
code: '[1, 2 ,3].map(x => { return <App /> });',
175180
errors: [{ messageId: 'missingIterKey' }],
176181
},
182+
{
183+
code: 'Array.from([1, 2 ,3], function(x) { return <App /> });',
184+
errors: [{ messageId: 'missingIterKey' }],
185+
},
186+
{
187+
code: 'Array.from([1, 2 ,3], (x => { return <App /> }));',
188+
errors: [{ messageId: 'missingIterKey' }],
189+
},
190+
{
191+
code: 'Array.from([1, 2 ,3], (x => <App />));',
192+
errors: [{ messageId: 'missingIterKey' }],
193+
},
177194
{
178195
code: '[1, 2, 3]?.map(x => <BabelEslintApp />)',
179196
features: ['no-default'],

0 commit comments

Comments
 (0)