diff --git a/README.md b/README.md index b1efbcd0de..49c727fc81 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Enable the rules that you would like to use. | | | [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md) | Enforces consistent naming for boolean props | | | | [react/button-has-type](docs/rules/button-has-type.md) | Forbid "button" element without an explicit "type" attribute | | | | [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md) | Enforce all defaultProps are defined and not "required" in propTypes. | -| | | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context | +| | 🔧 | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context | | ✔ | | [react/display-name](docs/rules/display-name.md) | Prevent missing displayName in a React component definition | | | | [react/forbid-component-props](docs/rules/forbid-component-props.md) | Forbid certain props on components | | | | [react/forbid-dom-props](docs/rules/forbid-dom-props.md) | Forbid certain props on DOM Nodes | diff --git a/docs/rules/destructuring-assignment.md b/docs/rules/destructuring-assignment.md index 622d348665..81236232e0 100644 --- a/docs/rules/destructuring-assignment.md +++ b/docs/rules/destructuring-assignment.md @@ -91,7 +91,7 @@ const Foo = class extends React.PureComponent { ```js ... -"react/destructuring-assignment": [, "always", { "ignoreClassFields": }] +"react/destructuring-assignment": [, "always", { "ignoreClassFields": , "destructAtParameter": "always" | "ignore" }] ... ``` @@ -104,3 +104,33 @@ class Foo extends React.PureComponent { bar = this.props.bar } ``` + +### `destructAtParameter` (default: "ignore") + +This option can be one of `always` or `ignore`. When configured with `always`, the rule will require props destructuring happens at function parameter. + +Examples of **incorrect** code for `destructAtParameter: 'always'` : + +```jsx +function Foo(props) { + const {a} = props; + return <>{a} +} +``` + +Examples of **correct** code for `destructAtParameter: 'always'` : + +```jsx +function Foo({a}) { + return <>{a} +} +``` + +```jsx +// Ignores when props is used elsewhere +function Foo(props) { + const {a} = props; + useProps(props); + return +} +``` diff --git a/lib/rules/destructuring-assignment.js b/lib/rules/destructuring-assignment.js index 8850df0d3d..bd020d41fc 100644 --- a/lib/rules/destructuring-assignment.js +++ b/lib/rules/destructuring-assignment.js @@ -50,6 +50,7 @@ const messages = { noDestructContextInSFCArg: 'Must never use destructuring context assignment in SFC argument', noDestructAssignment: 'Must never use destructuring {{type}} assignment', useDestructAssignment: 'Must use destructuring {{type}} assignment', + destructAtParameter: 'Must destruct props at function parameter.', }; module.exports = { @@ -60,7 +61,7 @@ module.exports = { recommended: false, url: docsUrl('destructuring-assignment'), }, - + fixable: 'code', messages, schema: [{ @@ -75,6 +76,13 @@ module.exports = { ignoreClassFields: { type: 'boolean', }, + destructAtParameter: { + type: 'string', + enum: [ + 'always', + 'ignore', + ], + }, }, additionalProperties: false, }], @@ -83,6 +91,7 @@ module.exports = { create: Components.detect((context, components, utils) => { const configuration = context.options[0] || DEFAULT_OPTION; const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false; + const destructAtParameter = (context.options[1] && context.options[1].destructAtParameter) || 'ignore'; const sfcParams = createSFCParams(); /** @@ -230,6 +239,35 @@ module.exports = { }, }); } + + if (SFCComponent && destructuringSFC && configuration === 'always' && destructAtParameter === 'always' + && node.init.name === 'props') { + const propsRefs = context.getScope().set.get('props') && context.getScope().set.get('props').references; + if (!propsRefs) { + return; + } + // Skip if props is used elsewhere + if (propsRefs.length > 1) { + return; + } + report(context, messages.destructAtParameter, 'destructAtParameter', { + node, + fix(fixer) { + const param = SFCComponent.node.params[0]; + if (!param) { + return; + } + const replaceRange = [ + param.range[0], + param.typeAnnotation ? param.typeAnnotation.range[0] : param.range[1], + ]; + return [ + fixer.replaceTextRange(replaceRange, context.getSourceCode().getText(node.id)), + fixer.remove(node.parent), + ]; + }, + }); + } }, }; }), diff --git a/tests/lib/rules/destructuring-assignment.js b/tests/lib/rules/destructuring-assignment.js index 6f4286958e..978fd5cca7 100644 --- a/tests/lib/rules/destructuring-assignment.js +++ b/tests/lib/rules/destructuring-assignment.js @@ -338,6 +338,24 @@ ruleTester.run('destructuring-assignment', rule, { } `, }, + { + code: ` + function Foo(props) { + const {a} = props; + return {a}; + } + `, + options: ['always', { destructAtParameter: 'always' }], + }, + { + code: ` + function Foo(props) { + const {a} = props; + return props}>{a}; + } + `, + options: ['always', { destructAtParameter: 'always' }], + }, ]), invalid: parsers.all([ @@ -632,7 +650,7 @@ ruleTester.run('destructuring-assignment', rule, { const TestComp = (props) => { props.onClick3102(); - + return (
{ @@ -720,5 +738,48 @@ ruleTester.run('destructuring-assignment', rule, { }, ], }, + { + code: ` + function Foo(props) { + const {a} = props; + return

{a}

; + } + `, + options: ['always', { destructAtParameter: 'always' }], + errors: [ + { + messageId: 'destructAtParameter', + line: 3, + }, + ], + output: ` + function Foo({a}) { + + return

{a}

; + } + `, + }, + { + code: ` + function Foo(props: FooProps) { + const {a} = props; + return

{a}

; + } + `, + options: ['always', { destructAtParameter: 'always' }], + errors: [ + { + messageId: 'destructAtParameter', + line: 3, + }, + ], + output: ` + function Foo({a}: FooProps) { + + return

{a}

; + } + `, + features: ['ts', 'no-babel'], + }, ]), });