Skip to content

Commit 64cf2d7

Browse files
authored
Merge pull request #1507 from petersendidit/1506-fix
Fix no-unused-prop-types setState updater
2 parents 006441f + c471d77 commit 64cf2d7

File tree

2 files changed

+176
-8
lines changed

2 files changed

+176
-8
lines changed

lib/rules/no-unused-prop-types.js

+67-8
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,48 @@ module.exports = {
101101
return false;
102102
}
103103

104+
/**
105+
* Check if the current node is in a setState updater method
106+
* @return {boolean} true if we are in a setState updater, false if not
107+
*/
108+
function inSetStateUpdater() {
109+
let scope = context.getScope();
110+
while (scope) {
111+
if (
112+
scope.block && scope.block.parent
113+
&& scope.block.parent.type === 'CallExpression'
114+
&& scope.block.parent.callee.property
115+
&& scope.block.parent.callee.property.name === 'setState'
116+
// Make sure we are in the updater not the callback
117+
&& scope.block.parent.arguments[0].start === scope.block.start
118+
) {
119+
return true;
120+
}
121+
scope = scope.upper;
122+
}
123+
return false;
124+
}
125+
126+
function isPropArgumentInSetStateUpdater(node) {
127+
let scope = context.getScope();
128+
while (scope) {
129+
if (
130+
scope.block && scope.block.parent
131+
&& scope.block.parent.type === 'CallExpression'
132+
&& scope.block.parent.callee.property
133+
&& scope.block.parent.callee.property.name === 'setState'
134+
// Make sure we are in the updater not the callback
135+
&& scope.block.parent.arguments[0].start === scope.block.start
136+
&& scope.block.parent.arguments[0].params
137+
&& scope.block.parent.arguments[0].params.length > 0
138+
) {
139+
return scope.block.parent.arguments[0].params[1].name === node.object.name;
140+
}
141+
scope = scope.upper;
142+
}
143+
return false;
144+
}
145+
104146
/**
105147
* Checks if we are using a prop
106148
* @param {ASTNode} node The AST node being checked.
@@ -109,7 +151,8 @@ module.exports = {
109151
function isPropTypesUsage(node) {
110152
const isClassUsage = (
111153
(utils.getParentES6Component() || utils.getParentES5Component()) &&
112-
node.object.type === 'ThisExpression' && node.property.name === 'props'
154+
((node.object.type === 'ThisExpression' && node.property.name === 'props')
155+
|| isPropArgumentInSetStateUpdater(node))
113156
);
114157
const isStatelessFunctionUsage = node.object.name === 'props';
115158
return isClassUsage || isStatelessFunctionUsage || inLifeCycleMethod();
@@ -534,16 +577,20 @@ module.exports = {
534577
const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node));
535578
const isDirectNextProp = DIRECT_NEXT_PROPS_REGEX.test(sourceCode.getText(node));
536579
const isDirectPrevProp = DIRECT_PREV_PROPS_REGEX.test(sourceCode.getText(node));
580+
const isDirectSetStateProp = isPropArgumentInSetStateUpdater(node);
537581
const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
538582
const isNotInConstructor = !inConstructor(node);
539583
const isNotInLifeCycleMethod = !inLifeCycleMethod();
540-
if ((isDirectProp || isDirectNextProp || isDirectPrevProp)
584+
const isNotInSetStateUpdater = !inSetStateUpdater();
585+
if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp)
541586
&& isInClassComponent
542587
&& isNotInConstructor
543-
&& isNotInLifeCycleMethod) {
588+
&& isNotInLifeCycleMethod
589+
&& isNotInSetStateUpdater
590+
) {
544591
return void 0;
545592
}
546-
if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp) {
593+
if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp && !isDirectSetStateProp) {
547594
node = node.parent;
548595
}
549596
const property = node.property;
@@ -607,6 +654,9 @@ module.exports = {
607654
case 'FunctionExpression':
608655
type = 'destructuring';
609656
properties = node.params[0].properties;
657+
if (inSetStateUpdater()) {
658+
properties = node.params[1].properties;
659+
}
610660
break;
611661
case 'VariableDeclarator':
612662
for (let i = 0, j = node.id.properties.length; i < j; i++) {
@@ -898,11 +948,20 @@ module.exports = {
898948
markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
899949
}
900950

951+
function handleSetStateUpdater(node) {
952+
if (!node.params || !node.params.length || !inSetStateUpdater()) {
953+
return;
954+
}
955+
markPropTypesAsUsed(node);
956+
}
957+
901958
/**
959+
* Handle both stateless functions and setState updater functions.
902960
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
903961
* FunctionDeclaration, or FunctionExpression
904962
*/
905-
function handleStatelessComponent(node) {
963+
function handleFunctionLikeExpressions(node) {
964+
handleSetStateUpdater(node);
906965
markDestructuredFunctionArgumentsAsUsed(node);
907966
markAnnotatedFunctionArgumentsAsDeclared(node);
908967
}
@@ -942,11 +1001,11 @@ module.exports = {
9421001
markPropTypesAsUsed(node);
9431002
},
9441003

945-
FunctionDeclaration: handleStatelessComponent,
1004+
FunctionDeclaration: handleFunctionLikeExpressions,
9461005

947-
ArrowFunctionExpression: handleStatelessComponent,
1006+
ArrowFunctionExpression: handleFunctionLikeExpressions,
9481007

949-
FunctionExpression: handleStatelessComponent,
1008+
FunctionExpression: handleFunctionLikeExpressions,
9501009

9511010
MemberExpression: function(node) {
9521011
let type;

tests/lib/rules/no-unused-prop-types.js

+109
Original file line numberDiff line numberDiff line change
@@ -2110,6 +2110,93 @@ ruleTester.run('no-unused-prop-types', rule, {
21102110
].join('\n'),
21112111
parser: 'babel-eslint',
21122112
options: [{skipShapeProps: false}]
2113+
}, {
2114+
// issue #1506
2115+
code: [
2116+
'class MyComponent extends React.Component {',
2117+
' onFoo() {',
2118+
' this.setState((prevState, props) => {',
2119+
' props.doSomething();',
2120+
' });',
2121+
' }',
2122+
' render() {',
2123+
' return (',
2124+
' <div onClick={this.onFoo}>Test</div>',
2125+
' );',
2126+
' }',
2127+
'}',
2128+
'MyComponent.propTypes = {',
2129+
' doSomething: PropTypes.func',
2130+
'};',
2131+
'var tempVar2;'
2132+
].join('\n'),
2133+
parser: 'babel-eslint',
2134+
options: [{skipShapeProps: false}]
2135+
}, {
2136+
// issue #1506
2137+
code: [
2138+
'class MyComponent extends React.Component {',
2139+
' onFoo() {',
2140+
' this.setState((prevState, { doSomething }) => {',
2141+
' doSomething();',
2142+
' });',
2143+
' }',
2144+
' render() {',
2145+
' return (',
2146+
' <div onClick={this.onFoo}>Test</div>',
2147+
' );',
2148+
' }',
2149+
'}',
2150+
'MyComponent.propTypes = {',
2151+
' doSomething: PropTypes.func',
2152+
'};'
2153+
].join('\n'),
2154+
parser: 'babel-eslint',
2155+
options: [{skipShapeProps: false}]
2156+
}, {
2157+
// issue #1506
2158+
code: [
2159+
'class MyComponent extends React.Component {',
2160+
' onFoo() {',
2161+
' this.setState((prevState, obj) => {',
2162+
' obj.doSomething();',
2163+
' });',
2164+
' }',
2165+
' render() {',
2166+
' return (',
2167+
' <div onClick={this.onFoo}>Test</div>',
2168+
' );',
2169+
' }',
2170+
'}',
2171+
'MyComponent.propTypes = {',
2172+
' doSomething: PropTypes.func',
2173+
'};',
2174+
'var tempVar2;'
2175+
].join('\n'),
2176+
parser: 'babel-eslint',
2177+
options: [{skipShapeProps: false}]
2178+
}, {
2179+
// issue #1506
2180+
code: [
2181+
'class MyComponent extends React.Component {',
2182+
' onFoo() {',
2183+
' this.setState(() => {',
2184+
' this.props.doSomething();',
2185+
' });',
2186+
' }',
2187+
' render() {',
2188+
' return (',
2189+
' <div onClick={this.onFoo}>Test</div>',
2190+
' );',
2191+
' }',
2192+
'}',
2193+
'MyComponent.propTypes = {',
2194+
' doSomething: PropTypes.func',
2195+
'};',
2196+
'var tempVar;'
2197+
].join('\n'),
2198+
parser: 'babel-eslint',
2199+
options: [{skipShapeProps: false}]
21132200
}, {
21142201
// issue #106
21152202
code: `
@@ -3788,6 +3875,28 @@ ruleTester.run('no-unused-prop-types', rule, {
37883875
errors: [{
37893876
message: '\'lastname\' PropType is defined but prop is never used'
37903877
}]
3878+
}, {
3879+
// issue #1506
3880+
code: [
3881+
'class MyComponent extends React.Component {',
3882+
' onFoo() {',
3883+
' this.setState(({ doSomething }, props) => {',
3884+
' return { doSomething: doSomething + 1 };',
3885+
' });',
3886+
' }',
3887+
' render() {',
3888+
' return (',
3889+
' <div onClick={this.onFoo}>Test</div>',
3890+
' );',
3891+
' }',
3892+
'}',
3893+
'MyComponent.propTypes = {',
3894+
' doSomething: PropTypes.func',
3895+
'};'
3896+
].join('\n'),
3897+
errors: [{
3898+
message: '\'doSomething\' PropType is defined but prop is never used'
3899+
}]
37913900
}, {
37923901
code: `
37933902
type Props = {

0 commit comments

Comments
 (0)