Skip to content

Commit 8b760cc

Browse files
author
Simon Schick
committed
feat(rules): add jsx-props-no-spread-multi
1 parent e4ecbcf commit 8b760cc

File tree

7 files changed

+162
-2
lines changed

7 files changed

+162
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
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 expression 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 | | | 🔧 | | |

configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = Object.assign({}, all, {
1111
'react/jsx-no-duplicate-props': 2,
1212
'react/jsx-no-target-blank': 2,
1313
'react/jsx-no-undef': 2,
14+
'react/jsx-props-no-spread-multi': 2,
1415
'react/jsx-uses-react': 2,
1516
'react/jsx-uses-vars': 2,
1617
'react/no-children-prop': 2,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Disallow JSX prop spreading the same expression multiple times (`react/jsx-props-no-spread-multi`)
2+
3+
💼 This rule is enabled in the ☑️ `recommended` [config](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
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
8+
the intent was not to. Even when that is not the case this will lead to unnecessary computations to be performed.
9+
10+
## Rule Details
11+
12+
Examples of **incorrect** code for this rule:
13+
14+
```jsx
15+
<App {...props} myAttr='1' {...props} />
16+
```
17+
18+
Examples of **correct** code for this rule:
19+
20+
```jsx
21+
<App myAttr='1' {...props} />
22+
<App {...props} myAttr='1' />
23+
```
24+
25+
## When Not To Use It
26+
27+
When spreading the same expression yields different values.

lib/rules/index.js

Lines changed: 1 addition & 0 deletions
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'),
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @fileoverview Prevent JSX prop spreading the same value 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 value multiple times is forbidden',
17+
};
18+
19+
/**
20+
* Filter for JSON.stringify that omits circular and position structures.
21+
*
22+
* @param {string} key
23+
* @param {*} value
24+
* @returns {*}
25+
*/
26+
const propertyFilter = (key, value) => (key !== 'parent' && key !== 'range' && key !== 'loc' ? value : undefined);
27+
28+
module.exports = {
29+
meta: {
30+
docs: {
31+
description: 'Disallow JSX prop spreading the same expression multiple times',
32+
category: 'Best Practices',
33+
recommended: true,
34+
url: docsUrl('jsx-props-no-spread-multi'),
35+
},
36+
messages,
37+
},
38+
39+
create(context) {
40+
return {
41+
JSXOpeningElement(node) {
42+
const spreads = node.attributes.filter((attr) => attr.type === 'JSXSpreadAttribute');
43+
if (spreads.length < 2) {
44+
return;
45+
}
46+
const hashes = new Set();
47+
for (const spread of spreads) {
48+
// TODO: Deep compare ast function?
49+
const hash = JSON.stringify(spread, propertyFilter);
50+
if (hashes.has(hash)) {
51+
report(context, messages.noMultiSpreading, 'noMultiSpreading', {
52+
node: spread,
53+
});
54+
}
55+
hashes.add(hash);
56+
}
57+
},
58+
};
59+
},
60+
};

lib/types.d.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ 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

18-
type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set<typeof annotation>) => object;
19+
type TypeDeclarationBuilder = (
20+
annotation: ASTNode,
21+
parentName: string,
22+
seen: Set<typeof annotation>
23+
) => object;
1924

2025
type TypeDeclarationBuilders = {
2126
[k in string]: TypeDeclarationBuilder;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
const b = {};
37+
<App {...a} {...b} />
38+
`,
39+
},
40+
]),
41+
42+
invalid: parsers.all([
43+
{
44+
code: `
45+
const props = {};
46+
<App {...props} {...props} />
47+
`,
48+
errors: [expectedError],
49+
},
50+
{
51+
code: `
52+
const props = {};
53+
<div {...props} a='a' {...props} />
54+
`,
55+
errors: [expectedError],
56+
},
57+
{
58+
code: `
59+
const func = () => ({});
60+
<div {...func()} {...func()} />
61+
`,
62+
errors: [expectedError],
63+
},
64+
]),
65+
});

0 commit comments

Comments
 (0)