Repository: yannickcr/eslint-plugin-react
Pull request #475 (2016-03-01 17:13:14)

Catch bind in render when assigned to a variable (#474)
Takes care of part of the issue brought up in #474.


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-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..ca6e3460 100644
--- a/lib/util/Components.js
+++ b/lib/util/Components.js
@@ -90,6 +90,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) {
@@ -270,9 +274,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..1eb5b612 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",


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-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..8c254e4c 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'
     }
   ],
 
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: [