Skip to content

Commit 015d168

Browse files
SimonSchickljharb
authored andcommitted
[New] add jsx-props-no-spread-multi
1 parent 393bfa2 commit 015d168

File tree

7 files changed

+158
-1
lines changed

7 files changed

+158
-1
lines changed

CHANGELOG.md

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

66
## Unreleased
77

8+
## Added
9+
* add [`jsx-props-no-spread-multi`] ([#3724][] @SimonSchick)
10+
811
### Fixed
912
* [`prop-types`]: null-check rootNode before calling getScope ([#3762][] @crnhrv)
1013
* [`boolean-prop-naming`]: avoid a crash with a spread prop ([#3733][] @ljharb)
1114
* [`jsx-boolean-value`]: `assumeUndefinedIsFalse` with `never` must not allow explicit `true` value ([#3757][] @6uliver)
1215
* [`no-object-type-as-default-prop`]: enable rule for components with many parameters ([#3768][] @JulienR1)
1316

17+
[#3724]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3724
1418
[#3768]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3768
1519
[#3762]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3762
1620
[#3757]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3757
@@ -4246,6 +4250,7 @@ If you're still not using React 15 you can keep the old behavior by setting the
42464250
[`jsx-one-expression-per-line`]: docs/rules/jsx-one-expression-per-line.md
42474251
[`jsx-pascal-case`]: docs/rules/jsx-pascal-case.md
42484252
[`jsx-props-no-multi-spaces`]: docs/rules/jsx-props-no-multi-spaces.md
4253+
[`jsx-props-no-spread-multi`]: docs/rules/jsx-props-no-spread-multi.md
42494254
[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md
42504255
[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md
42514256
[`jsx-sort-default-props`]: docs/rules/jsx-sort-default-props.md

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ module.exports = [
338338
| [jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md) | Require one JSX element per line | | | 🔧 | | |
339339
| [jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components | | | | | |
340340
| [jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props | | | 🔧 | | |
341+
| [jsx-props-no-spread-multi](docs/rules/jsx-props-no-spread-multi.md) | Disallow JSX prop spreading the same identifier multiple times | | | | | |
341342
| [jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Disallow JSX prop spreading | | | | | |
342343
| [jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting | | | | ||
343344
| [jsx-sort-props](docs/rules/jsx-sort-props.md) | Enforce props alphabetical sorting | | | 🔧 | | |
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 express 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
6+
the intent was not to. Even when that is not the case this will lead to unnecessary computations to be performed.
7+
8+
## Rule Details
9+
10+
Examples of **incorrect** code for this rule:
11+
12+
```jsx
13+
<App {...props} myAttr="1" {...props} />
14+
```
15+
16+
Examples of **correct** code for this rule:
17+
18+
```jsx
19+
<App myAttr="1" {...props} />
20+
<App {...props} myAttr="1" />
21+
```
22+
23+
## When Not To Use It
24+
25+
When spreading the same expression yields different values.

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)