Repository: jsx-eslint/eslint-plugin-react (JavaScript)

Issue:
Rule prop-types should include nextProps checking in shouldComponentUpdate
Consider this example:
``` javascript
class Hello extends Component {
  static propTypes = {
    bar: PropTypes.func
  }
  shouldComponentUpdate(nextProps) {
    return nextProps.foo !== null
  }
  render() {
    return <div bar={this.props.bar} />;
  }
}
```

This is a case where we want to check to make sure that `foo` is properly defined in `propTypes`. I think this case should be included in the `prop-type` rule. Any thoughts?

Please refer to my comment here: https://github.com/yannickcr/eslint-plugin-react/issues/1636#issuecomment-364195848

I've made the changes in a forked repo and seems to work just fine. If we wanted to get this in, I'd need some help with the test cases but here is my fork: https://github.com/mindspacepdx/eslint-plugin-react/tree/am-should-component-update


Discussion:
A PR would be great :-)



Patch:
diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js
index 7466c3a2f1..bd66503f58 100644
--- a/lib/rules/prop-types.js
+++ b/lib/rules/prop-types.js
@@ -121,6 +121,24 @@ module.exports = {
       return false;
     }
 
+    /**
+     * Check if we are in a class constructor
+     * @return {boolean} true if we are in a class constructor, false if not
+     */
+    function inShouldComponentUpdate() {
+      let scope = context.getScope();
+      while (scope) {
+        if (
+          scope.block && scope.block.parent &&
+          scope.block.parent.key && scope.block.parent.key.name === 'shouldComponentUpdate'
+        ) {
+          return true;
+        }
+        scope = scope.upper;
+      }
+      return false;
+    }
+
     /**
     * Checks if a prop is being assigned a value props.bar = 'bar'
     * @param {ASTNode} node The AST node being checked.
@@ -146,7 +164,7 @@ module.exports = {
         node.object.type === 'ThisExpression' && node.property.name === 'props'
       );
       const isStatelessFunctionUsage = node.object.name === 'props' && !isAssignmentToProp(node);
-      const isNextPropsUsage = node.object.name === 'nextProps' && inComponentWillReceiveProps();
+      const isNextPropsUsage = node.object.name === 'nextProps' && (inComponentWillReceiveProps() || inShouldComponentUpdate());
       return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage;
     }
 
@@ -549,7 +567,9 @@ module.exports = {
       const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
       const isNotInConstructor = !inConstructor();
       const isNotInComponentWillReceiveProps = !inComponentWillReceiveProps();
-      if (isDirectProp && isInClassComponent && isNotInConstructor && isNotInComponentWillReceiveProps) {
+      const isNotInShouldComponentUpdate = !inShouldComponentUpdate();
+      if (isDirectProp && isInClassComponent && isNotInConstructor && isNotInComponentWillReceiveProps
+        && isNotInShouldComponentUpdate) {
         return void 0;
       }
       if (!isDirectProp) {
@@ -1043,6 +1063,10 @@ module.exports = {
           markPropTypesAsUsed(node);
         }
 
+        if (node.key.name === 'shouldComponentUpdate' && destructuring) {
+          markPropTypesAsUsed(node);
+        }
+
         if (!node.static || node.kind !== 'get' || !propsUtil.isPropTypesDeclaration(node)) {
           return;
         }


Test patch:
diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js
index de25474b76..a41b86976e 100644
--- a/tests/lib/rules/prop-types.js
+++ b/tests/lib/rules/prop-types.js
@@ -3148,6 +3148,48 @@ ruleTester.run('prop-types', rule, {
       errors: [
         {message: '\'foo\' is missing in props validation'}
       ]
+    }, {
+      code: [
+        'class Hello extends Component {',
+        '  static propTypes = forbidExtraProps({',
+        '    bar: PropTypes.func',
+        '  })',
+        '  shouldComponentUpdate(nextProps) {',
+        '    if (nextProps.foo) {',
+        '      return;',
+        '    }',
+        '  }',
+        '  render() {',
+        '    return <div bar={this.props.bar} />;',
+        '  }',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      settings: Object.assign({}, settings, {
+        propWrapperFunctions: ['forbidExtraProps']
+      }),
+      errors: [
+        {message: '\'foo\' is missing in props validation'}
+      ]
+    }, {
+      code: [
+        'class Hello extends Component {',
+        '  shouldComponentUpdate({foo}) {',
+        '    if (foo) {',
+        '      return;',
+        '    }',
+        '  }',
+        '  render() {',
+        '    return <div bar={this.props.bar} />;',
+        '  }',
+        '}',
+        'Hello.propTypes = {',
+        '    bar: PropTypes.func',
+        '  }'
+      ].join('\n'),
+      errors: [
+        {message: '\'foo\' is missing in props validation'}
+      ]
     }, {
       code: [
         'class Hello extends React.Component {',