Skip to content

Commit a72ed42

Browse files
committed
Merge remote-tracking branch 'upstream/master' into zach/unstable-memo-message
2 parents bd7e407 + 567bc7d commit a72ed42

14 files changed

+529
-37
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ 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+
11+
### Fixed
12+
* [`hook-use-state`]: Allow UPPERCASE setState setter prefixes ([#3244][] @duncanbeevers)
13+
* `propTypes`: add `VFC` to react generic type param map ([#3230][] @dlech)
14+
15+
[#3244]: https://github.com/yannickcr/eslint-plugin-react/pull/3244
16+
[#3235]: https://github.com/yannickcr/eslint-plugin-react/pull/3235
17+
[#3230]: https://github.com/yannickcr/eslint-plugin-react/issues/3230
18+
19+
## [7.29.4] - 2022.03.13
20+
21+
### Fixed
22+
* [`no-unused-state`]: avoid a crash on a class field gDSFP ([#3236][] @ljharb)
23+
* [`boolean-prop-naming`]: handle React.FC, intersection, union types ([#3241][] @ljharb)
24+
25+
[7.29.4]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.29.3...v7.29.4
26+
[#3241]: https://github.com/yannickcr/eslint-plugin-react/pull/3241
27+
[#3236]: https://github.com/yannickcr/eslint-plugin-react/issues/3236
28+
829
## [7.29.3] - 2022.03.03
930

1031
### Fixed

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/boolean-prop-naming.js

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,65 @@ module.exports = {
228228
args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
229229
}
230230

231+
function getComponentTypeAnnotation(component) {
232+
// If this is a functional component that uses a global type, check it
233+
if (
234+
(component.node.type === 'FunctionDeclaration' || component.node.type === 'ArrowFunctionExpression')
235+
&& component.node.params
236+
&& component.node.params.length > 0
237+
&& component.node.params[0].typeAnnotation
238+
) {
239+
return component.node.params[0].typeAnnotation.typeAnnotation;
240+
}
241+
242+
if (
243+
component.node.parent
244+
&& component.node.parent.type === 'VariableDeclarator'
245+
&& component.node.parent.id
246+
&& component.node.parent.id.type === 'Identifier'
247+
&& component.node.parent.id.typeAnnotation
248+
&& component.node.parent.id.typeAnnotation.typeAnnotation
249+
&& component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters
250+
&& (
251+
component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation'
252+
|| component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.type === 'TypeParameterInstantiation'
253+
)
254+
) {
255+
return component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters.params.find(
256+
(param) => param.type === 'TSTypeReference' || param.type === 'GenericTypeAnnotation'
257+
);
258+
}
259+
}
260+
261+
function findAllTypeAnnotations(identifier, node) {
262+
if (node.type === 'TSTypeLiteral' || node.type === 'ObjectTypeAnnotation') {
263+
const currentNode = [].concat(
264+
objectTypeAnnotations.get(identifier.name) || [],
265+
node
266+
);
267+
objectTypeAnnotations.set(identifier.name, currentNode);
268+
} else if (
269+
node.type === 'TSParenthesizedType'
270+
&& (
271+
node.typeAnnotation.type === 'TSIntersectionType'
272+
|| node.typeAnnotation.type === 'TSUnionType'
273+
)
274+
) {
275+
node.typeAnnotation.types.forEach((type) => {
276+
findAllTypeAnnotations(identifier, type);
277+
});
278+
} else if (
279+
node.type === 'TSIntersectionType'
280+
|| node.type === 'TSUnionType'
281+
|| node.type === 'IntersectionTypeAnnotation'
282+
|| node.type === 'UnionTypeAnnotation'
283+
) {
284+
node.types.forEach((type) => {
285+
findAllTypeAnnotations(identifier, type);
286+
});
287+
}
288+
}
289+
231290
// --------------------------------------------------------------------------
232291
// Public
233292
// --------------------------------------------------------------------------
@@ -292,16 +351,11 @@ module.exports = {
292351
},
293352

294353
TypeAlias(node) {
295-
// Cache all ObjectType annotations, we will check them at the end
296-
if (node.right.type === 'ObjectTypeAnnotation') {
297-
objectTypeAnnotations.set(node.id.name, node.right);
298-
}
354+
findAllTypeAnnotations(node.id, node.right);
299355
},
300356

301357
TSTypeAliasDeclaration(node) {
302-
if (node.typeAnnotation.type === 'TSTypeLiteral') {
303-
objectTypeAnnotations.set(node.id.name, node.typeAnnotation);
304-
}
358+
findAllTypeAnnotations(node.id, node.typeAnnotation);
305359
},
306360

307361
// eslint-disable-next-line object-shorthand
@@ -311,19 +365,11 @@ module.exports = {
311365
}
312366

313367
const list = components.list();
368+
314369
Object.keys(list).forEach((component) => {
315-
// If this is a functional component that uses a global type, check it
316-
if (
317-
(
318-
list[component].node.type === 'FunctionDeclaration'
319-
|| list[component].node.type === 'ArrowFunctionExpression'
320-
)
321-
&& list[component].node.params
322-
&& list[component].node.params.length
323-
&& list[component].node.params[0].typeAnnotation
324-
) {
325-
const typeNode = list[component].node.params[0].typeAnnotation;
326-
const annotation = typeNode.typeAnnotation;
370+
const annotation = getComponentTypeAnnotation(list[component]);
371+
372+
if (annotation) {
327373
let propType;
328374
if (annotation.type === 'GenericTypeAnnotation') {
329375
propType = objectTypeAnnotations.get(annotation.id.name);
@@ -334,10 +380,12 @@ module.exports = {
334380
}
335381

336382
if (propType) {
337-
validatePropNaming(
338-
list[component].node,
339-
propType.properties || propType.members
340-
);
383+
[].concat(propType).forEach((prop) => {
384+
validatePropNaming(
385+
list[component].node,
386+
prop.properties || prop.members
387+
);
388+
});
341389
}
342390
}
343391

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
}),

lib/rules/hook-use-state.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,31 @@ module.exports = {
6666
? setterVariable.name
6767
: undefined;
6868

69-
const expectedSetterVariableName = valueVariableName ? (
70-
`set${valueVariableName.charAt(0).toUpperCase()}${valueVariableName.slice(1)}`
71-
) : undefined;
69+
const caseCandidateMatch = valueVariableName ? valueVariableName.match(/(^[a-z]+)(.*)/) : undefined;
70+
const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch[1] : undefined;
71+
const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch[2] : undefined;
72+
const expectedSetterVariableNames = upperCaseCandidatePrefix ? [
73+
`set${upperCaseCandidatePrefix.charAt(0).toUpperCase()}${upperCaseCandidatePrefix.slice(1)}${caseCandidateSuffix}`,
74+
`set${upperCaseCandidatePrefix.toUpperCase()}${caseCandidateSuffix}`,
75+
] : [];
7276

7377
const isSymmetricGetterSetterPair = valueVariable
7478
&& setterVariable
75-
&& setterVariableName === expectedSetterVariableName
79+
&& expectedSetterVariableNames.indexOf(setterVariableName) !== -1
7680
&& variableNodes.length === 2;
7781

7882
if (!isSymmetricGetterSetterPair) {
7983
const suggestions = [
8084
{
8185
desc: 'Destructure useState call into value + setter pair',
8286
fix: (fixer) => {
87+
if (expectedSetterVariableNames.length === 0) {
88+
return;
89+
}
90+
8391
const fix = fixer.replaceTextRange(
8492
node.parent.id.range,
85-
`[${valueVariableName}, ${expectedSetterVariableName}]`
93+
`[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
8694
);
8795

8896
return fix;

lib/rules/no-unused-state.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ module.exports = {
249249
!node.static
250250
|| name !== 'getDerivedStateFromProps'
251251
|| !node.value
252+
|| !node.value.params
252253
|| node.value.params.length < 2 // no `state` argument
253254
) {
254255
return false;

lib/util/propTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ module.exports = function propTypesInstructions(context, components, utils) {
105105
ForwardRefRenderFunction: 1,
106106
forwardRef: 1,
107107
VoidFunctionComponent: 0,
108+
VFC: 0,
108109
PropsWithChildren: 0,
109110
SFC: 0,
110111
StatelessComponent: 0,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-react",
3-
"version": "7.29.3",
3+
"version": "7.29.4",
44
"author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
55
"description": "React specific linting rules for ESLint",
66
"main": "index.js",

0 commit comments

Comments
 (0)