Skip to content

Commit e0f2872

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

File tree

6 files changed

+170
-1
lines changed

6 files changed

+170
-1
lines changed

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 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 | | | 🔧 | | |
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Disallow JSX prop spreading the same expression 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'),
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
const ignoredAstProperties = new Set(['parent', 'range', 'loc', 'start', 'end', '_babelType']);
20+
21+
/**
22+
* Filter for JSON.stringify that omits circular and position structures.
23+
*
24+
* @param {string} key
25+
* @param {*} value
26+
* @returns {*}
27+
*/
28+
const propertyFilter = (key, value) => (ignoredAstProperties.has(key) ? undefined : value);
29+
30+
module.exports = {
31+
meta: {
32+
docs: {
33+
description: 'Disallow JSX prop spreading the same expression multiple times',
34+
category: 'Best Practices',
35+
recommended: false,
36+
url: docsUrl('jsx-props-no-spread-multi'),
37+
},
38+
messages,
39+
},
40+
41+
create(context) {
42+
return {
43+
JSXOpeningElement(node) {
44+
const spreads = node.attributes.filter((attr) => attr.type === 'JSXSpreadAttribute');
45+
if (spreads.length < 2) {
46+
return;
47+
}
48+
// We detect duplicate expressions by hashing the ast nodes
49+
const argumentHashes = new Set();
50+
for (const spread of spreads) {
51+
// TODO: Deep compare ast function?
52+
const hash = JSON.stringify(spread.argument, propertyFilter);
53+
if (argumentHashes.has(hash)) {
54+
report(context, messages.noMultiSpreading, 'noMultiSpreading', {
55+
node: spread,
56+
});
57+
}
58+
argumentHashes.add(hash);
59+
}
60+
},
61+
};
62+
},
63+
};

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,78 @@
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+
code: `
72+
const func = () => ({});
73+
<div {...func()} {...func()} />
74+
`,
75+
errors: [expectedError],
76+
},
77+
]),
78+
});

0 commit comments

Comments
 (0)