Skip to content

Commit 752de70

Browse files
ashbhirljharb
authored andcommitted
[New] add jsx-props-no-spreading
1 parent b7f0b02 commit 752de70

File tree

6 files changed

+365
-0
lines changed

6 files changed

+365
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,3 +2497,4 @@ If you're still not using React 15 you can keep the old behavior by setting the
24972497
[`jsx-props-no-multi-spaces`]: docs/rules/jsx-props-no-multi-spaces.md
24982498
[`no-unsafe`]: docs/rules/no-unsafe.md
24992499
[`jsx-fragments`]: docs/rules/jsx-fragments.md
2500+
[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ Enable the rules that you would like to use.
172172
* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments
173173
* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
174174
* [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md): Disallow multiple spaces between inline JSX props (fixable)
175+
* [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md): Disallow JSX props spreading
175176
* [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting
176177
* [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable)
177178
* [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable)

docs/rules/jsx-props-no-spreading.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Disallow JSX props spreading (react/jsx-props-no-spreading)
2+
3+
Enforces that there is no spreading for any JSX attribute. This enhances readability of code by being more explicit about what props are received by the component. It is also good for maintainability by avoiding passing unintentional extra props and allowing react to emit warnings when invalid HTML props are passed to HTML elements.
4+
5+
## Rule Details
6+
7+
The following patterns are considered warnings:
8+
9+
```jsx
10+
<App {...props} />
11+
<MyCustomComponent {...props} some_other_prop={some_other_prop} />
12+
<img {...props} />
13+
```
14+
15+
The following patterns are **not** considered warnings:
16+
17+
```jsx
18+
const {src, alt} = props;
19+
const {one_prop, two_prop} = otherProps;
20+
<MyCustomComponent one_prop={one_prop} two_prop={two_prop} />
21+
<img src={src} alt={alt} />
22+
```
23+
24+
25+
## Rule Options
26+
27+
```js
28+
...
29+
"react/jsx-props-no-spreading": [{
30+
"html": "ignore" / "enforce",
31+
"custom": "ignore" / "enforce",
32+
"exceptions": [<string>]
33+
}]
34+
...
35+
```
36+
37+
### html
38+
39+
`html` set to `ignore` will ignore all html jsx tags like `div`, `img` etc. Default is set to `enforce`.
40+
41+
The following patterns are **not** considered warnings when `html` is set to `ignore`:
42+
43+
```jsx
44+
<img {...props} />
45+
```
46+
47+
The following patterns are still considered warnings:
48+
49+
```jsx
50+
<MyCustomComponent {...props} />
51+
```
52+
53+
### custom
54+
55+
`custom` set to `ignore` will ignore all custom jsx tags like `App`, `MyCustomComponent` etc. Default is set to `enforce`.
56+
57+
The following patterns are **not** considered warnings when `custom` is set to `ignore`:
58+
59+
```jsx
60+
<MyCustomComponent {...props} />
61+
```
62+
63+
The following patterns are still considered warnings:
64+
```jsx
65+
<img {...props} />
66+
```
67+
68+
### exceptions
69+
70+
An "exception" will always flip the resulting html or custom setting for that component - ie, html set to `ignore`, with an exception of `div` will enforce on an `div`; custom set to `enforce` with an exception of `Foo` will ignore `Foo`.
71+
72+
```js
73+
{ "exceptions": ["Image", "img"] }
74+
```
75+
76+
The following patterns are **not** considered warnings:
77+
78+
```jsx
79+
const {src, alt} = props;
80+
<Image {...props} />
81+
<img {...props} />
82+
```
83+
84+
The following patterns are considered warnings:
85+
```jsx
86+
<MyCustomComponent {...props} />
87+
```
88+
89+
```js
90+
{ "html": "ignore", "exceptions": ["MyCustomComponent", "img"] }
91+
```
92+
93+
The following patterns are **not** considered warnings:
94+
95+
```jsx
96+
const {src, alt} = props;
97+
const {one_prop, two_prop} = otherProps;
98+
<img src={src} alt={alt} />
99+
<MyCustomComponent {...otherProps} />
100+
```
101+
102+
The following patterns are considered warnings:
103+
```jsx
104+
<img {...props} />
105+
```
106+
107+
## When Not To Use It
108+
109+
If you are not using JSX or have lots of props to be passed or the props spreading is used inside HOC.

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const allRules = {
3838
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
3939
'jsx-fragments': require('./lib/rules/jsx-fragments'),
4040
'jsx-props-no-multi-spaces': require('./lib/rules/jsx-props-no-multi-spaces'),
41+
'jsx-props-no-spreading': require('./lib/rules/jsx-props-no-spreading'),
4142
'jsx-sort-default-props': require('./lib/rules/jsx-sort-default-props'),
4243
'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
4344
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),

lib/rules/jsx-props-no-spreading.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @fileoverview Prevent JSX prop spreading
3+
* @author Ashish Gambhir
4+
*/
5+
'use strict';
6+
7+
const docsUrl = require('../util/docsUrl');
8+
9+
// ------------------------------------------------------------------------------
10+
// Constants
11+
// ------------------------------------------------------------------------------
12+
13+
const OPTIONS = {ignore: 'ignore', enforce: 'enforce'};
14+
const DEFAULTS = {html: OPTIONS.enforce, custom: OPTIONS.enforce, exceptions: []};
15+
16+
// ------------------------------------------------------------------------------
17+
// Rule Definition
18+
// ------------------------------------------------------------------------------
19+
20+
module.exports = {
21+
meta: {
22+
docs: {
23+
description: 'Prevent JSX prop spreading',
24+
category: 'Best Practices',
25+
recommended: false,
26+
url: docsUrl('jsx-props-no-spreading')
27+
},
28+
schema: [{
29+
allOf: [{
30+
type: 'object',
31+
properties: {
32+
html: {
33+
enum: [OPTIONS.enforce, OPTIONS.ignore]
34+
},
35+
custom: {
36+
enum: [OPTIONS.enforce, OPTIONS.ignore]
37+
},
38+
exceptions: {
39+
type: 'array',
40+
items: {
41+
type: 'string',
42+
uniqueItems: true
43+
}
44+
}
45+
}
46+
}, {
47+
not: {
48+
type: 'object',
49+
required: ['html', 'custom'],
50+
properties: {
51+
html: {
52+
enum: [OPTIONS.ignore]
53+
},
54+
custom: {
55+
enum: [OPTIONS.ignore]
56+
},
57+
exceptions: {
58+
type: 'array',
59+
minItems: 0,
60+
maxItems: 0
61+
}
62+
}
63+
}
64+
}]
65+
}]
66+
},
67+
68+
create: function (context) {
69+
const configuration = context.options[0] || {};
70+
const ignoreHtmlTags = (configuration.html || DEFAULTS.html) === OPTIONS.ignore;
71+
const ignoreCustomTags = (configuration.custom || DEFAULTS.custom) === OPTIONS.ignore;
72+
const exceptions = configuration.exceptions || DEFAULTS.exceptions;
73+
const isException = (tag, allExceptions) => allExceptions.indexOf(tag) !== -1;
74+
return {
75+
JSXSpreadAttribute: function (node) {
76+
const tagName = node.parent.name.name;
77+
const isHTMLTag = tagName && tagName[0] !== tagName[0].toUpperCase();
78+
const isCustomTag = tagName && tagName[0] === tagName[0].toUpperCase();
79+
if (isHTMLTag &&
80+
((ignoreHtmlTags && !isException(tagName, exceptions)) ||
81+
(!ignoreHtmlTags && isException(tagName, exceptions)))) {
82+
return;
83+
}
84+
if (isCustomTag &&
85+
((ignoreCustomTags && !isException(tagName, exceptions)) ||
86+
(!ignoreCustomTags && isException(tagName, exceptions)))) {
87+
return;
88+
}
89+
context.report({
90+
node: node,
91+
message: 'Prop spreading is forbidden'
92+
});
93+
}
94+
};
95+
}
96+
};
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* @fileoverview Tests for jsx-props-no-spreading
3+
*/
4+
'use strict';
5+
6+
// -----------------------------------------------------------------------------
7+
// Requirements
8+
// -----------------------------------------------------------------------------
9+
10+
const rule = require('../../../lib/rules/jsx-props-no-spreading');
11+
const RuleTester = require('eslint').RuleTester;
12+
13+
const parserOptions = {
14+
ecmaVersion: 2018,
15+
sourceType: 'module',
16+
ecmaFeatures: {
17+
jsx: true
18+
}
19+
};
20+
21+
// -----------------------------------------------------------------------------
22+
// Tests
23+
// -----------------------------------------------------------------------------
24+
25+
const ruleTester = new RuleTester({parserOptions});
26+
const expectedError = {message: 'Prop spreading is forbidden'};
27+
28+
ruleTester.run('jsx-props-no-spreading', rule, {
29+
valid: [{
30+
code: [
31+
'const {one_prop, two_prop} = props;',
32+
'<App one_prop={one_prop} two_prop={two_prop}/>'
33+
].join('\n')
34+
}, {
35+
code: [
36+
'const {one_prop, two_prop} = props;',
37+
'<div one_prop={one_prop} two_prop={two_prop}></div>'
38+
].join('\n')
39+
}, {
40+
code: [
41+
'const newProps = {...props};',
42+
'<App one_prop={newProps.one_prop} two_prop={newProps.two_prop} style={{...styles}}/>'
43+
].join('\n')
44+
}, {
45+
code: [
46+
'const props = {src: "dummy.jpg", alt: "dummy"};',
47+
'<App>',
48+
' <Image {...props}/>',
49+
' <img {...props}/>',
50+
'</App>'
51+
].join('\n'),
52+
options: [{exceptions: ['Image', 'img']}]
53+
}, {
54+
code: [
55+
'const props = {src: "dummy.jpg", alt: "dummy"};',
56+
'const { src, alt } = props;',
57+
'<App>',
58+
' <Image {...props}/>',
59+
' <img src={src} alt={alt}/>',
60+
'</App>'
61+
].join('\n'),
62+
options: [{custom: 'ignore'}]
63+
}, {
64+
code: [
65+
'const props = {src: "dummy.jpg", alt: "dummy"};',
66+
'const { src, alt } = props;',
67+
'<App>',
68+
' <Image {...props}/>',
69+
' <img {...props}/>',
70+
'</App>'
71+
].join('\n'),
72+
options: [{custom: 'enforce', html: 'ignore', exceptions: ['Image']}]
73+
}, {
74+
code: [
75+
'const props = {src: "dummy.jpg", alt: "dummy"};',
76+
'const { src, alt } = props;',
77+
'<App>',
78+
' <img {...props}/>',
79+
' <Image src={src} alt={alt}/>',
80+
' <div {...someOtherProps}/>',
81+
'</App>'
82+
].join('\n'),
83+
options: [{html: 'ignore'}]
84+
}],
85+
86+
invalid: [{
87+
code: [
88+
'<App {...props}/>'
89+
].join('\n'),
90+
errors: [expectedError]
91+
}, {
92+
code: [
93+
'<div {...props}></div>'
94+
].join('\n'),
95+
errors: [expectedError]
96+
}, {
97+
code: [
98+
'<App {...props} some_other_prop={some_other_prop}/>'
99+
].join('\n'),
100+
errors: [expectedError]
101+
}, {
102+
code: [
103+
'const props = {src: "dummy.jpg", alt: "dummy"};',
104+
'<App>',
105+
' <Image {...props}/>',
106+
' <span {...props}/>',
107+
'</App>'
108+
].join('\n'),
109+
options: [{exceptions: ['Image', 'img']}],
110+
errors: [expectedError]
111+
}, {
112+
code: [
113+
'const props = {src: "dummy.jpg", alt: "dummy"};',
114+
'const { src, alt } = props;',
115+
'<App>',
116+
' <Image {...props}/>',
117+
' <img {...props}/>',
118+
'</App>'
119+
].join('\n'),
120+
options: [{custom: 'ignore'}],
121+
errors: [expectedError]
122+
}, {
123+
code: [
124+
'const props = {src: "dummy.jpg", alt: "dummy"};',
125+
'const { src, alt } = props;',
126+
'<App>',
127+
' <Image {...props}/>',
128+
' <img {...props}/>',
129+
'</App>'
130+
].join('\n'),
131+
options: [{html: 'ignore', exceptions: ['Image', 'img']}],
132+
errors: [expectedError]
133+
}, {
134+
code: [
135+
'const props = {src: "dummy.jpg", alt: "dummy"};',
136+
'const { src, alt } = props;',
137+
'<App>',
138+
' <Image {...props}/>',
139+
' <img {...props}/>',
140+
' <div {...props}/>',
141+
'</App>'
142+
].join('\n'),
143+
options: [{custom: 'ignore', html: 'ignore', exceptions: ['Image', 'img']}],
144+
errors: [expectedError, expectedError]
145+
}, {
146+
code: [
147+
'const props = {src: "dummy.jpg", alt: "dummy"};',
148+
'const { src, alt } = props;',
149+
'<App>',
150+
' <img {...props}/>',
151+
' <Image {...props}/>',
152+
'</App>'
153+
].join('\n'),
154+
options: [{html: 'ignore'}],
155+
errors: [expectedError]
156+
}]
157+
});

0 commit comments

Comments
 (0)