Skip to content

Commit 24621e4

Browse files
nix6839ljharb
authored andcommitted
[New] require-default-props: add option functions
1 parent 5554bd4 commit 24621e4

File tree

4 files changed

+614
-24
lines changed

4 files changed

+614
-24
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
* [`jsx-tag-spacing`]: Add `multiline-always` option ([#3260][] @Nokel81)
1212
* [`function-component-definition`]: replace `var` by `const` in certain situations ([#3248][] @JohnBerd @SimeonC)
1313
* add [`jsx-no-leaked-render`] ([#3203][] @Belco90)
14+
* [`require-default-props`]: add option `functions` ([#3249][] @nix6839)
1415

1516
### Fixed
1617
* [`hook-use-state`]: Allow UPPERCASE setState setter prefixes ([#3244][] @duncanbeevers)
@@ -41,11 +42,12 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
4142
[#3258]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3258
4243
[#3254]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3254
4344
[#3251]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3251
44-
[#3203]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3203
45+
[#3249]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3249
4546
[#3248]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3248
4647
[#3244]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3244
4748
[#3235]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3235
4849
[#3230]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3230
50+
[#3203]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3203
4951

5052
## [7.29.4] - 2022.03.13
5153

docs/rules/require-default-props.md

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ HelloWorld.defaultProps = {
3636

3737
// Logs:
3838
// Invalid prop `name` of type `string` supplied to `HelloWorld`, expected `object`.
39-
ReactDOM.render(<HelloWorld />, document.getElementById('app'));
39+
ReactDOM.render(<HelloWorld />, document.getElementById('app'));
4040
```
4141

4242
Without `defaultProps`:
@@ -55,7 +55,7 @@ HelloWorld.propTypes = {
5555

5656
// Nothing is logged, renders:
5757
// "Hello,!"
58-
ReactDOM.render(<HelloWorld />, document.getElementById('app'));
58+
ReactDOM.render(<HelloWorld />, document.getElementById('app'));
5959
```
6060

6161
## Rule Details
@@ -197,13 +197,21 @@ NotAComponent.propTypes = {
197197

198198
```js
199199
...
200-
"react/require-default-props": [<enabled>, { forbidDefaultForRequired: <boolean>, ignoreFunctionalComponents: <boolean> }]
200+
"react/require-default-props": [<enabled>, {
201+
"forbidDefaultForRequired": <boolean>,
202+
"classes": "defaultProps | "ignore",
203+
"functions": "defaultProps" | "defaultArguments" | "ignore"
204+
// @deprecated Use `functions` option instead.
205+
"ignoreFunctionalComponents": <boolean>,
206+
}]
201207
...
202208
```
203209
204-
* `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
205-
* `forbidDefaultForRequired`: optional boolean to forbid prop default for a required prop. Defaults to false.
206-
* `ignoreFunctionalComponents`: optional boolean to ignore this rule for functional components. Defaults to false.
210+
- `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
211+
- `forbidDefaultForRequired`: optional boolean to forbid prop default for a required prop. Defaults to false.
212+
- `classes`: optional string to determine which strategy a class component uses for defaulting props. Defaults to "defaultProps".
213+
- `functions`: optional string to determine which strategy a functional component uses for defaulting props. Defaults to "defaultProps".
214+
- `ignoreFunctionalComponents`: optional boolean to ignore this rule for functional components. Defaults to false. Deprecated, use `functions` instead.
207215
208216
### `forbidDefaultForRequired`
209217
@@ -279,9 +287,129 @@ MyStatelessComponent.propTypes = {
279287
};
280288
```
281289
290+
### `classes`
291+
292+
- "defaultProps": Use `.defaultProps`. It's default.
293+
- "ignore": Ignore this rule for class components.
294+
295+
Examples of **incorrect** code for this rule, when set to `defaultProps`:
296+
297+
```jsx
298+
class Greeting extends React.Component {
299+
render() {
300+
return (
301+
<h1>Hello, {this.props.foo} {this.props.bar}</h1>
302+
);
303+
}
304+
305+
static propTypes = {
306+
foo: PropTypes.string,
307+
bar: PropTypes.string.isRequired
308+
};
309+
}
310+
```
311+
312+
Examples of **correct** code for this rule, when set to `defaultProps`:
313+
314+
```jsx
315+
class Greeting extends React.Component {
316+
render() {
317+
return (
318+
<h1>Hello, {this.props.foo} {this.props.bar}</h1>
319+
);
320+
}
321+
322+
static propTypes = {
323+
foo: PropTypes.string,
324+
bar: PropTypes.string.isRequired
325+
};
326+
327+
static defaultProps = {
328+
foo: "foo"
329+
};
330+
}
331+
```
332+
333+
### `functions`
334+
335+
- "defaultProps": Use `.defaultProps`. It's default.
336+
- "defaultArguments": Use default arguments in the function signature.
337+
- "ignore": Ignore this rule for functional components.
338+
339+
Examples of **incorrect** code for this rule, when set to `defaultArguments`:
340+
341+
```jsx
342+
function Hello({ foo }) {
343+
return <div>{foo}</div>;
344+
}
345+
346+
Hello.propTypes = {
347+
foo: PropTypes.string
348+
};
349+
Hello.defaultProps = {
350+
foo: 'foo'
351+
}
352+
```
353+
354+
```jsx
355+
function Hello({ foo = 'foo' }) {
356+
return <div>{foo}</div>;
357+
}
358+
359+
Hello.propTypes = {
360+
foo: PropTypes.string
361+
};
362+
Hello.defaultProps = {
363+
foo: 'foo'
364+
}
365+
```
366+
367+
```jsx
368+
function Hello(props) {
369+
return <div>{props.foo}</div>;
370+
}
371+
372+
Hello.propTypes = {
373+
foo: PropTypes.string
374+
};
375+
```
376+
377+
Examples of **correct** code for this rule, when set to `defaultArguments`:
378+
379+
```jsx
380+
function Hello({ foo = 'foo' }) {
381+
return <div>{foo}</div>;
382+
}
383+
384+
Hello.propTypes = {
385+
foo: PropTypes.string
386+
};
387+
```
388+
389+
```jsx
390+
function Hello({ foo }) {
391+
return <div>{foo}</div>;
392+
}
393+
394+
Hello.propTypes = {
395+
foo: PropTypes.string.isRequired
396+
};
397+
```
398+
399+
```jsx
400+
function Hello(props) {
401+
return <div>{props.foo}</div>;
402+
}
403+
404+
Hello.propTypes = {
405+
foo: PropTypes.string.isRequired
406+
};
407+
```
408+
282409
### `ignoreFunctionalComponents`
283410
284411
When set to `true`, ignores this rule for all functional components.
412+
**Deprecated**, use `functions` instead.
285413
286414
Examples of **incorrect** code for this rule:
287415
@@ -345,6 +473,7 @@ MyStatelessComponent.propTypes = {
345473
If you don't care about using `defaultProps` for your component's props that are not required, you can disable this rule.
346474
347475
# Resources
476+
348477
- [Official React documentation on defaultProps](https://facebook.github.io/react/docs/typechecking-with-proptypes.html#default-prop-values)
349478
350479
[PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html

lib/rules/require-default-props.js

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
'use strict';
77

8+
const entries = require('object.entries');
9+
const values = require('object.values');
810
const Components = require('../util/Components');
911
const docsUrl = require('../util/docsUrl');
1012
const astUtil = require('../util/ast');
@@ -17,6 +19,9 @@ const report = require('../util/report');
1719
const messages = {
1820
noDefaultWithRequired: 'propType "{{name}}" is required and should not have a defaultProps declaration.',
1921
shouldHaveDefault: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.',
22+
noDefaultPropsWithFunction: 'Don’t use defaultProps with function components.',
23+
shouldAssignObjectDefault: 'propType "{{name}}" is not required, but has no corresponding default argument value.',
24+
destructureInSignature: 'Must destructure props in the function signature to initialize an optional prop.',
2025
};
2126

2227
module.exports = {
@@ -35,6 +40,19 @@ module.exports = {
3540
forbidDefaultForRequired: {
3641
type: 'boolean',
3742
},
43+
classes: {
44+
allow: {
45+
enum: ['defaultProps', 'ignore'],
46+
},
47+
},
48+
functions: {
49+
allow: {
50+
enum: ['defaultArguments', 'defaultProps', 'ignore'],
51+
},
52+
},
53+
/**
54+
* @deprecated
55+
*/
3856
ignoreFunctionalComponents: {
3957
type: 'boolean',
4058
},
@@ -46,7 +64,15 @@ module.exports = {
4664
create: Components.detect((context, components) => {
4765
const configuration = context.options[0] || {};
4866
const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false;
49-
const ignoreFunctionalComponents = configuration.ignoreFunctionalComponents || false;
67+
const classes = configuration.classes || 'defaultProps';
68+
/**
69+
* @todo
70+
* - Remove ignoreFunctionalComponents
71+
* - Change default to 'defaultArguments'
72+
*/
73+
const functions = configuration.ignoreFunctionalComponents
74+
? 'ignore'
75+
: configuration.functions || 'defaultProps';
5076

5177
/**
5278
* Reports all propTypes passed in that don't have a defaultProps counterpart.
@@ -55,14 +81,10 @@ module.exports = {
5581
* @return {void}
5682
*/
5783
function reportPropTypesWithoutDefault(propTypes, defaultProps) {
58-
// If this defaultProps is "unresolved", then we should ignore this component and not report
59-
// any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators.
60-
if (defaultProps === 'unresolved') {
61-
return;
62-
}
84+
entries(propTypes).forEach((propType) => {
85+
const propName = propType[0];
86+
const prop = propType[1];
6387

64-
Object.keys(propTypes).forEach((propName) => {
65-
const prop = propTypes[propName];
6688
if (!prop.node) {
6789
return;
6890
}
@@ -87,6 +109,48 @@ module.exports = {
87109
});
88110
}
89111

112+
/**
113+
* If functions option is 'defaultArguments', reports defaultProps is used and all params that doesn't initialized.
114+
* @param {Object} componentNode Node of component.
115+
* @param {Object[]} declaredPropTypes List of propTypes to check `isRequired`.
116+
* @param {Object} defaultProps Object of defaultProps to check used.
117+
*/
118+
function reportFunctionComponent(componentNode, declaredPropTypes, defaultProps) {
119+
if (defaultProps) {
120+
report(context, messages.noDefaultPropsWithFunction, 'noDefaultPropsWithFunction', {
121+
node: componentNode,
122+
});
123+
}
124+
125+
const props = componentNode.params[0];
126+
const propTypes = declaredPropTypes;
127+
128+
if (props.type === 'Identifier') {
129+
const hasOptionalProp = values(propTypes).some((propType) => !propType.isRequired);
130+
if (hasOptionalProp) {
131+
report(context, messages.destructureInSignature, 'destructureInSignature', {
132+
node: props,
133+
});
134+
}
135+
} else if (props.type === 'ObjectPattern') {
136+
props.properties.filter((prop) => {
137+
if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
138+
return false;
139+
}
140+
const propType = propTypes[prop.key.name];
141+
if (!propType || propType.isRequired) {
142+
return false;
143+
}
144+
return prop.value.type !== 'AssignmentPattern';
145+
}).forEach((prop) => {
146+
report(context, messages.shouldAssignObjectDefault, 'shouldAssignObjectDefault', {
147+
node: prop,
148+
data: { name: prop.key.name },
149+
});
150+
});
151+
}
152+
}
153+
90154
// --------------------------------------------------------------------------
91155
// Public API
92156
// --------------------------------------------------------------------------
@@ -95,17 +159,33 @@ module.exports = {
95159
'Program:exit'() {
96160
const list = components.list();
97161

98-
Object.keys(list).filter((component) => {
99-
if (ignoreFunctionalComponents
100-
&& (astUtil.isFunction(list[component].node) || astUtil.isFunctionLikeExpression(list[component].node))) {
162+
values(list).filter((component) => {
163+
if (functions === 'ignore' && astUtil.isFunctionLike(component.node)) {
101164
return false;
102165
}
103-
return list[component].declaredPropTypes;
166+
if (classes === 'ignore' && astUtil.isClass(component.node)) {
167+
return false;
168+
}
169+
170+
// If this defaultProps is "unresolved", then we should ignore this component and not report
171+
// any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators.
172+
if (component.defaultProps === 'unresolved') {
173+
return false;
174+
}
175+
return component.declaredPropTypes !== undefined;
104176
}).forEach((component) => {
105-
reportPropTypesWithoutDefault(
106-
list[component].declaredPropTypes,
107-
list[component].defaultProps || {}
108-
);
177+
if (functions === 'defaultArguments' && astUtil.isFunctionLike(component.node)) {
178+
reportFunctionComponent(
179+
component.node,
180+
component.declaredPropTypes,
181+
component.defaultProps
182+
);
183+
} else {
184+
reportPropTypesWithoutDefault(
185+
component.declaredPropTypes,
186+
component.defaultProps || {}
187+
);
188+
}
109189
});
110190
},
111191
};

0 commit comments

Comments
 (0)