Skip to content

Commit 567bc7d

Browse files
golopotljharb
authored andcommitted
[New] destructuring-assignment: add option destructureInSignature
1 parent 37b4f8e commit 567bc7d

File tree

5 files changed

+150
-6
lines changed

5 files changed

+150
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
55

66
## Unreleased
77

8+
### Added
9+
* [`destructuring-assignment`]: add option `destructureInSignature` ([#3235][] @golopot)
10+
811
### Fixed
912
* [`hook-use-state`]: Allow UPPERCASE setState setter prefixes ([#3244][] @duncanbeevers)
1013
* `propTypes`: add `VFC` to react generic type param map ([#3230][] @dlech)
1114

1215
[#3244]: https://github.com/yannickcr/eslint-plugin-react/pull/3244
16+
[#3235]: https://github.com/yannickcr/eslint-plugin-react/pull/3235
1317
[#3230]: https://github.com/yannickcr/eslint-plugin-react/issues/3230
1418

1519
## [7.29.4] - 2022.03.13

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Enable the rules that you would like to use.
123123
| | | [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md) | Enforces consistent naming for boolean props |
124124
| | | [react/button-has-type](docs/rules/button-has-type.md) | Forbid "button" element without an explicit "type" attribute |
125125
| | | [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md) | Enforce all defaultProps are defined and not "required" in propTypes. |
126-
| | | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context |
126+
| | 🔧 | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context |
127127
|| | [react/display-name](docs/rules/display-name.md) | Prevent missing displayName in a React component definition |
128128
| | | [react/forbid-component-props](docs/rules/forbid-component-props.md) | Forbid certain props on components |
129129
| | | [react/forbid-dom-props](docs/rules/forbid-dom-props.md) | Forbid certain props on DOM Nodes |

docs/rules/destructuring-assignment.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const Foo = class extends React.PureComponent {
9191

9292
```js
9393
...
94-
"react/destructuring-assignment": [<enabled>, "always", { "ignoreClassFields": <boolean> }]
94+
"react/destructuring-assignment": [<enabled>, "always", { "ignoreClassFields": <boolean>, "destructureInSignature": "always" | "ignore" }]
9595
...
9696
```
9797

@@ -104,3 +104,33 @@ class Foo extends React.PureComponent {
104104
bar = this.props.bar
105105
}
106106
```
107+
108+
### `destructureInSignature` (default: "ignore")
109+
110+
This option can be one of `always` or `ignore`. When configured with `always`, the rule will require props destructuring happens in the function signature.
111+
112+
Examples of **incorrect** code for `destructureInSignature: 'always'` :
113+
114+
```jsx
115+
function Foo(props) {
116+
const {a} = props;
117+
return <>{a}</>
118+
}
119+
```
120+
121+
Examples of **correct** code for `destructureInSignature: 'always'` :
122+
123+
```jsx
124+
function Foo({a}) {
125+
return <>{a}</>
126+
}
127+
```
128+
129+
```jsx
130+
// Ignores when props is used elsewhere
131+
function Foo(props) {
132+
const {a} = props;
133+
useProps(props); // NOTE: it is a bad practice to pass the props object anywhere else!
134+
return <Goo a={a}/>
135+
}
136+
```

lib/rules/destructuring-assignment.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const messages = {
5050
noDestructContextInSFCArg: 'Must never use destructuring context assignment in SFC argument',
5151
noDestructAssignment: 'Must never use destructuring {{type}} assignment',
5252
useDestructAssignment: 'Must use destructuring {{type}} assignment',
53+
destructureInSignature: 'Must destructure props in the function signature.',
5354
};
5455

5556
module.exports = {
@@ -60,7 +61,7 @@ module.exports = {
6061
recommended: false,
6162
url: docsUrl('destructuring-assignment'),
6263
},
63-
64+
fixable: 'code',
6465
messages,
6566

6667
schema: [{
@@ -75,6 +76,13 @@ module.exports = {
7576
ignoreClassFields: {
7677
type: 'boolean',
7778
},
79+
destructureInSignature: {
80+
type: 'string',
81+
enum: [
82+
'always',
83+
'ignore',
84+
],
85+
},
7886
},
7987
additionalProperties: false,
8088
}],
@@ -83,6 +91,7 @@ module.exports = {
8391
create: Components.detect((context, components, utils) => {
8492
const configuration = context.options[0] || DEFAULT_OPTION;
8593
const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false;
94+
const destructureInSignature = (context.options[1] && context.options[1].destructureInSignature) || 'ignore';
8695
const sfcParams = createSFCParams();
8796

8897
/**
@@ -230,6 +239,41 @@ module.exports = {
230239
},
231240
});
232241
}
242+
243+
if (
244+
SFCComponent
245+
&& destructuringSFC
246+
&& configuration === 'always'
247+
&& destructureInSignature === 'always'
248+
&& node.init.name === 'props'
249+
) {
250+
const scopeSetProps = context.getScope().set.get('props');
251+
const propsRefs = scopeSetProps && scopeSetProps.references;
252+
if (!propsRefs) {
253+
return;
254+
}
255+
// Skip if props is used elsewhere
256+
if (propsRefs.length > 1) {
257+
return;
258+
}
259+
report(context, messages.destructureInSignature, 'destructureInSignature', {
260+
node,
261+
fix(fixer) {
262+
const param = SFCComponent.node.params[0];
263+
if (!param) {
264+
return;
265+
}
266+
const replaceRange = [
267+
param.range[0],
268+
param.typeAnnotation ? param.typeAnnotation.range[0] : param.range[1],
269+
];
270+
return [
271+
fixer.replaceTextRange(replaceRange, context.getSourceCode().getText(node.id)),
272+
fixer.remove(node.parent),
273+
];
274+
},
275+
});
276+
}
233277
},
234278
};
235279
}),

tests/lib/rules/destructuring-assignment.js

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
'use strict';
66

77
const RuleTester = require('eslint').RuleTester;
8+
const semver = require('semver');
9+
const eslintPkg = require('eslint/package.json');
810
const rule = require('../../../lib/rules/destructuring-assignment');
911

1012
const parsers = require('../../helpers/parsers');
@@ -338,9 +340,27 @@ ruleTester.run('destructuring-assignment', rule, {
338340
}
339341
`,
340342
},
343+
{
344+
code: `
345+
function Foo(props) {
346+
const {a} = props;
347+
return <Goo {...props}>{a}</Goo>;
348+
}
349+
`,
350+
options: ['always', { destructureInSignature: 'always' }],
351+
},
352+
{
353+
code: `
354+
function Foo(props) {
355+
const {a} = props;
356+
return <Goo f={() => props}>{a}</Goo>;
357+
}
358+
`,
359+
options: ['always', { destructureInSignature: 'always' }],
360+
},
341361
]),
342362

343-
invalid: parsers.all([
363+
invalid: parsers.all([].concat(
344364
{
345365
code: `
346366
const MyComponent = (props) => {
@@ -632,7 +652,7 @@ ruleTester.run('destructuring-assignment', rule, {
632652
633653
const TestComp = (props) => {
634654
props.onClick3102();
635-
655+
636656
return (
637657
<div
638658
onClick={(evt) => {
@@ -720,5 +740,51 @@ ruleTester.run('destructuring-assignment', rule, {
720740
},
721741
],
722742
},
723-
]),
743+
// Ignore for ESLint < 4 because ESLint < 4 does not support array fixer.
744+
semver.satisfies(eslintPkg.version, '>= 4') ? [
745+
{
746+
code: `
747+
function Foo(props) {
748+
const {a} = props;
749+
return <p>{a}</p>;
750+
}
751+
`,
752+
options: ['always', { destructureInSignature: 'always' }],
753+
errors: [
754+
{
755+
messageId: 'destructureInSignature',
756+
line: 3,
757+
},
758+
],
759+
output: `
760+
function Foo({a}) {
761+
762+
return <p>{a}</p>;
763+
}
764+
`,
765+
},
766+
{
767+
code: `
768+
function Foo(props: FooProps) {
769+
const {a} = props;
770+
return <p>{a}</p>;
771+
}
772+
`,
773+
options: ['always', { destructureInSignature: 'always' }],
774+
errors: [
775+
{
776+
messageId: 'destructureInSignature',
777+
line: 3,
778+
},
779+
],
780+
output: `
781+
function Foo({a}: FooProps) {
782+
783+
return <p>{a}</p>;
784+
}
785+
`,
786+
features: ['ts', 'no-babel'],
787+
},
788+
] : []
789+
)),
724790
});

0 commit comments

Comments
 (0)