Repository: yannickcr/eslint-plugin-react
Pull request #513 (2016-03-24 12:09:03)

Parse explicit declaration that class extends React.Component
This small addition enables linting component-specific rules if the author uses a JSDoc explicitly indicating that the component is a descendent of React.Component

e.g.
```
/** @extends React.Component */
class MyClass extends ChildClass {
...
```

Provides a good workaround for issues such as #68 

coveralls: 
[![Coverage Status](https://coveralls.io/builds/5533058/badge)](https://coveralls.io/builds/5533058)

Coverage decreased (-0.04%) to 97.005% when pulling **51fde3cdf1ab7e3fbb9a622e4374d1a28a066ab9 on gausie:feature/explicit-react-component-parent** into **e3c27f9ecea4d71e5fa6d0aa9131fdd1f5c1db75 on yannickcr:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/5535096/badge)](https://coveralls.io/builds/5535096)

Coverage increased (+0.02%) to 97.057% when pulling **f3e634c59e8928ab170a9bfa9d056cf0f8778064 on gausie:feature/explicit-react-component-parent** into **e3c27f9ecea4d71e5fa6d0aa9131fdd1f5c1db75 on yannickcr:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/5558310/badge)](https://coveralls.io/builds/5558310)

Coverage increased (+0.02%) to 97.057% when pulling **875421dd96536b3a00388a249c5fd1815bbc7dfa on gausie:feature/explicit-react-component-parent** into **e3c27f9ecea4d71e5fa6d0aa9131fdd1f5c1db75 on yannickcr:master**.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1161e20b..4a5ea1ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,13 @@ 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).
 
