From 4df159b2ffde555ab5a1d1c9497374e8e5247b53 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Fri, 23 Jun 2017 19:28:25 -0700 Subject: [PATCH 1/7] Add support for wrapped propTypes to require-default-props --- lib/rules/require-default-props.js | 23 ++++++++++++++++++++++- tests/lib/rules/require-default-props.js | 17 +++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index 19a9c52fd0..c0328b9767 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -22,11 +22,25 @@ module.exports = { category: 'Best Practices' }, - schema: [] + schema: [{ + type: 'object', + properties: { + propWrapperFunctions: { + type: 'array', + items: { + type: 'string' + }, + uniqueItems: true + } + }, + additionalProperties: false + }] }, create: Components.detect(function(context, components, utils) { var sourceCode = context.getSourceCode(); + var configuration = context.options[0] || {}; + var propWrapperFunctions = new Set(configuration.propWrapperFunctions || []); /** * Get properties name @@ -105,6 +119,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; } diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js index ca6efc8dd4..bbb088bd30 100644 --- a/tests/lib/rules/require-default-props.js +++ b/tests/lib/rules/require-default-props.js @@ -757,6 +757,23 @@ ruleTester.run('require-default-props', rule, { column: 3 }] }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = forbidExtraProps({', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '});' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 5, + column: 3 + }], + options: [{propWrapperFunctions: ['forbidExtraProps']}] + }, { code: [ 'function MyStatelessComponent({ foo, bar }) {', From b6e3a2f7c0ad08b817468b63a855f04c697f1d55 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Fri, 23 Jun 2017 20:19:49 -0700 Subject: [PATCH 2/7] Move propWrapperFunctions into settings --- docs/rules/no-unused-prop-types.md | 3 +-- lib/rules/no-unused-prop-types.js | 9 +-------- lib/rules/require-default-props.js | 17 ++--------------- tests/lib/rules/no-unused-prop-types.js | 8 ++++++-- tests/lib/rules/require-default-props.js | 4 +++- 5 files changed, 13 insertions(+), 28 deletions(-) diff --git a/docs/rules/no-unused-prop-types.md b/docs/rules/no-unused-prop-types.md index f39eaf68e3..52e50bc6ed 100644 --- a/docs/rules/no-unused-prop-types.md +++ b/docs/rules/no-unused-prop-types.md @@ -47,14 +47,13 @@ This rule can take one argument to ignore some specific props during validation. ```js ... -"react/no-unused-prop-types": [, { customValidators: , skipShapeProps: , propWrapperFunctions: }] +"react/no-unused-prop-types": [, { customValidators: , 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 diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index afcb4df6c6..e4a3931f74 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -44,13 +44,6 @@ module.exports = { }, skipShapeProps: { type: 'boolean' - }, - propWrapperFunctions: { - type: 'array', - items: { - type: 'string' - }, - uniqueItems: true } }, additionalProperties: false @@ -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 || []); // Used to track the type annotations in scope. // Necessary because babel's scopes do not track type annotations. diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index c0328b9767..9364cf0fd6 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -22,25 +22,12 @@ module.exports = { category: 'Best Practices' }, - schema: [{ - type: 'object', - properties: { - propWrapperFunctions: { - type: 'array', - items: { - type: 'string' - }, - uniqueItems: true - } - }, - additionalProperties: false - }] + schema: [] }, create: Components.detect(function(context, components, utils) { var sourceCode = context.getSourceCode(); - var configuration = context.options[0] || {}; - var propWrapperFunctions = new Set(configuration.propWrapperFunctions || []); + var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []); /** * Get properties name diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index c74585487b..a028683614 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -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 {', @@ -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 diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js index bbb088bd30..8ed8f7d37a 100644 --- a/tests/lib/rules/require-default-props.js +++ b/tests/lib/rules/require-default-props.js @@ -772,7 +772,9 @@ ruleTester.run('require-default-props', rule, { line: 5, column: 3 }], - options: [{propWrapperFunctions: ['forbidExtraProps']}] + settings: { + propWrapperFunctions: ['forbidExtraProps'] + } }, { code: [ From b25d7910b13d7f0b8775a9961bd9e15824997a5c Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Fri, 23 Jun 2017 21:03:32 -0700 Subject: [PATCH 3/7] Add support for wrapped propTypes to sort-prop-types --- lib/rules/sort-prop-types.js | 28 +++++++++++++++++-- tests/lib/rules/sort-prop-types.js | 44 ++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js index d05ec22058..c3eabddbc6 100644 --- a/lib/rules/sort-prop-types.js +++ b/lib/rules/sort-prop-types.js @@ -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 @@ -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; } }, @@ -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; diff --git a/tests/lib/rules/sort-prop-types.js b/tests/lib/rules/sort-prop-types.js index cd8996bbd6..9fcc7a6c55 100644 --- a/tests/lib/rules/sort-prop-types.js +++ b/tests/lib/rules/sort-prop-types.js @@ -449,6 +449,24 @@ ruleTester.run('sort-prop-types', rule, { ].join('\n'), parser: 'babel-eslint', errors: 2 + }, { + code: [ + 'class Component extends React.Component {', + ' static propTypes = forbidExtraProps({', + ' z: PropTypes.any,', + ' y: PropTypes.any,', + ' a: PropTypes.any', + ' });', + ' render() {', + ' return
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + settings: { + propWrapperFunctions: ['forbidExtraProps'] + }, + errors: 2 }, { code: [ 'var First = createReactClass({', @@ -519,6 +537,32 @@ ruleTester.run('sort-prop-types', rule, { column: 5, type: 'Property' }] + }, { + code: [ + 'class First extends React.Component {', + ' render() {', + ' return
;', + ' }', + '}', + 'First.propTypes = forbidExtraProps({', + ' a: PropTypes.any,', + ' z: PropTypes.string,', + ' onFoo: PropTypes.func,', + ' onBar: PropTypes.func', + '});' + ].join('\n'), + options: [{ + callbacksLast: true + }], + settings: { + propWrapperFunctions: ['forbidExtraProps'] + }, + errors: [{ + message: ERROR_MESSAGE, + line: 10, + column: 5, + type: 'Property' + }] }, { code: [ 'var First = createReactClass({', From 55e496dd4e2a096e42cca3f85260d6b27b43d808 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Fri, 23 Jun 2017 21:14:13 -0700 Subject: [PATCH 4/7] Add support for wrapped propTypes to forbid-prop-types --- lib/rules/forbid-prop-types.js | 43 ++++++++++++++++++++++++---- tests/lib/rules/forbid-prop-types.js | 32 +++++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/lib/rules/forbid-prop-types.js b/lib/rules/forbid-prop-types.js index 3d8296d3fb..20718fd652 100644 --- a/lib/rules/forbid-prop-types.js +++ b/lib/rules/forbid-prop-types.js @@ -36,6 +36,8 @@ module.exports = { }, create: function(context) { + var propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []); + function isForbidden(type) { var configuration = context.options[0] || {}; @@ -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; } }, diff --git a/tests/lib/rules/forbid-prop-types.js b/tests/lib/rules/forbid-prop-types.js index 53aa736e40..3cd9f2539b 100644 --- a/tests/lib/rules/forbid-prop-types.js +++ b/tests/lib/rules/forbid-prop-types.js @@ -383,6 +383,21 @@ ruleTester.run('forbid-prop-types', rule, { '};' ].join('\n'), errors: 4 + }, { + code: [ + 'class First extends React.Component {', + ' render() {', + ' return
;', + ' }', + '}', + 'First.propTypes = forbidExtraProps({', + ' a: PropTypes.array', + '});' + ].join('\n'), + errors: 1, + settings: { + propWrapperFunctions: ['forbidExtraProps'] + } }, { code: [ 'class Component extends React.Component {', @@ -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
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: 2, + settings: { + propWrapperFunctions: ['forbidExtraProps'] + } }, { code: [ 'var Hello = createReactClass({', From bb97fc894ccf28d5fa063ef45ab56f86855919c2 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sat, 24 Jun 2017 07:14:35 -0700 Subject: [PATCH 5/7] Update documentation --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1839668232..2735746b75 100644 --- a/README.md +++ b/README.md @@ -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. } } ``` From eda7e1ddf8209e6285b12269a9f2aa9ba522a2cb Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sat, 24 Jun 2017 21:04:56 -0700 Subject: [PATCH 6/7] Add support for wrapped propTypes to prop-types --- lib/rules/prop-types.js | 10 ++++++++ tests/lib/rules/prop-types.js | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index 17809767a5..07daffe1f9 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -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; @@ -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: diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 95194d938c..e5d1138463 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -2208,6 +2208,26 @@ ruleTester.run('prop-types', rule, { errors: [ {message: '\'lastname\' is missing in props validation'} ] + }, { + code: [ + 'class Test extends Foo.Component {', + ' render() {', + ' return (', + '
{this.props.firstname} {this.props.lastname}
', + ' );', + ' }', + '}', + 'Test.propTypes = forbidExtraProps({', + ' firstname: PropTypes.string', + '});' + ].join('\n'), + parser: 'babel-eslint', + settings: Object.assign({}, settings, { + propWrapperFunctions: ['forbidExtraProps'] + }), + errors: [ + {message: '\'lastname\' is missing in props validation'} + ] }, { code: [ '/** @jsx Foo */', @@ -2689,6 +2709,29 @@ ruleTester.run('prop-types', rule, { errors: [ {message: '\'foo\' is missing in props validation'} ] + }, { + code: [ + 'class Hello extends Component {', + ' static propTypes = forbidExtraProps({', + ' bar: PropTypes.func', + ' })', + ' componentWillReceiveProps(nextProps) {', + ' if (nextProps.foo) {', + ' return;', + ' }', + ' }', + ' render() {', + ' return
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + settings: Object.assign({}, settings, { + propWrapperFunctions: ['forbidExtraProps'] + }), + errors: [ + {message: '\'foo\' is missing in props validation'} + ] }, { code: [ 'class Hello extends Component {', From 412f44c73fce776111535048a4a9f9327dfc126f Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sat, 24 Jun 2017 21:20:44 -0700 Subject: [PATCH 7/7] Add support for wrapped propTypes to default-props-match-prop-types --- lib/rules/default-props-match-prop-types.js | 9 +++- .../rules/default-props-match-prop-types.js | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/rules/default-props-match-prop-types.js b/lib/rules/default-props-match-prop-types.js index 992fd31d21..0ccdc8e080 100644 --- a/lib/rules/default-props-match-prop-types.js +++ b/lib/rules/default-props-match-prop-types.js @@ -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 @@ -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; } diff --git a/tests/lib/rules/default-props-match-prop-types.js b/tests/lib/rules/default-props-match-prop-types.js index 363df98398..9ea4b12a58 100644 --- a/tests/lib/rules/default-props-match-prop-types.js +++ b/tests/lib/rules/default-props-match-prop-types.js @@ -761,6 +761,28 @@ ruleTester.run('default-props-match-prop-types', rule, { column: 3 }] }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + '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 }) {', @@ -1219,6 +1241,25 @@ ruleTester.run('default-props-match-prop-types', rule, { column: 5 }] }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + '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 {',