From a762b25e6dcbdca9e4ff1eb1c564123a1105b075 Mon Sep 17 00:00:00 2001 From: Simon Schick Date: Tue, 2 Apr 2024 20:47:46 -0700 Subject: [PATCH] [New] add `jsx-props-no-spread-multi` --- CHANGELOG.md | 3 + README.md | 1 + docs/rules/jsx-props-no-spread-multi.md | 26 +++++++ lib/rules/index.js | 1 + lib/rules/jsx-props-no-spread-multi.js | 53 +++++++++++++++ lib/types.d.ts | 3 +- tests/lib/rules/jsx-props-no-spread-multi.js | 71 ++++++++++++++++++++ 7 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 docs/rules/jsx-props-no-spread-multi.md create mode 100644 lib/rules/jsx-props-no-spread-multi.js create mode 100644 tests/lib/rules/jsx-props-no-spread-multi.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c62eed275..b7ca4f6713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Added * export flat configs from plugin root and fix flat config crash ([#3694][] @bradzacher @mdjermanovic) +* add [`jsx-props-no-spread-multi`] ([#3724][] @SimonSchick) +[#3724]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3724 [#3694]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3694 ## [7.34.4] - 2024.07.13 @@ -4267,6 +4269,7 @@ If you're still not using React 15 you can keep the old behavior by setting the [`jsx-one-expression-per-line`]: docs/rules/jsx-one-expression-per-line.md [`jsx-pascal-case`]: docs/rules/jsx-pascal-case.md [`jsx-props-no-multi-spaces`]: docs/rules/jsx-props-no-multi-spaces.md +[`jsx-props-no-spread-multi`]: docs/rules/jsx-props-no-spread-multi.md [`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md [`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md [`jsx-sort-default-props`]: docs/rules/jsx-sort-default-props.md diff --git a/README.md b/README.md index 5528152083..693ad667b0 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,7 @@ module.exports = [ | [jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md) | Require one JSX element per line | | | 🔧 | | | | [jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components | | | | | | | [jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props | | | 🔧 | | | +| [jsx-props-no-spread-multi](docs/rules/jsx-props-no-spread-multi.md) | Disallow JSX prop spreading the same identifier multiple times | | | | | | | [jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Disallow JSX prop spreading | | | | | | | [jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting | | | | | ❌ | | [jsx-sort-props](docs/rules/jsx-sort-props.md) | Enforce props alphabetical sorting | | | 🔧 | | | diff --git a/docs/rules/jsx-props-no-spread-multi.md b/docs/rules/jsx-props-no-spread-multi.md new file mode 100644 index 0000000000..bc3a8bc148 --- /dev/null +++ b/docs/rules/jsx-props-no-spread-multi.md @@ -0,0 +1,26 @@ +# Disallow JSX prop spreading the same identifier multiple times (`react/jsx-props-no-spread-multi`) + + + +Enforces that any unique expression is only spread once. +Generally spreading the same expression twice is an indicator of a mistake since any attribute between the spreads may be overridden when the intent was not to. +Even when that is not the case this will lead to unnecessary computations being performed. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```jsx + +``` + +Examples of **correct** code for this rule: + +```jsx + + +``` + +## When Not To Use It + +When spreading the same expression multiple times yields different results. diff --git a/lib/rules/index.js b/lib/rules/index.js index c30dc6e609..11a4475ba2 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -50,6 +50,7 @@ module.exports = { 'jsx-fragments': require('./jsx-fragments'), 'jsx-props-no-multi-spaces': require('./jsx-props-no-multi-spaces'), 'jsx-props-no-spreading': require('./jsx-props-no-spreading'), + 'jsx-props-no-spread-multi': require('./jsx-props-no-spread-multi'), 'jsx-sort-default-props': require('./jsx-sort-default-props'), 'jsx-sort-props': require('./jsx-sort-props'), 'jsx-space-before-closing': require('./jsx-space-before-closing'), diff --git a/lib/rules/jsx-props-no-spread-multi.js b/lib/rules/jsx-props-no-spread-multi.js new file mode 100644 index 0000000000..2eeed0be49 --- /dev/null +++ b/lib/rules/jsx-props-no-spread-multi.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Prevent JSX prop spreading the same expression multiple times + * @author Simon Schick + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); +const report = require('../util/report'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const messages = { + noMultiSpreading: 'Spreading the same expression multiple times is forbidden', +}; + +module.exports = { + meta: { + docs: { + description: 'Disallow JSX prop spreading the same identifier multiple times', + category: 'Best Practices', + recommended: false, + url: docsUrl('jsx-props-no-spread-multi'), + }, + messages, + }, + + create(context) { + return { + JSXOpeningElement(node) { + const spreads = node.attributes.filter( + (attr) => attr.type === 'JSXSpreadAttribute' + && attr.argument.type === 'Identifier' + ); + if (spreads.length < 2) { + return; + } + // We detect duplicate expressions by their identifier + const identifierNames = new Set(); + spreads.forEach((spread) => { + if (identifierNames.has(spread.argument.name)) { + report(context, messages.noMultiSpreading, 'noMultiSpreading', { + node: spread, + }); + } + identifierNames.add(spread.argument.name); + }); + }, + }; + }, +}; diff --git a/lib/types.d.ts b/lib/types.d.ts index e13e204524..30df9c02c1 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -11,9 +11,10 @@ declare global { type JSXAttribute = ASTNode; type JSXElement = ASTNode; type JSXFragment = ASTNode; + type JSXOpeningElement = ASTNode; type JSXSpreadAttribute = ASTNode; - type Context = eslint.Rule.RuleContext + type Context = eslint.Rule.RuleContext; type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set) => object; diff --git a/tests/lib/rules/jsx-props-no-spread-multi.js b/tests/lib/rules/jsx-props-no-spread-multi.js new file mode 100644 index 0000000000..4566ef9066 --- /dev/null +++ b/tests/lib/rules/jsx-props-no-spread-multi.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Tests for jsx-props-no-spread-multi + */ + +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/jsx-props-no-spread-multi'); + +const parsers = require('../../helpers/parsers'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ruleTester = new RuleTester({ parserOptions }); +const expectedError = { messageId: 'noMultiSpreading' }; + +ruleTester.run('jsx-props-no-spread-multi', rule, { + valid: parsers.all([ + { + code: ` + const a = {}; + + `, + }, + { + code: ` + const a = {}; + const b = {}; + + `, + }, + ]), + + invalid: parsers.all([ + { + code: ` + const props = {}; + + `, + errors: [expectedError], + }, + { + code: ` + const props = {}; +
+ `, + errors: [expectedError], + }, + { + code: ` + const props = {}; +
+ `, + errors: [expectedError, expectedError], + }, + ]), +});