Skip to content

Add more support for wrapped propTypes #1272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ You can also specify some settings that will be shared across all the plugin rul
"createClass": "createReactClass", // Regex for Component Factory to use, default to "createReactClass"
"pragma": "React", // Pragma to use, default to "React"
"version": "15.0" // React version, default to the latest React stable release
}
},
"propWrapperFunctions": [ "forbidExtraProps" ] // The names of any functions used to wrap the propTypes object, such as `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
}
}
```
Expand Down
3 changes: 1 addition & 2 deletions docs/rules/no-unused-prop-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,13 @@ This rule can take one argument to ignore some specific props during validation.

```js
...
"react/no-unused-prop-types": [<enabled>, { customValidators: <customValidator>, skipShapeProps: <skipShapeProps>, propWrapperFunctions: <propWrapperFunctions> }]
"react/no-unused-prop-types": [<enabled>, { customValidators: <customValidator>, skipShapeProps: <skipShapeProps> }]
...
```

* `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
* `customValidators`: optional array of validators used for propTypes validation.
* `skipShapeProps`: In some cases it is impossible to accurately detect whether or not a `PropTypes.shape`'s values are being used. Setting this option to `true` will skip validation of `PropTypes.shape` (`true` by default).
* `propWrapperFunctions`: The names of any functions used to wrap the propTypes object, such as `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.

## Caveats

Expand Down
9 changes: 8 additions & 1 deletion lib/rules/default-props-match-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = {
create: Components.detect(function(context, components, utils) {
const configuration = context.options[0] || {};
const allowRequiredDefaults = configuration.allowRequiredDefaults || false;
var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);

/**
* Get properties name
Expand Down Expand Up @@ -113,7 +114,13 @@ module.exports = {
if (node.type === 'Identifier') {
return findVariableByName(node.name);
}

if (
node.type === 'CallExpression' &&
propWrapperFunctions.has(node.callee.name) &&
node.arguments && node.arguments[0]
) {
return node.arguments[0];
}
return node;
}

Expand Down
43 changes: 37 additions & 6 deletions lib/rules/forbid-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ module.exports = {
},

create: function(context) {
var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);

function isForbidden(type) {
var configuration = context.options[0] || {};

Expand Down Expand Up @@ -108,17 +110,46 @@ module.exports = {

return {
ClassProperty: function(node) {
if (isPropTypesDeclaration(node) && node.value && node.value.type === 'ObjectExpression') {
checkForbidden(node.value.properties);
if (!isPropTypesDeclaration(node)) {
return;
}
switch (node.value && node.value.type) {
case 'ObjectExpression':
checkForbidden(node.value.properties);
break;
case 'CallExpression':
if (
propWrapperFunctions.has(node.value.callee.name) &&
node.value.arguments && node.value.arguments[0]
) {
checkForbidden(node.value.arguments[0].properties);
}
break;
default:
break;
}
},

MemberExpression: function(node) {
if (isPropTypesDeclaration(node.property)) {
var right = node.parent.right;
if (right && right.type === 'ObjectExpression') {
if (!isPropTypesDeclaration(node.property)) {
return;
}

var right = node.parent.right;
switch (right && right.type) {
case 'ObjectExpression':
checkForbidden(right.properties);
}
break;
case 'CallExpression':
if (
propWrapperFunctions.has(right.callee.name) &&
right.arguments && right.arguments[0]
) {
checkForbidden(right.arguments[0].properties);
}
break;
default:
break;
}
},

Expand Down
9 changes: 1 addition & 8 deletions lib/rules/no-unused-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,6 @@ module.exports = {
},
skipShapeProps: {
type: 'boolean'
},
propWrapperFunctions: {
type: 'array',
items: {
type: 'string'
},
uniqueItems: true
}
},
additionalProperties: false
Expand All @@ -63,7 +56,7 @@ module.exports = {
var configuration = Object.assign({}, defaults, context.options[0] || {});
var skipShapeProps = configuration.skipShapeProps;
var customValidators = configuration.customValidators || [];
var propWrapperFunctions = new Set(configuration.propWrapperFunctions || []);
var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will context.settings always exist and be an object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I tested both with and without settings defined - context.settings will always be an object.


// Used to track the type annotations in scope.
// Necessary because babel's scopes do not track type annotations.
Expand Down
10 changes: 10 additions & 0 deletions lib/rules/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ module.exports = {
create: Components.detect(function(context, components, utils) {
var sourceCode = context.getSourceCode();
var configuration = context.options[0] || {};
var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
var ignored = configuration.ignore || [];
var customValidators = configuration.customValidators || [];
var skipUndeclared = configuration.skipUndeclared || false;
Expand Down Expand Up @@ -769,6 +770,15 @@ module.exports = {
}
ignorePropsValidation = true;
break;
case 'CallExpression':
if (
propWrapperFunctions.has(propTypes.callee.name) &&
propTypes.arguments && propTypes.arguments[0]
) {
markPropTypesAsDeclared(node, propTypes.arguments[0]);
return;
}
break;
case null:
break;
default:
Expand Down
8 changes: 8 additions & 0 deletions lib/rules/require-default-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {

create: Components.detect(function(context, components, utils) {
var sourceCode = context.getSourceCode();
var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);

/**
* Get properties name
Expand Down Expand Up @@ -105,6 +106,13 @@ module.exports = {
if (node.type === 'Identifier') {
return findVariableByName(node.name);
}
if (
node.type === 'CallExpression' &&
propWrapperFunctions.has(node.callee.name) &&
node.arguments && node.arguments[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be node.arguments.length > 0?

Copy link
Contributor Author

@dustinsoftware dustinsoftware Jun 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't matter, if arguments is empty, then arguments[0] will return undefined, which will cause this expression to evaluate false. It also handles the case where the first element of arguments contains something that is falsy :)

It looks like checking the arguments array this way also matches code style in other places in this project.

Copy link
Member

@ljharb ljharb Jun 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in v8, accessing an arguments index that's greater than length deoptimizes, so it'd be a performance issue - altho in this case it's not an arguments object, so it's probably fine :-)

) {
return node.arguments[0];
}

return node;
}
Expand Down
28 changes: 26 additions & 2 deletions lib/rules/sort-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module.exports = {
var requiredFirst = configuration.requiredFirst || false;
var callbacksLast = configuration.callbacksLast || false;
var ignoreCase = configuration.ignoreCase || false;
var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);

/**
* Checks if node is `propTypes` declaration
Expand Down Expand Up @@ -144,8 +145,23 @@ module.exports = {

return {
ClassProperty: function(node) {
if (isPropTypesDeclaration(node) && node.value && node.value.type === 'ObjectExpression') {
checkSorted(node.value.properties);
if (!isPropTypesDeclaration(node)) {
return;
}
switch (node.value && node.value.type) {
case 'ObjectExpression':
checkSorted(node.value.properties);
break;
case 'CallExpression':
if (
propWrapperFunctions.has(node.value.callee.name) &&
node.value.arguments && node.value.arguments[0]
) {
checkSorted(node.value.arguments[0].properties);
}
break;
default:
break;
}
},

Expand All @@ -156,6 +172,14 @@ module.exports = {
var right = node.parent.right;
var declarations;
switch (right && right.type) {
case 'CallExpression':
if (
propWrapperFunctions.has(right.callee.name) &&
right.arguments && right.arguments[0]
) {
declarations = right.arguments[0].properties;
}
break;
case 'ObjectExpression':
declarations = right.properties;
break;
Expand Down
41 changes: 41 additions & 0 deletions tests/lib/rules/default-props-match-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,28 @@ ruleTester.run('default-props-match-prop-types', rule, {
column: 3
}]
},
{
code: [
'function MyStatelessComponent({ foo, bar }) {',
' return <div>{foo}{bar}</div>;',
'}',
'MyStatelessComponent.propTypes = forbidExtraProps({',
' foo: React.PropTypes.string,',
' bar: React.PropTypes.string.isRequired',
'})',
'MyStatelessComponent.defaultProps = {',
' baz: "baz"',
'};'
].join('\n'),
settings: {
propWrapperFunctions: ['forbidExtraProps']
},
errors: [{
message: 'defaultProp "baz" has no corresponding propTypes declaration.',
line: 9,
column: 3
}]
},
{
code: [
'function MyStatelessComponent({ foo, bar }) {',
Expand Down Expand Up @@ -1219,6 +1241,25 @@ ruleTester.run('default-props-match-prop-types', rule, {
column: 5
}]
},
{
code: [
'function MyStatelessComponent({ foo, bar }) {',
' return <div>{foo}{bar}</div>;',
'}',
'MyStatelessComponent.propTypes = {',
' foo: React.PropTypes.string,',
' bar: React.PropTypes.string.isRequired',
'};',
'MyStatelessComponent.defaultProps = {',
' baz: "baz"',
'};'
].join('\n'),
errors: [{
message: 'defaultProp "baz" has no corresponding propTypes declaration.',
line: 9,
column: 3
}]
},
{
code: [
'class Greeting extends React.Component {',
Expand Down
32 changes: 32 additions & 0 deletions tests/lib/rules/forbid-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,21 @@ ruleTester.run('forbid-prop-types', rule, {
'};'
].join('\n'),
errors: 4
}, {
code: [
'class First extends React.Component {',
' render() {',
' return <div />;',
' }',
'}',
'First.propTypes = forbidExtraProps({',
' a: PropTypes.array',
'});'
].join('\n'),
errors: 1,
settings: {
propWrapperFunctions: ['forbidExtraProps']
}
}, {
code: [
'class Component extends React.Component {',
Expand All @@ -397,6 +412,23 @@ ruleTester.run('forbid-prop-types', rule, {
].join('\n'),
parser: 'babel-eslint',
errors: 2
}, {
code: [
'class Component extends React.Component {',
' static propTypes = forbidExtraProps({',
' a: PropTypes.array,',
' o: PropTypes.object',
' });',
' render() {',
' return <div />;',
' }',
'}'
].join('\n'),
parser: 'babel-eslint',
errors: 2,
settings: {
propWrapperFunctions: ['forbidExtraProps']
}
}, {
code: [
'var Hello = createReactClass({',
Expand Down
8 changes: 6 additions & 2 deletions tests/lib/rules/no-unused-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -2798,7 +2798,9 @@ ruleTester.run('no-unused-prop-types', rule, {
line: 10,
column: 8
}],
options: [{propWrapperFunctions: ['forbidExtraProps']}]
settings: {
propWrapperFunctions: ['forbidExtraProps']
}
}, {
code: [
'class Hello extends Component {',
Expand All @@ -2819,7 +2821,9 @@ ruleTester.run('no-unused-prop-types', rule, {
line: 4,
column: 10
}],
options: [{propWrapperFunctions: ['forbidExtraProps']}]
settings: {
propWrapperFunctions: ['forbidExtraProps']
}
}
/* , {
// Enable this when the following issue is fixed
Expand Down
Loading