diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9e559291..f5975115a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,19 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Fixed * [`no-unknown-property`]: add `dialog` attributes ([#3436][] @ljharb) * [`no-arrow-function-lifecycle`]: when converting from an arrow, remove the semi and wrapping parens ([#3337][] @ljharb) +* [`jsx-key`]: Ignore elements inside `React.Children.toArray()` ([#1591][] @silvenon) +* [`jsx-no-constructed-context-values`]: fix false positive for usage in non-components ([#3448][] @golopot) +### Changed +* [Docs] [`no-unknown-property`]: fix typo in link ([#3445][] @denkristoffer) +* [Perf] component detection: improve performance by optimizing getId ([#3451][] @golopot) + +[#3451]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3451 +[#3448]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3448 +[#3445]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3445 [#3436]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3436 [#3337]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3337 +[#1591]: https://github.com/jsx-eslint/eslint-plugin-react/pull/1591 ## [7.31.8] - 2022.09.08 diff --git a/docs/rules/no-unknown-property.md b/docs/rules/no-unknown-property.md index d142def850..e885474f20 100644 --- a/docs/rules/no-unknown-property.md +++ b/docs/rules/no-unknown-property.md @@ -58,7 +58,7 @@ var AtomPanel = ; If you are using a library that passes something as a prop to JSX elements, it is recommended to add those props to the ignored properties. -For example, if you use [emotion](https://emotion.sh/docs/introduction) and its [`css` prop](https://emotion.sh/docs/css-prop)), +For example, if you use [emotion](https://emotion.sh/docs/introduction) and its [`css` prop](https://emotion.sh/docs/css-prop), add the following to your `.eslintrc` config file: ```js diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js index a23d843845..b1e0a62a4f 100644 --- a/lib/rules/jsx-key.js +++ b/lib/rules/jsx-key.js @@ -155,10 +155,33 @@ module.exports = { } } + const childrenToArraySelector = `:matches( + CallExpression + [callee.object.object.name=${reactPragma}] + [callee.object.property.name=Children] + [callee.property.name=toArray], + CallExpression + [callee.object.name=Children] + [callee.property.name=toArray] + )`.replace(/\s/g, ''); + let isWithinChildrenToArray = false; + const seen = new WeakSet(); return { + [childrenToArraySelector]() { + isWithinChildrenToArray = true; + }, + + [`${childrenToArraySelector}:exit`]() { + isWithinChildrenToArray = false; + }, + 'ArrayExpression, JSXElement > JSXElement'(node) { + if (isWithinChildrenToArray) { + return; + } + const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter((x) => x && x.type === 'JSXElement'); if (jsx.length === 0) { return; @@ -205,7 +228,7 @@ module.exports = { }, JSXFragment(node) { - if (!checkFragmentShorthand) { + if (!checkFragmentShorthand || isWithinChildrenToArray) { return; } @@ -226,6 +249,10 @@ module.exports = { CallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"],\ OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\ OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) { + if (isWithinChildrenToArray) { + return; + } + const fn = node.arguments.length > 0 && node.arguments[0]; if (!fn || !astUtil.isFunctionLikeExpression(fn)) { return; @@ -238,6 +265,10 @@ module.exports = { // Array.from 'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) { + if (isWithinChildrenToArray) { + return; + } + const fn = node.arguments.length > 1 && node.arguments[1]; if (!astUtil.isFunctionLikeExpression(fn)) { return; diff --git a/lib/rules/jsx-no-constructed-context-values.js b/lib/rules/jsx-no-constructed-context-values.js index af7c9575df..ac34ead85e 100644 --- a/lib/rules/jsx-no-constructed-context-values.js +++ b/lib/rules/jsx-no-constructed-context-values.js @@ -6,6 +6,7 @@ 'use strict'; +const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -139,7 +140,8 @@ module.exports = { messages, }, - create(context) { + // eslint-disable-next-line arrow-body-style + create: Components.detect((context, components, utils) => { return { JSXOpeningElement(node) { const openingElementName = node.name; @@ -184,6 +186,10 @@ module.exports = { return; } + if (!utils.getParentComponent(node)) { + return; + } + // Report found error const constructType = constructInfo.type; const constructNode = constructInfo.node; @@ -214,5 +220,5 @@ module.exports = { }); }, }; - }, + }), }; diff --git a/lib/util/Components.js b/lib/util/Components.js index 7b931c2234..b112923f63 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -21,7 +21,7 @@ const isFirstLetterCapitalized = require('./isFirstLetterCapitalized'); const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport'); function getId(node) { - return node && node.range.join(':'); + return node ? `${node.range[0]}:${node.range[1]}` : ''; } function usedPropTypesAreEquivalent(propA, propB) { diff --git a/tests/lib/rules/jsx-key.js b/tests/lib/rules/jsx-key.js index 451bcb0d57..15b89c19b7 100644 --- a/tests/lib/rules/jsx-key.js +++ b/tests/lib/rules/jsx-key.js @@ -176,6 +176,33 @@ ruleTester.run('jsx-key', rule, { `, features: ['types', 'no-babel-old'], }, + { code: 'React.Children.toArray([1, 2 ,3].map(x => ));' }, + { + code: ` + import { Children } from "react"; + Children.toArray([1, 2 ,3].map(x => )); + `, + }, + { + // TODO: uncomment the commented lines below + code: ` + import Act from 'react'; + import { Children as ReactChildren } from 'react'; + + const { Children } = Act; + const { toArray } = Children; + + Act.Children.toArray([1, 2 ,3].map(x => )); + Act.Children.toArray(Array.from([1, 2 ,3], x => )); + Children.toArray([1, 2 ,3].map(x => )); + Children.toArray(Array.from([1, 2 ,3], x => )); + // ReactChildren.toArray([1, 2 ,3].map(x => )); + // ReactChildren.toArray(Array.from([1, 2 ,3], x => )); + // toArray([1, 2 ,3].map(x => )); + // toArray(Array.from([1, 2 ,3], x => )); + `, + settings, + }, ]), 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 1ec0592382..c8c9dee995 100644 --- a/tests/lib/rules/jsx-no-constructed-context-values.js +++ b/tests/lib/rules/jsx-no-constructed-context-values.js @@ -31,13 +31,13 @@ const ruleTester = new RuleTester({ parserOptions }); ruleTester.run('react-no-constructed-context-values', rule, { valid: parsers.all([ { - code: '', + code: 'const Component = () => ', }, { - code: '', + code: 'const Component = () => ', }, { - code: '', + code: 'const Component = () => ', }, { code: 'function Component() { const foo = useMemo(() => { return {} }, []); return ()}', @@ -137,6 +137,16 @@ ruleTester.run('react-no-constructed-context-values', rule, { } `, }, + { + code: ` + const root = ReactDOM.createRoot(document.getElementById('root')); + root.render( + + + + ); + `, + }, ]), invalid: parsers.all([ {