diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index 0471913737..f3c764be48 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -100,6 +100,48 @@ module.exports = { return false; } + /** + * Check if the current node is in a setState updater method + * @return {boolean} true if we are in a setState updater, false if not + */ + function inSetStateUpdater() { + let scope = context.getScope(); + while (scope) { + if ( + scope.block && scope.block.parent + && scope.block.parent.type === 'CallExpression' + && scope.block.parent.callee.property + && scope.block.parent.callee.property.name === 'setState' + // Make sure we are in the updater not the callback + && scope.block.parent.arguments[0].start === scope.block.start + ) { + return true; + } + scope = scope.upper; + } + return false; + } + + function isPropArgumentInSetStateUpdater(node) { + let scope = context.getScope(); + while (scope) { + if ( + scope.block && scope.block.parent + && scope.block.parent.type === 'CallExpression' + && scope.block.parent.callee.property + && scope.block.parent.callee.property.name === 'setState' + // Make sure we are in the updater not the callback + && scope.block.parent.arguments[0].start === scope.block.start + && scope.block.parent.arguments[0].params + && scope.block.parent.arguments[0].params.length > 0 + ) { + return scope.block.parent.arguments[0].params[1].name === node.object.name; + } + scope = scope.upper; + } + return false; + } + /** * Checks if we are using a prop * @param {ASTNode} node The AST node being checked. @@ -108,7 +150,8 @@ module.exports = { function isPropTypesUsage(node) { const isClassUsage = ( (utils.getParentES6Component() || utils.getParentES5Component()) && - node.object.type === 'ThisExpression' && node.property.name === 'props' + ((node.object.type === 'ThisExpression' && node.property.name === 'props') + || isPropArgumentInSetStateUpdater(node)) ); const isStatelessFunctionUsage = node.object.name === 'props'; return isClassUsage || isStatelessFunctionUsage || inLifeCycleMethod(); @@ -558,16 +601,20 @@ module.exports = { const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); const isDirectNextProp = DIRECT_NEXT_PROPS_REGEX.test(sourceCode.getText(node)); const isDirectPrevProp = DIRECT_PREV_PROPS_REGEX.test(sourceCode.getText(node)); + const isDirectSetStateProp = isPropArgumentInSetStateUpdater(node); const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component(); const isNotInConstructor = !inConstructor(node); const isNotInLifeCycleMethod = !inLifeCycleMethod(); - if ((isDirectProp || isDirectNextProp || isDirectPrevProp) + const isNotInSetStateUpdater = !inSetStateUpdater(); + if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp) && isInClassComponent && isNotInConstructor - && isNotInLifeCycleMethod) { + && isNotInLifeCycleMethod + && isNotInSetStateUpdater + ) { return void 0; } - if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp) { + if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp && !isDirectSetStateProp) { node = node.parent; } const property = node.property; @@ -631,6 +678,9 @@ module.exports = { case 'FunctionExpression': type = 'destructuring'; properties = node.params[0].properties; + if (inSetStateUpdater()) { + properties = node.params[1].properties; + } break; case 'VariableDeclarator': for (let i = 0, j = node.id.properties.length; i < j; i++) { @@ -915,11 +965,20 @@ module.exports = { markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0])); } + function handleSetStateUpdater(node) { + if (!node.params || !node.params.length || !inSetStateUpdater()) { + return; + } + markPropTypesAsUsed(node); + } + /** + * Handle both stateless functions and setState updater functions. * @param {ASTNode} node We expect either an ArrowFunctionExpression, * FunctionDeclaration, or FunctionExpression */ - function handleStatelessComponent(node) { + function handleFunctionLikeExpressions(node) { + handleSetStateUpdater(node); markDestructuredFunctionArgumentsAsUsed(node); markAnnotatedFunctionArgumentsAsDeclared(node); } @@ -959,11 +1018,11 @@ module.exports = { markPropTypesAsUsed(node); }, - FunctionDeclaration: handleStatelessComponent, + FunctionDeclaration: handleFunctionLikeExpressions, - ArrowFunctionExpression: handleStatelessComponent, + ArrowFunctionExpression: handleFunctionLikeExpressions, - FunctionExpression: handleStatelessComponent, + FunctionExpression: handleFunctionLikeExpressions, MemberExpression: function(node) { let type; diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 62ef6094c0..f7de810736 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -829,10 +829,10 @@ ruleTester.run('no-unused-prop-types', rule, { type PropsA = { a: string } type PropsB = { b: string } type Props = PropsA & PropsB; - + class MyComponent extends React.Component { props: Props; - + render() { return