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'],