Skip to content

Commit 4989a30

Browse files
committed
Update to support exact prop wrapper functions
1 parent 1819fd9 commit 4989a30

File tree

6 files changed

+252
-22
lines changed

6 files changed

+252
-22
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ You should also specify settings that will be shared across all the plugin rules
4646
// The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
4747
"forbidExtraProps",
4848
{"property": "freeze", "object": "Object"}
49-
{"property": "myFavoriteWrapper"}
49+
{"property": "myFavoriteWrapper"},
50+
// for rules that check exact prop wrappers
51+
{"property": "forbidExtraProps", "exact": true}
5052
]
5153
}
5254
}

docs/rules/prefer-exact-props.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ See the [Flow docs](https://flow.org/en/docs/types/objects/#toc-exact-object-typ
5050

5151
## Rule Details
5252

53+
This rule will only produce errors for prop types when combined with the appropriate entries in `propWrapperFunctions`. For example:
54+
55+
```json
56+
{
57+
"settings": {
58+
"propWrapperFunctions": [
59+
{"property": "exact", "exact": true}
60+
]
61+
}
62+
}
63+
```
64+
5365
The following patterns are considered warnings:
5466

5567
```jsx

lib/rules/prefer-exact-props.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
'use strict';
55

66
const Components = require('../util/Components');
7+
const docsUrl = require('../util/docsUrl');
78
const propsUtil = require('../util/props');
9+
const propWrapperUtil = require('../util/propWrapper');
810
const variableUtil = require('../util/variable');
911

10-
const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using prop-types-exact.';
12+
const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using {{exactPropWrappers}}.';
1113
const FLOW_MESSAGE = 'Component flow props should be set with exact objects.';
1214

1315
// -----------------------------------------------------------------------------
@@ -19,12 +21,21 @@ module.exports = {
1921
docs: {
2022
description: 'Prefer exact proptype definitions',
2123
category: 'Possible Errors',
22-
recommended: false
24+
recommended: false,
25+
url: docsUrl('prefer-exact-props')
2326
},
2427
schema: []
2528
},
2629

2730
create: Components.detect((context, components, utils) => {
31+
const exactWrappers = propWrapperUtil.getExactPropWrapperFunctions(context);
32+
33+
function getPropTypesErrorMessage() {
34+
const formattedWrappers = propWrapperUtil.formatPropWrapperFunctions(exactWrappers);
35+
const message = exactWrappers.size > 1 ? `one of ${formattedWrappers}` : formattedWrappers;
36+
return {exactPropWrappers: message};
37+
}
38+
2839
function isNonExactObjectTypeAnnotation(node) {
2940
return (
3041
node &&
@@ -71,10 +82,11 @@ module.exports = {
7182
node: node,
7283
message: FLOW_MESSAGE
7384
});
74-
} else if (isNonEmptyObjectExpression(node.value)) {
85+
} else if (isNonEmptyObjectExpression(node.value) && exactWrappers.size > 0) {
7586
context.report({
7687
node: node,
77-
message: PROP_TYPES_MESSAGE
88+
message: PROP_TYPES_MESSAGE,
89+
data: getPropTypesErrorMessage()
7890
});
7991
}
8092
},
@@ -83,6 +95,7 @@ module.exports = {
8395
if (!utils.getParentStatelessComponent(node)) {
8496
return;
8597
}
98+
8699
if (hasNonExactObjectTypeAnnotation(node)) {
87100
context.report({
88101
node: node,
@@ -101,23 +114,25 @@ module.exports = {
101114
},
102115

103116
MemberExpression: function(node) {
104-
if (!propsUtil.isPropTypesDeclaration(node)) {
117+
if (!propsUtil.isPropTypesDeclaration(node) || exactWrappers.size === 0) {
105118
return;
106119
}
107120

108121
const right = node.parent.right;
109122
if (isNonEmptyObjectExpression(right)) {
110123
context.report({
111124
node: node,
112-
message: PROP_TYPES_MESSAGE
125+
message: PROP_TYPES_MESSAGE,
126+
data: getPropTypesErrorMessage()
113127
});
114128
} else if (right.type === 'Identifier') {
115129
const identifier = right.name;
116130
const propsDefinition = variableUtil.findVariableByName(context, identifier);
117131
if (isNonEmptyObjectExpression(propsDefinition)) {
118132
context.report({
119133
node: node,
120-
message: PROP_TYPES_MESSAGE
134+
message: PROP_TYPES_MESSAGE,
135+
data: getPropTypesErrorMessage()
121136
});
122137
}
123138
}

lib/util/propWrapper.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
*/
44
'use strict';
55

6-
function getPropWrapperFunctions(context) {
7-
return new Set(context.settings.propWrapperFunctions || []);
8-
}
9-
10-
function isPropWrapperFunction(context, name) {
11-
const propWrapperFunctions = getPropWrapperFunctions(context);
6+
function searchPropWrapperFunctions(name, propWrapperFunctions) {
127
const splitName = name.split('.');
138
return Array.from(propWrapperFunctions).some(func => {
149
if (splitName.length === 2 && func.object === splitName[0] && func.property === splitName[1]) {
@@ -18,7 +13,41 @@ function isPropWrapperFunction(context, name) {
1813
});
1914
}
2015

16+
function getPropWrapperFunctions(context) {
17+
return new Set(context.settings.propWrapperFunctions || []);
18+
}
19+
20+
function isPropWrapperFunction(context, name) {
21+
const propWrapperFunctions = getPropWrapperFunctions(context);
22+
return searchPropWrapperFunctions(name, propWrapperFunctions);
23+
}
24+
25+
function getExactPropWrapperFunctions(context) {
26+
const propWrapperFunctions = getPropWrapperFunctions(context);
27+
const exactPropWrappers = Array.from(propWrapperFunctions).filter(func => func.exact === true);
28+
return new Set(exactPropWrappers);
29+
}
30+
31+
function isExactPropWrapperFunction(context, name) {
32+
const exactPropWrappers = getExactPropWrapperFunctions(context);
33+
return searchPropWrapperFunctions(name, exactPropWrappers);
34+
}
35+
36+
function formatPropWrapperFunctions(propWrapperFunctions) {
37+
return Array.from(propWrapperFunctions).map(func => {
38+
if (func.object && func.property) {
39+
return `'${func.object}.${func.property}'`;
40+
} else if (func.property) {
41+
return `'${func.property}'`;
42+
}
43+
return `'${func}'`;
44+
}).join(', ');
45+
}
46+
2147
module.exports = {
48+
formatPropWrapperFunctions: formatPropWrapperFunctions,
49+
getExactPropWrapperFunctions: getExactPropWrapperFunctions,
2250
getPropWrapperFunctions: getPropWrapperFunctions,
51+
isExactPropWrapperFunction: isExactPropWrapperFunction,
2352
isPropWrapperFunction: isPropWrapperFunction
2453
};

tests/lib/rules/prefer-exact-props.js

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ const parserOptions = {
1919
}
2020
};
2121

22-
const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using prop-types-exact.';
22+
const settings = {
23+
propWrapperFunctions: [
24+
{property: 'exact', exact: true}
25+
]
26+
};
27+
28+
const PROP_TYPES_MESSAGE = 'Component propTypes should be exact by using \'exact\'.';
2329
const FLOW_MESSAGE = 'Component flow props should be set with exact objects.';
2430

2531
const ruleTester = new RuleTester({parserOptions});
@@ -32,7 +38,8 @@ ruleTester.run('prefer-exact-props', rule, {
3238
}
3339
}
3440
Component.propTypes = {};
35-
`
41+
`,
42+
settings: settings
3643
}, {
3744
code: `
3845
class Component extends React.Component {
@@ -42,7 +49,8 @@ ruleTester.run('prefer-exact-props', rule, {
4249
}
4350
}
4451
`,
45-
parser: 'babel-eslint'
52+
parser: 'babel-eslint',
53+
settings: settings
4654
}, {
4755
code: `
4856
class Component extends React.Component {
@@ -52,14 +60,16 @@ ruleTester.run('prefer-exact-props', rule, {
5260
}
5361
}
5462
`,
55-
parser: 'babel-eslint'
63+
parser: 'babel-eslint',
64+
settings: settings
5665
}, {
5766
code: `
5867
function Component(props) {
5968
return <div />;
6069
}
6170
Component.propTypes = {};
62-
`
71+
`,
72+
settings: settings
6373
}, {
6474
code: `
6575
function Component(props: {}) {
@@ -107,7 +117,8 @@ ruleTester.run('prefer-exact-props', rule, {
107117
return <div />;
108118
}
109119
Component.propTypes = props;
110-
`
120+
`,
121+
settings: settings
111122
}, {
112123
code: `
113124
const props = {};
@@ -117,7 +128,8 @@ ruleTester.run('prefer-exact-props', rule, {
117128
}
118129
}
119130
Component.propTypes = props;
120-
`
131+
`,
132+
settings: settings
121133
}, {
122134
code: `
123135
import props from 'foo';
@@ -127,6 +139,52 @@ ruleTester.run('prefer-exact-props', rule, {
127139
}
128140
}
129141
Component.propTypes = props;
142+
`,
143+
settings: settings
144+
}, {
145+
code: `
146+
class Component extends React.Component {
147+
state = {hi: 'hi'}
148+
render() {
149+
return <div>{this.state.hi}</div>;
150+
}
151+
}
152+
`,
153+
parser: 'babel-eslint'
154+
}, {
155+
code: `
156+
import exact from "prop-types-exact";
157+
function Component({ foo, bar }) {
158+
return <div>{foo}{bar}</div>;
159+
}
160+
Component.propTypes = exact({
161+
foo: PropTypes.string,
162+
bar: PropTypes.string,
163+
});
164+
`,
165+
settings: settings
166+
}, {
167+
code: `
168+
function Component({ foo, bar }) {
169+
return <div>{foo}{bar}</div>;
170+
}
171+
Component.propTypes = {
172+
foo: PropTypes.string,
173+
bar: PropTypes.string,
174+
};
175+
`
176+
}, {
177+
code: `
178+
class Component extends React.Component {
179+
render() {
180+
const { foo, bar } = this.props;
181+
return <div>{foo}{bar}</div>;
182+
}
183+
}
184+
Component.propTypes = {
185+
foo: PropTypes.string,
186+
bar: PropTypes.string,
187+
};
130188
`
131189
}],
132190
invalid: [{
@@ -140,6 +198,7 @@ ruleTester.run('prefer-exact-props', rule, {
140198
foo: PropTypes.string
141199
};
142200
`,
201+
settings: settings,
143202
errors: [{message: PROP_TYPES_MESSAGE}]
144203
}, {
145204
code: `
@@ -152,6 +211,7 @@ ruleTester.run('prefer-exact-props', rule, {
152211
}
153212
}
154213
`,
214+
settings: settings,
155215
parser: 'babel-eslint',
156216
errors: [{message: PROP_TYPES_MESSAGE}]
157217
}, {
@@ -196,6 +256,7 @@ ruleTester.run('prefer-exact-props', rule, {
196256
}
197257
Component.propTypes = props;
198258
`,
259+
settings: settings,
199260
errors: [{message: PROP_TYPES_MESSAGE}]
200261
}, {
201262
code: `
@@ -209,6 +270,26 @@ ruleTester.run('prefer-exact-props', rule, {
209270
}
210271
Component.propTypes = props;
211272
`,
273+
settings: settings,
212274
errors: [{message: PROP_TYPES_MESSAGE}]
275+
}, {
276+
code: `
277+
const props = {
278+
foo: PropTypes.string
279+
};
280+
class Component extends React.Component {
281+
render() {
282+
return <div />;
283+
}
284+
}
285+
Component.propTypes = props;
286+
`,
287+
settings: {
288+
propWrapperFunctions: [
289+
{property: 'exact', exact: true},
290+
{property: 'forbidExtraProps', exact: true}
291+
]
292+
},
293+
errors: [{message: 'Component propTypes should be exact by using one of \'exact\', \'forbidExtraProps\'.'}]
213294
}]
214295
});

0 commit comments

Comments
 (0)