Repository: yannickcr/eslint-plugin-react (js)

Issue:
nextProps aren't handled by no-unused-props
I have the following (simplified) piece of code:

```js
  componentWillReceiveProps(nextProps) {
    if (!nextProps.error) {
      console.log('oh noez');
    }
  }
```

This triggers the following ESLint error:

```
  89:27  error  'error' is missing in props validation  react/prop-types
```

However, if I do add `error` to `propTypes`, another error is triggered:

```js
  33:10  error  'error' PropType is defined but prop is never used  react/no-unused-prop-types
```

I think the two rules should cooperate.

I'm using the following relevant packages:

- eslint@3.15.0
- eslint-config-airbnb@14.1.0
- eslint-plugin-react@6.10.0

Pull request description:
Fix nextProps false positive in no-unused-prop-types
Fixes #1079

I did the same comparisons as the `prop-types` rules (https://github.com/yannickcr/eslint-plugin-react/blob/b64648521f9a7949eef4cd1a210768e906b4f3d1/lib/rules/prop-types.js#L130) for the `no-unused-prop-types` rule.
This way they both handle nextProps identically.

I also borrowed the test case from #1105.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e28279f7..06d1e4ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,33 +3,6 @@ All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
 
-## [6.10.3] - 2017-03-20
-### Fixed
-* Revert [#1057][] due to issues with [`jsx-indent`][] ([#1117][])
-
-[6.10.3]: https://github.com/yannickcr/eslint-plugin-react/compare/v6.10.2...v6.10.3
-
-## [6.10.2] - 2017-03-19
-### Fixed
-* Fix [`jsx-indent`][] indentation calculation with nested JSX ([#1117][])
-
-[6.10.2]: https://github.com/yannickcr/eslint-plugin-react/compare/v6.10.1...v6.10.2
-[#1117]: https://github.com/yannickcr/eslint-plugin-react/issues/1117
-
-## [6.10.1] - 2017-03-19
-### Fixed
-* Fix [`jsx-indent`][] auto fix with tabs ([#1057][] @kentcdodds @webOS101)
-* Fix [`jsx-indent`][] crash ([#1061][] @iancmyers)
-* Fix [`void-dom-elements-no-children`][] crash and fix it to only checks for a createElement call from
-React ([#1073][] @jomasti)
-* Fix component detection that caused a false positive in [`no-multi-comp`][] ([#1088][] @benstepp)
-
-[6.10.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v6.10.0...v6.10.1
-[#1057]: https://github.com/yannickcr/eslint-plugin-react/issues/1057
-[#1061]: https://github.com/yannickcr/eslint-plugin-react/issues/1061
-[#1073]: https://github.com/yannickcr/eslint-plugin-react/issues/1073
-[#1088]: https://github.com/yannickcr/eslint-plugin-react/issues/1088
-
 ## [6.10.0] - 2017-02-16
 ### Added
 * Add [`forbid-foreign-prop-types`][] rule ([#696][] @iancmyers)
@@ -97,7 +70,7 @@ React ([#1073][] @jomasti)
 * Fix [`jsx-indent`][] with multiline jsx in ternaries ([#966][])
 * Fix component detection to ignore async functions ([#989][] @taion)
 * Fix [`jsx-curly-spacing`][] autofix to not delete comments ([#648][])
-* Fix auto-enabling of `eslint-plugin-react` in exported configurations ([#984][] @jamischarles)
+* FIx auto-enabling of `eslint-plugin-react` in exported configurations ([#984][] @jamischarles)
 
 ### Changed
 * Update dependencies
diff --git a/docs/rules/jsx-indent.md b/docs/rules/jsx-indent.md
index 909c5818..336e72e3 100644
--- a/docs/rules/jsx-indent.md
+++ b/docs/rules/jsx-indent.md
@@ -77,6 +77,68 @@ The following patterns are not warnings:
 </App>
 ```
 
+#### indentLogicalExpressions
+
+```js
+...
+"react/jsx-indent": [<enabled>, 'tab'|<number>, {indentLogicalExpressions: true}]
+...
+```
+
+By default this is set to false. When enabled, an additional indentation is required when the JSX is the right of a LogicalExpression
+
+The following patterns are considered warnings:
+
+```jsx
+// 2 spaces indentation with indentLogicalExpressions as false
+// [2, 2, {indentLogicalExpressions: false}]
+<App>
+  {
+    condition &&
+      <Container>
+        <Child></Child>
+      </Container>
+  }
+</App>
+
+// 2 spaces indentation with indentLogicalExpressions as true
+// [2, 2, {indentLogicalExpressions: true}]
+<App>
+  {
+    condition &&
+    <Container>
+      <Child></Child>
+    </Container>
+  }
+</App>
+```
+
+The following patterns are not warnings:
+
+```jsx
+// 2 spaces indentation with indentLogicalExpressions as true
+// [2, 2, {indentLogicalExpressions: true}]
+<App>
+  {
+    condition &&
+      <Container>
+        <Child></Child>
+      </Container>
+  }
+</App>
+
+// 2 spaces indentation with indentLogicalExpressions as false
+// [2, 2, {indentLogicalExpressions: false}]
+<App>
+  {
+    condition &&
+    <Container>
+      <Child></Child>
+    </Container>
+  }
+</App>
+```
+
 ## When not to use
 
 If you are not using JSX then you can disable this rule.
diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js
index 8d5af542..f6ed3ac5 100644
--- a/lib/rules/jsx-indent.js
+++ b/lib/rules/jsx-indent.js
@@ -46,6 +46,14 @@ module.exports = {
       }, {
         type: 'integer'
       }]
+    }, {
+      type: 'object',
+      properties: {
+        indentLogicalExpressions: {
+          type: 'boolean'
+        }
+      },
+      additionalProperties: false
     }]
   },
 
@@ -56,6 +64,7 @@ module.exports = {
     var extraColumnStart = 0;
     var indentType = 'space';
     var indentSize = 4;
+    var indentLogicalExpressions = false;
 
     var sourceCode = context.getSourceCode();
 
@@ -67,24 +76,25 @@ module.exports = {
         indentSize = context.options[0];
         indentType = 'space';
       }
+      if (context.options[1]) {
+        indentLogicalExpressions = context.options[1].indentLogicalExpressions || false;
+      }
     }
 
     var indentChar = indentType === 'space' ? ' ' : '\t';
 
     /**
      * Responsible for fixing the indentation issue fix
-     * @param {ASTNode} node Node violating the indent rule
+     * @param {Boolean} rangeToReplace is used to specify the range
+     *   to replace with the correct indentation.
      * @param {Number} needed Expected indentation character count
      * @returns {Function} function to be executed by the fixer
      * @private
      */
-    function getFixerFunction(node, needed) {
+    function getFixerFunction(rangeToReplace, needed) {
       return function(fixer) {
         var indent = Array(needed + 1).join(indentChar);
-        return fixer.replaceTextRange(
-          [node.start - node.loc.start.column, node.start],
-          indent
-        );
+        return fixer.replaceTextRange(rangeToReplace, indent);
       };
     }
 
@@ -93,46 +103,38 @@ module.exports = {
      * @param {ASTNode} node Node violating the indent rule
      * @param {Number} needed Expected indentation character count
      * @param {Number} gotten Indentation character count in the actual node/code
-     * @param {Object} loc Error line and column location
+     * @param {Array} rangeToReplace is used in the fixer.
+     *   Defaults to the indent of the start of the node
+     * @param {Object} loc Error line and column location (defaults to node.loc
      */
-    function report(node, needed, gotten, loc) {
+    function report(node, needed, gotten, rangeToReplace, loc) {
       var msgContext = {
         needed: needed,
         type: indentType,
         characters: needed === 1 ? 'character' : 'characters',
         gotten: gotten
       };
+      rangeToReplace = rangeToReplace || [node.start - node.loc.start.column, node.start];
 
-      if (loc) {
-        context.report({
-          node: node,
-          loc: loc,
-          message: MESSAGE,
-          data: msgContext,
-          fix: getFixerFunction(node, needed)
-        });
-      } else {
-        context.report({
-          node: node,
-          message: MESSAGE,
-          data: msgContext,
-          fix: getFixerFunction(node, needed)
-        });
-      }
+      context.report({
+        node: node,
+        loc: loc || node.loc,
+        message: MESSAGE,
+        data: msgContext,
+        fix: getFixerFunction(rangeToReplace, needed)
+      });
     }
 
     /**
-     * Get node indent
-     * @param {ASTNode} node Node to examine
-     * @param {Boolean} byLastLine get indent of node's last line
-     * @param {Boolean} excludeCommas skip comma on start of line
-     * @return {Number} Indent
+     * Get the indentation (of the proper indentType) that exists in the source
+     * @param {String} src the source string
+     * @param {Boolean} byLastLine whether the line checked should be the last
+     *   Defaults to the first line
+     * @param {Boolean} excludeCommas whether to skip commas in the check
+     *   Defaults to false
+     * @return {Number} the indentation of the indentType that exists on the line
      */
-    function getNodeIndent(node, byLastLine, excludeCommas) {
-      byLastLine = byLastLine || false;
-      excludeCommas = excludeCommas || false;
-
-      var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart);
+    function getIndentFromString(src, byLastLine, excludeCommas) {
       var lines = src.split('\n');
       if (byLastLine) {
         src = lines[lines.length - 1];
@@ -154,7 +156,24 @@ module.exports = {
     }
 
     /**
-     * Checks node is the first in its own start line. By default it looks by start line.
+     * Get node indent
+     * @param {ASTNode} node Node to examine
+     * @param {Boolean} byLastLine get indent of node's last line
+     * @param {Boolean} excludeCommas skip comma on start of line
+     * @return {Number} Indent
+     */
+    function getNodeIndent(node, byLastLine, excludeCommas) {
+      byLastLine = byLastLine || false;
+      excludeCommas = excludeCommas || false;
+
+      var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart);
+
+      return getIndentFromString(src, byLastLine, excludeCommas);
+    }
+
+    /**
+     * Checks if the node is the first in its own start line. By default it looks by start line.
+     * One exception is closing tags with preceeding whitespace
      * @param {ASTNode} node The node to check
      * @return {Boolean} true if its the first in the its start line
      */
@@ -165,8 +184,9 @@ module.exports = {
       } while (token.type === 'JSXText' && /^\s*$/.test(token.value));
       var startLine = node.loc.start.line;
       var endLine = token ? token.loc.end.line : -1;
+      var whitespaceOnly = token ? /\n\s*$/.test(token.value) : false;
 
-      return startLine !== endLine;
+      return startLine !== endLine || whitespaceOnly;
     }
 
   /**
@@ -218,41 +238,83 @@ module.exports = {
       }
     }
 
+    /**
+     * Checks the end of the tag (>) to determine whether it's on its own line
+     * If so, it verifies the indentation is correct and reports if it is not
+     * @param {ASTNode} node The node to check
+     * @param {Number} startIndent The indentation of the start of the tag
+     */
+    function checkTagEndIndent(node, startIndent) {
+      var source = sourceCode.getText(node);
+      var isTagEndOnOwnLine = /\n\s*\/?>$/.exec(source);
+      if (isTagEndOnOwnLine) {
+        var endIndent = getIndentFromString(source, true, false);
+        if (endIndent !== startIndent) {
+          var rangeToReplace = [node.end - node.loc.end.column, node.end - 1];
+          report(node, startIndent, endIndent, rangeToReplace);
+        }
+      }
+    }
+
+    /**
+     * Gets what the JSXOpeningElement's indentation should be
+     * @param {ASTNode} node The JSXOpeningElement
+     * @return {Number} the number of indentation characters it should have
+     */
+    function getOpeningElementIndent(node) {
+      var prevToken = sourceCode.getTokenBefore(node);
+      if (!prevToken) {
+        return 0;
+      }
+      if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
+        // Use the parent in a list or an array
+        prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
+        prevToken = prevToken.type === 'Literal' ? prevToken.parent : prevToken;
+      } else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
+        // Use the first non-punctuator token in a conditional expression
+        do {
+          prevToken = sourceCode.getTokenBefore(prevToken);
+        } while (prevToken.type === 'Punctuator');
+        prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
+
+        while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
+          prevToken = prevToken.parent;
+        }
+      }
+      prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
+
+      var parentElementIndent = getNodeIndent(prevToken);
+      if (prevToken.type === 'JSXElement') {
+        parentElementIndent = getOpeningElementIndent(prevToken.openingElement);
+      }
+
+      if (isRightInLogicalExp(node) && indentLogicalExpressions) {
+        parentElementIndent += indentSize;
+      }
+
+      var indent = (
+        prevToken.loc.start.line === node.loc.start.line ||
+        isRightInLogicalExp(node) ||
+        isAlternateInConditionalExp(node)
+      ) ? 0 : indentSize;
+      return parentElementIndent + indent;
+    }
+
     return {
       JSXOpeningElement: function(node) {
         var prevToken = sourceCode.getTokenBefore(node);
         if (!prevToken) {
           return;
         }
-        // Use the parent in a list or an array
-        if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
-          prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
-          prevToken = prevToken.type === 'Literal' ? prevToken.parent : prevToken;
-        // Use the first non-punctuator token in a conditional expression
-        } else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
-          do {
-            prevToken = sourceCode.getTokenBefore(prevToken);
-          } while (prevToken.type === 'Punctuator');
-          prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
-          while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
-            prevToken = prevToken.parent;
-          }
-        }
-        prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
-
-        var parentElementIndent = getNodeIndent(prevToken);
-        var indent = (
-          prevToken.loc.start.line === node.loc.start.line ||
-          isRightInLogicalExp(node) ||
-          isAlternateInConditionalExp(node)
-        ) ? 0 : indentSize;
-        checkNodesIndent(node, parentElementIndent + indent);
+        var startIndent = getOpeningElementIndent(node);
+        checkNodesIndent(node, startIndent);
+        checkTagEndIndent(node, startIndent);
       },
       JSXClosingElement: function(node) {
         if (!node.parent) {
           return;
         }
-        var peerElementIndent = getNodeIndent(node.parent.openingElement);
+        var peerElementIndent = getOpeningElementIndent(node.parent.openingElement);
         checkNodesIndent(node, peerElementIndent);
       },
       JSXExpressionContainer: function(node) {
@@ -261,6 +323,34 @@ module.exports = {
         }
         var parentNodeIndent = getNodeIndent(node.parent);
         checkNodesIndent(node, parentNodeIndent + indentSize);
+      },
+      Literal: function(node) {
+        if (!node.parent || node.parent.type !== 'JSXElement' || node.loc.start.line === node.parent.loc.start.line) {
+          return;
+        }
+        var parentElementIndent = getOpeningElementIndent(node.parent.openingElement);
+        var expectedIndent = parentElementIndent + indentSize;
+        var source = sourceCode.getText(node);
+        var lines = source.split('\n');
+        var currentIndex = 0;
+        lines.forEach(function(line, lineNumber) {
+          if (line.trim()) {
+            var lineIndent = getIndentFromString(line);
+            if (lineIndent !== expectedIndent) {
+              var lineStart = source.indexOf(line, currentIndex);
+              var lineIndentStart = line.search(/\S/);
+              var lineIndentEnd = lineStart + lineIndentStart;
+              var rangeToReplace = [node.start + lineStart, node.start + lineIndentEnd];
+              var locLine = lineNumber + node.loc.start.line;
+              var loc = {
+                start: {line: locLine, column: lineIndentStart},
+                end: {line: locLine, column: lineIndentEnd}
+              };
+              report(node, expectedIndent, lineIndent, rangeToReplace, loc);
+            }
+          }
+          currentIndex += line.length;
+        });
       }
     };
 
diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js
index be905493..744f211c 100644
--- a/lib/rules/no-unused-prop-types.js
+++ b/lib/rules/no-unused-prop-types.js
@@ -18,6 +18,7 @@ var annotations = require('../util/annotations');
 // ------------------------------------------------------------------------------
 
 var DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/;
+var DIRECT_NEXT_PROPS_REGEX = /^nextProps\s*(\.|\[)/;
 
 // ------------------------------------------------------------------------------
 // Rule Definition
@@ -77,6 +78,24 @@ module.exports = {
       return value;
     }
 
+    /**
+     * Check if we are in a class constructor
+     * @return {boolean} true if we are in a class constructor, false if not
+     **/
+    function inComponentWillReceiveProps() {
+      var scope = context.getScope();
+      while (scope) {
+        if (
+          scope.block && scope.block.parent &&
+          scope.block.parent.key && scope.block.parent.key.name === 'componentWillReceiveProps'
+        ) {
+          return true;
+        }
+        scope = scope.upper;
+      }
+      return false;
+    }
+
     /**
      * Checks if we are using a prop
      * @param {ASTNode} node The AST node being checked.
@@ -88,7 +107,8 @@ module.exports = {
         node.object.type === 'ThisExpression' && node.property.name === 'props'
       );
       var isStatelessFunctionUsage = node.object.name === 'props';
-      return isClassUsage || isStatelessFunctionUsage;
+      var isNextPropsUsage = node.object.name === 'nextProps' && inComponentWillReceiveProps();
+      return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage;
     }
 
     /**
@@ -491,12 +511,17 @@ module.exports = {
      */
     function getPropertyName(node) {
       var isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node));
+      var isDirectNextProp = DIRECT_NEXT_PROPS_REGEX.test(sourceCode.getText(node));
       var isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
       var isNotInConstructor = !inConstructor(node);
-      if (isDirectProp && isInClassComponent && isNotInConstructor) {
+      var isNotInComponentWillReceiveProps = !inComponentWillReceiveProps();
+      if ((isDirectProp || isDirectNextProp)
+        && isInClassComponent
+        && isNotInConstructor
+        && isNotInComponentWillReceiveProps) {
         return void 0;
       }
-      if (!isDirectProp) {
+      if (!isDirectProp && !isDirectNextProp) {
         node = node.parent;
       }
       var property = node.property;
diff --git a/package.json b/package.json
index 5c211c24..3950bcc0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-react",
-  "version": "6.10.3",
+  "version": "6.10.0",
   "author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
   "description": "React specific linting rules for ESLint",
   "main": "index.js",


Test patch:
diff --git a/tests/lib/rules/jsx-indent.js b/tests/lib/rules/jsx-indent.js
index 4d1789ec..960a1009 100644
--- a/tests/lib/rules/jsx-indent.js
+++ b/tests/lib/rules/jsx-indent.js
@@ -51,14 +51,6 @@ ruleTester.run('jsx-indent', rule, {
     ].join('\n'),
     options: [0],
     parserOptions: parserOptions
-  }, {
-    code: [
-      '  <App>',
-      '<Foo />',
-      '  </App>'
-    ].join('\n'),
-    options: [-2],
-    parserOptions: parserOptions
   }, {
     code: [
       '<App>',
@@ -211,17 +203,6 @@ ruleTester.run('jsx-indent', rule, {
       '</div>'
     ].join('\n'),
     parserOptions: parserOptions
-  }, {
-    // Literals indentation is not touched
-    code: [
-      '<div>',
-      'bar <div>',
-      '   bar',
-      '   bar {foo}',
-      'bar </div>',
-      '</div>'
-    ].join('\n'),
-    parserOptions: parserOptions
   }, {
     // Multiline ternary
     // (colon at the end of the first expression)
@@ -416,6 +397,19 @@ ruleTester.run('jsx-indent', rule, {
       ')'
     ].join('\n'),
     parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '    {',
+      '        condition &&',
+      '            <Container>',
+      '                <Child></Child>',
+      '            </Container>',
+      '    }',
+      '</App>'
+    ].join('\n'),
+    options: [4, {indentLogicalExpressions: true}],
+    parserOptions: parserOptions
   }, {
     code: [
       '<span>',
@@ -457,6 +451,14 @@ ruleTester.run('jsx-indent', rule, {
     ].join('\n'),
     options: [2],
     parserOptions: parserOptions
+  }, {
+    code: [
+      'function foo() {',
+      '  <span>Text</span>',
+      '}'
+    ].join('\n'),
+    options: [2],
+    parserOptions: parserOptions
   }],
 
   invalid: [{
@@ -500,6 +502,39 @@ ruleTester.run('jsx-indent', rule, {
     options: ['tab'],
     parserOptions: parserOptions,
     errors: [{message: 'Expected indentation of 1 tab character but found 0.'}]
+  }, {
+    code: [
+      'function MyComponent(props) {',
+      '\treturn (',
+      '    <div',
+      '\t\t\tclassName="foo-bar"',
+      '\t\t\tid="thing"',
+      '    >',
+      '      Hello world!',
+      '    </div>',
+      '\t)',
+      '}'
+    ].join('\n'),
+    output: [
+      'function MyComponent(props) {',
+      '\treturn (',
+      '\t\t<div',
+      '\t\t\tclassName="foo-bar"',
+      '\t\t\tid="thing"',
+      '\t\t>',
+      '\t\t\tHello world!',
+      '\t\t</div>',
+      '\t)',
+      '}'
+    ].join('\n'),
+    options: ['tab'],
+    parserOptions: parserOptions,
+    errors: [
+      {message: 'Expected indentation of 2 tab characters but found 0.'},
+      {message: 'Expected indentation of 2 tab characters but found 0.'},
+      {message: 'Expected indentation of 3 tab characters but found 0.'},
+      {message: 'Expected indentation of 2 tab characters but found 0.'}
+    ]
   }, {
     code: [
       'function App() {',
@@ -546,11 +581,7 @@ ruleTester.run('jsx-indent', rule, {
       '  );',
       '}'
     ].join('\n'),
-    // The detection logic only thinks <App> is indented wrong, not the other
-    // two lines following. I *think* because it incorrectly uses <App>'s indention
-    // as the baseline for the next two, instead of the realizing the entire three
-    // lines are wrong together. See #608
-    /* output: [
+    output: [
       'function App() {',
       '  return (',
       '    <App>',
@@ -558,10 +589,14 @@ ruleTester.run('jsx-indent', rule, {
       '    </App>',
       '  );',
       '}'
-    ].join('\n'), */
+    ].join('\n'),
     options: [2],
     parserOptions: parserOptions,
-    errors: [{message: 'Expected indentation of 4 space characters but found 0.'}]
+    errors: [
+      {message: 'Expected indentation of 4 space characters but found 0.'},
+      {message: 'Expected indentation of 6 space characters but found 2.'},
+      {message: 'Expected indentation of 4 space characters but found 0.'}
+    ]
   }, {
     code: [
       '<App>',
diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js
index 02a60322..8611e10d 100644
--- a/tests/lib/rules/no-unused-prop-types.js
+++ b/tests/lib/rules/no-unused-prop-types.js
@@ -1461,6 +1461,22 @@ ruleTester.run('no-unused-prop-types', rule, {
         '};'
       ].join('\n'),
       parserOptions: parserOptions
+    }, {
+      code: [
+        'class Hello extends Component {',
+        '  componentWillReceiveProps (nextProps) {',
+        '    if (nextProps.foo) {',
+        '      doSomething(this.props.bar);',
+        '    }',
+        '  }',
+        '}',
+
+        'Hello.propTypes = {',
+        '  foo: PropTypes.bool,',
+        '  bar: PropTypes.bool',
+        '};'
+      ].join('\n'),
+      parserOptions: parserOptions
     }
   ],