Skip to content

Commit a762b25

Browse files
SimonSchickljharb
authored andcommitted
[New] add jsx-props-no-spread-multi
1 parent e64e260 commit a762b25

File tree

7 files changed

+157
-1
lines changed

7 files changed

+157
-1
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
77

88
### Added
99
* export flat configs from plugin root and fix flat config crash ([#3694][] @bradzacher @mdjermanovic)
10+
* add [`jsx-props-no-spread-multi`] ([#3724][] @SimonSchick)
1011

12+
[#3724]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3724
1113
[#3694]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3694
1214

1315
## [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
42674269
[`jsx-one-expression-per-line`]: docs/rules/jsx-one-expression-per-line.md
42684270
[`jsx-pascal-case`]: docs/rules/jsx-pascal-case.md
42694271
[`jsx-props-no-multi-spaces`]: docs/rules/jsx-props-no-multi-spaces.md
4272+
[`jsx-props-no-spread-multi`]: docs/rules/jsx-props-no-spread-multi.md
42704273
[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md
42714274
[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md
42724275
[`jsx-sort-default-props`]: docs/rules/jsx-sort-default-props.md

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ module.exports = [
333333
| [jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md) | Require one JSX element per line | | | 🔧 | | |
334334
| [jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components | | | | | |
335335
| [jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props | | | 🔧 | | |
336+
| [jsx-props-no-spread-multi](docs/rules/jsx-props-no-spread-multi.md) | Disallow JSX prop spreading the same identifier multiple times | | | | | |
336337
| [jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Disallow JSX prop spreading | | | | | |
337338
| [jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting | | | | ||
338339
| [jsx-sort-props](docs/rules/jsx-sort-props.md) | Enforce props alphabetical sorting | | | 🔧 | | |
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Disallow JSX prop spreading the same identifier multiple times (`react/jsx-props-no-spread-multi`)
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Enforces that any unique expression is only spread once.
6+
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.
7+
Even when that is not the case this will lead to unnecessary computations being performed.
8+
9+
## Rule Details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```jsx
14+
<App {...props} myAttr="1" {...props} />
15+
```
16+
17+
Examples of **correct** code for this rule:
18+
19+
```jsx
20+
<App myAttr="1" {...props} />
21+
<App {...props} myAttr="1" />
22+
```
23+
24+
## When Not To Use It
25+
26+
When spreading the same expression multiple times yields different results.

lib/rules/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module.exports = {
5050
'jsx-fragments': require('./jsx-fragments'),
5151
'jsx-props-no-multi-spaces': require('./jsx-props-no-multi-spaces'),
5252
'jsx-props-no-spreading': require('./jsx-props-no-spreading'),
53+
'jsx-props-no-spread-multi': require('./jsx-props-no-spread-multi'),
5354
'jsx-sort-default-props': require('./jsx-sort-default-props'),
5455
'jsx-sort-props': require('./jsx-sort-props'),
5556
'jsx-space-before-closing': require('./jsx-space-before-closing'),
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @fileoverview Prevent JSX prop spreading the same expression multiple times
3+
* @author Simon Schick
4+
*/
5+
6+
'use strict';
7+
8+
const docsUrl = require('../util/docsUrl');
9+
const report = require('../util/report');
10+
11+
// ------------------------------------------------------------------------------
12+
// Rule Definition
13+
// ------------------------------------------------------------------------------
14+
15+
const messages = {
16+
noMultiSpreading: 'Spreading the same expression multiple times is forbidden',
17+
};
18+
19+
module.exports = {
20+
meta: {
21+
docs: {
22+
description: 'Disallow JSX prop spreading the same identifier multiple times',
23+
category: 'Best Practices',
24+
recommended: false,
25+
url: docsUrl('jsx-props-no-spread-multi'),
26+
},
27+
messages,
28+
},
29+
30+
create(context) {
31+
return {
32+
JSXOpeningElement(node) {
33+
const spreads = node.attributes.filter(
34+
(attr) => attr.type === 'JSXSpreadAttribute'
35+
&& attr.argument.type === 'Identifier'
36+
);
37+
if (spreads.length < 2) {
38+
return;
39+
}
40+
// We detect duplicate expressions by their identifier
41+
const identifierNames = new Set();
42+
spreads.forEach((spread) => {
43+
if (identifierNames.has(spread.argument.name)) {
44+
report(context, messages.noMultiSpreading, 'noMultiSpreading', {
45+
node: spread,
46+
});
47+
}
48+
identifierNames.add(spread.argument.name);
49+
});
50+
},
51+
};
52+
},
53+
};

lib/types.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ declare global {
1111
type JSXAttribute = ASTNode;
1212
type JSXElement = ASTNode;
1313
type JSXFragment = ASTNode;
14+
type JSXOpeningElement = ASTNode;
1415
type JSXSpreadAttribute = ASTNode;
1516

16-
type Context = eslint.Rule.RuleContext
17+
type Context = eslint.Rule.RuleContext;
1718

1819
type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set<typeof annotation>) => object;
1920

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @fileoverview Tests for jsx-props-no-spread-multi
3+
*/
4+
5+
'use strict';
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
const RuleTester = require('eslint').RuleTester;
12+
const rule = require('../../../lib/rules/jsx-props-no-spread-multi');
13+
14+
const parsers = require('../../helpers/parsers');
15+
16+
const parserOptions = {
17+
ecmaVersion: 2018,
18+
sourceType: 'module',
19+
ecmaFeatures: {
20+
jsx: true,
21+
},
22+
};
23+
24+
// -----------------------------------------------------------------------------
25+
// Tests
26+
// -----------------------------------------------------------------------------
27+
28+
const ruleTester = new RuleTester({ parserOptions });
29+
const expectedError = { messageId: 'noMultiSpreading' };
30+
31+
ruleTester.run('jsx-props-no-spread-multi', rule, {
32+
valid: parsers.all([
33+
{
34+
code: `
35+
const a = {};
36+
<App {...a} />
37+
`,
38+
},
39+
{
40+
code: `
41+
const a = {};
42+
const b = {};
43+
<App {...a} {...b} />
44+
`,
45+
},
46+
]),
47+
48+
invalid: parsers.all([
49+
{
50+
code: `
51+
const props = {};
52+
<App {...props} {...props} />
53+
`,
54+
errors: [expectedError],
55+
},
56+
{
57+
code: `
58+
const props = {};
59+
<div {...props} a="a" {...props} />
60+
`,
61+
errors: [expectedError],
62+
},
63+
{
64+
code: `
65+
const props = {};
66+
<div {...props} {...props} {...props} />
67+
`,
68+
errors: [expectedError, expectedError],
69+
},
70+
]),
71+
});

0 commit comments

Comments
 (0)