+## [5.1.1] - 2016-05-10
+### Fixed
+* Fix `require-render-return` crash ([#589][])
+
+[5.1.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v5.1.0...v5.1.1
+[#589]: https://github.com/yannickcr/eslint-plugin-react/issues/589
+
 ## [5.1.0] - 2016-05-10
 ### Added
 * Add `jsx-no-target-blank` rule ([#582][] @Gasparila)
diff --git a/README.md b/README.md
index cc71502b..abf3161c 100644
--- a/README.md
+++ b/README.md
@@ -105,14 +105,14 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
 * [jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Enforce or disallow spaces around equal signs in JSX attributes (fixable)
 * [jsx-first-prop-new-line](docs/rules/jsx-first-prop-new-line.md): Enforce position of the first prop in JSX
 * [jsx-handler-names](docs/rules/jsx-handler-names.md): Enforce event handler naming conventions in JSX
-* [jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX (fixable)
 * [jsx-indent](docs/rules/jsx-indent.md): Validate JSX indentation
+* [jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX (fixable)
 * [jsx-key](docs/rules/jsx-key.md): Validate JSX has key prop when in array or iterator
 * [jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX
 * [jsx-no-bind](docs/rules/jsx-no-bind.md): Prevent usage of `.bind()` and arrow functions in JSX props
 * [jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Prevent duplicate props in JSX
 * [jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings
-* [jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='\_blank'`
+* [jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'`
 * [jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
 * [jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
 * [jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting
diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js
index 647420b0..f2abb00f 100644
--- a/lib/rules/display-name.js
+++ b/lib/rules/display-name.js
@@ -24,23 +24,25 @@ module.exports = Components.detect(function(context, components, utils) {
    * @returns {Boolean} True if we are declaring a display name, false if not.
    */
   function isDisplayNameDeclaration(node) {
-    // Special case for class properties
-    // (babel-eslint does not expose property name so we have to rely on tokens)
-    if (node.type === 'ClassProperty') {
-      var tokens = sourceCode.getFirstTokens(node, 2);
-      if (
-        tokens[0].value === 'displayName' ||
-        (tokens[1] && tokens[1].value === 'displayName')
-      ) {
-        return true;
-      }
-      return false;
+    switch (node.type) {
+      // Special case for class properties
+      // (babel-eslint does not expose property name so we have to rely on tokens)
+      case 'ClassProperty':
+        var tokens = sourceCode.getFirstTokens(node, 2);
+        if (
+          tokens[0].value === 'displayName' ||
+          (tokens[1] && tokens[1].value === 'displayName')
+        ) {
+          return true;
+        }
+        return false;
+      case 'Identifier':
+        return node.name === 'displayName';
+      case 'Literal':
+        return node.value === 'displayName';
+      default:
+        return false;
     }
-
-    return Boolean(
-      node &&
-      node.name === 'displayName'
-    );
   }
 
   /**
diff --git a/lib/rules/jsx-closing-bracket-location.js b/lib/rules/jsx-closing-bracket-location.js
index 50a9e1f1..8a8f180d 100644
--- a/lib/rules/jsx-closing-bracket-location.js
+++ b/lib/rules/jsx-closing-bracket-location.js
@@ -57,7 +57,7 @@ module.exports = function(context) {
     if (typeof tokens.lastProp === 'undefined') {
       location = 'after-tag';
     // Is always after the last prop if this one is on the same line as the opening bracket
-    } else if (tokens.opening.line === tokens.lastProp.line) {
+    } else if (tokens.opening.line === tokens.lastProp.firstLine) {
       location = 'after-props';
     // Else use configuration dependent on selfClosing property
     } else {
@@ -97,7 +97,7 @@ module.exports = function(context) {
       case 'after-tag':
         return tokens.tag.line === tokens.closing.line;
       case 'after-props':
-        return tokens.lastProp.line === tokens.closing.line;
+        return tokens.lastProp.lastLine === tokens.closing.line;
       case 'props-aligned':
       case 'tag-aligned':
       case 'line-aligned':
@@ -124,7 +124,8 @@ module.exports = function(context) {
       lastProp = node.attributes[node.attributes.length - 1];
       lastProp = {
         column: sourceCode.getFirstToken(lastProp).loc.start.column,
-        line: sourceCode.getLastToken(lastProp).loc.end.line
+        firstLine: sourceCode.getFirstToken(lastProp).loc.start.line,
+        lastLine: sourceCode.getLastToken(lastProp).loc.end.line
       };
     }
     var openingLine = sourceCode.lines[opening.line - 1];
@@ -180,7 +181,7 @@ module.exports = function(context) {
 
       if (correctColumn !== null) {
         expectedNextLine = tokens.lastProp &&
-          (tokens.lastProp.line === tokens.closing.line);
+          (tokens.lastProp.lastLine === tokens.closing.line);
         data.details = ' (expected column ' + (correctColumn + 1) +
           (expectedNextLine ? ' on the next line)' : ')');
       }
diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js
index cbb0705a..bbf24d1f 100644
--- a/lib/rules/jsx-no-bind.js
+++ b/lib/rules/jsx-no-bind.js
@@ -13,6 +13,31 @@ module.exports = function(context) {
   var configuration = context.options[0] || {};
 
   return {
+    CallExpression: function(node) {
+      var callee = node.callee;
+      if (
+        !configuration.allowBind &&
+        callee.type !== 'MemberExpression' ||
+        callee.property.name !== 'bind'
+      ) {
+        return;
+      }
+      var ancestors = context.getAncestors(callee).reverse();
+      for (var i = 0, j = ancestors.length; i < j; i++) {
+        if (
+          !configuration.allowBind &&
+          (ancestors[i].type === 'MethodDefinition' && ancestors[i].key.name === 'render') ||
+          (ancestors[i].type === 'Property' && ancestors[i].key.name === 'render')
+        ) {
+          context.report({
+            node: callee,
+            message: 'JSX props should not use .bind()'
+          });
+          break;
+        }
+      }
+    },
+
     JSXAttribute: function(node) {
       var isRef = configuration.ignoreRefs && node.name.name === 'ref';
       if (isRef || !node.value || !node.value.expression) {
diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js
index a94ac44f..ccec8c60 100644
--- a/lib/rules/require-render-return.js
+++ b/lib/rules/require-render-return.js
@@ -49,9 +49,10 @@ module.exports = Components.detect(function(context, components, utils) {
     if (node.type === 'ClassProperty') {
       var tokens = context.getFirstTokens(node, 2);
       return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
+    } else if (['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
+      return node.key.name;
     }
-
-    return node.key.name;
+    return '';
   }
 
   /**
diff --git a/lib/util/Components.js b/lib/util/Components.js
index a213c293..f8692f17 100644
--- a/lib/util/Components.js
+++ b/lib/util/Components.js
@@ -5,6 +5,7 @@
 'use strict';
 
 var util = require('util');
+var doctrine = require('doctrine');
 var variableUtil = require('./variable');
 var pragmaUtil = require('./pragma');
 
@@ -90,6 +91,10 @@ Components.prototype.list = function() {
     node = this._list[i].node;
     while (!component && node.parent) {
       node = node.parent;
+      // Stop moving up if we reach a decorator
+      if (node.type === 'Decorator') {
+        break;
+      }
       component = this.get(node);
     }
     if (component) {
@@ -156,12 +161,41 @@ function componentRule(rule, context) {
      * @returns {Boolean} True if the node is a React ES6 component, false if not
      */
     isES6Component: function(node) {
+      if (utils.isExplicitComponent(node)) {
+        return true;
+      }
+
       if (!node.superClass) {
         return false;
       }
       return new RegExp('^(' + pragma + '\\.)?Component$').test(sourceCode.getText(node.superClass));
     },
 
+    /**
+     * Check if the node is explicitly declared as a descendant of a React Component
+     *
+     * @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression).
+     * @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not
+     */
+    isExplicitComponent: function(node) {
+      var comment = sourceCode.getJSDocComment(node);
+
+      if (comment === null) {
+        return false;
+      }
+
+      var commentAst = doctrine.parse(comment.value, {
+        unwrap: true,
+        tags: ['extends', 'augments']
+      });
+
+      var relevantTags = commentAst.tags.filter(function(tag) {
+        return tag.name === 'React.Component';
+      });
+
+      return relevantTags.length > 0;
+    },
+
     /**
      * Check if the node is returning JSX
      *
@@ -270,9 +304,15 @@ function componentRule(rule, context) {
       var scope = context.getScope();
       while (scope) {
         var node = scope.block;
+        var isClass = node.type === 'ClassExpression';
         var isFunction = /Function/.test(node.type); // Ignore non functions
         var isNotMethod = !node.parent || node.parent.type !== 'MethodDefinition'; // Ignore classes methods
         var isNotArgument = !node.parent || node.parent.type !== 'CallExpression'; // Ignore arguments (callback, etc.)
+        // Stop moving up if we reach a class
+        if (isClass) {
+          return null;
+        }
+        // Return the node if it is a function that is not a class method or an argument (like a callback)
         if (isFunction && isNotMethod && isNotArgument) {
           return node;
         }
diff --git a/package.json b/package.json
index eb05a64d..08091238 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-react",
-  "version": "5.1.0",
+  "version": "5.1.1",
   "author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
   "description": "React specific linting rules for ESLint",
   "main": "index.js",
@@ -22,6 +22,9 @@
   },
   "homepage": "https://github.com/yannickcr/eslint-plugin-react",
   "bugs": "https://github.com/yannickcr/eslint-plugin-react/issues",
+  "dependencies": {
+    "doctrine": "^1.2.0"
+  },
   "devDependencies": {
     "babel-eslint": "6.0.4",
     "coveralls": "2.11.9",


Test patch:
diff --git a/tests/lib/rules/display-name.js b/tests/lib/rules/display-name.js
index f2fa89f7..1d831088 100644
--- a/tests/lib/rules/display-name.js
+++ b/tests/lib/rules/display-name.js
@@ -153,6 +153,16 @@ ruleTester.run('display-name', rule, {
       '});'
     ].join('\n'),
     parserOptions: parserOptions
+  }, {
+    code: [
+      'module.exports = React.createClass({',
+      '  "displayName": "Hello",',
+      '  "render": function() {',
+      '    return <div>Hello {this.props.name}</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    parserOptions: parserOptions
   }, {
     code: [
       'var Hello = React.createClass({',
diff --git a/tests/lib/rules/jsx-closing-bracket-location.js b/tests/lib/rules/jsx-closing-bracket-location.js
index e28f2505..1f126947 100644
--- a/tests/lib/rules/jsx-closing-bracket-location.js
+++ b/tests/lib/rules/jsx-closing-bracket-location.js
@@ -186,6 +186,38 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
     ].join('\n'),
     options: [{location: 'line-aligned'}],
     parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '  console.log(\'bar\');',
+      '}}/>'
+    ].join('\n'),
+    options: [{location: 'after-props'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '  console.log(\'bar\');',
+      '}}/>'
+    ].join('\n'),
+    options: [{location: 'props-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '  console.log(\'bar\');',
+      '}}/>'
+    ].join('\n'),
+    options: [{location: 'tag-aligned'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={function() {',
+      '  console.log(\'bar\');',
+      '}}/>'
+    ].join('\n'),
+    options: [{location: 'line-aligned'}],
+    parserOptions: parserOptions
   }, {
     code: [
       '<Provider store>',
diff --git a/tests/lib/rules/jsx-no-bind.js b/tests/lib/rules/jsx-no-bind.js
index e751ab32..f6e68637 100644
--- a/tests/lib/rules/jsx-no-bind.js
+++ b/tests/lib/rules/jsx-no-bind.js
@@ -83,6 +83,30 @@ ruleTester.run('jsx-no-bind', rule, {
       errors: [{message: 'JSX props should not use .bind()'}],
       parser: 'babel-eslint'
     },
+    {
+      code: [
+        'var Hello = React.createClass({',
+        '  render: function() {',
+        '    const click = this.someMethod.bind(this);',
+        '    return <div onClick={click}>Hello {this.state.name}</div>;',
+        '  }',
+        '});'
+      ].join('\n'),
+      errors: [{message: 'JSX props should not use .bind()'}],
+      parser: 'babel-eslint'
+    },
+    {
+      code: [
+        'class Hello23 extends React.Component {',
+        '  render() {',
+        '    const click = this.someMethod.bind(this);',
+        '    return <div onClick={click}>Hello {this.state.name}</div>;',
+        '  }',
+        '};'
+      ].join('\n'),
+      errors: [{message: 'JSX props should not use .bind()'}],
+      parser: 'babel-eslint'
+    },
 
     // Arrow functions
     {
diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js
index 86e2f035..8ab3292b 100644
--- a/tests/lib/rules/prop-types.js
+++ b/tests/lib/rules/prop-types.js
@@ -1206,6 +1206,23 @@ ruleTester.run('prop-types', rule, {
         '}'
       ].join('\n'),
       parser: 'babel-eslint'
+    }, {
+      // Should stop at the class when searching for a parent component
+      code: [
+        'export default (ComposedComponent) => class Something extends SomeOtherComponent {',
+        '  someMethod = ({width}) => {}',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint'
+    }, {
+      // Should stop at the decorator when searching for a parent component
+      code: [
+        '@asyncConnect([{',
+        '  promise: ({dispatch}) => {}',
+        '}])',
+        'class Something extends Component {}'
+      ].join('\n'),
+      parser: 'babel-eslint'
     }
   ],
 
@@ -1257,6 +1274,22 @@ ruleTester.run('prop-types', rule, {
         column: 35,
         type: 'Identifier'
       }]
+    }, {
+      code: [
+        '/** @extends React.Component */',
+        'class Hello extends ChildComponent {',
+        '  render() {',
+        '    return <div>Hello {this.props.name}</div>;',
+        '  }',
+        '}'
+      ].join('\n'),
+      parserOptions: parserOptions,
+      errors: [{
+        message: '\'name\' is missing in props validation',
+        line: 4,
+        column: 35,
+        type: 'Identifier'
+      }]
     }, {
       code: [
         'class Hello extends React.Component {',
diff --git a/tests/lib/rules/require-render-return.js b/tests/lib/rules/require-render-return.js
index 8da8fbff..9c805b93 100644
--- a/tests/lib/rules/require-render-return.js
+++ b/tests/lib/rules/require-render-return.js
@@ -78,6 +78,15 @@ ruleTester.run('require-render-return', rule, {
       '}'
     ].join('\n'),
     parserOptions: parserOptions
+  }, {
+    // Stateless arrow function
+    code: [
+      'var Hello = () => (',
+      '  <div></div>',
+      ');'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    parserOptions: parserOptions
   }, {
     // Return in a switch...case
     code: [