Repository: yannickcr/eslint-plugin-react
Pull request #493 (2016-03-08 19:55:25)

Improved multiline prop support for jsx-closing-bracket-location
Currently situation where last prop is both multiline and starts at the same line as the opening bracket is not handled very well.

Example of the issue can be found here: https://github.com/yannickcr/eslint-plugin-react/issues/454#issuecomment-193284260

yannickcr: Thanks. I will review it this week, but I need to do a bit of cleanup in all "indent-related" issues first :|
lixiaoyan: any update on this?
coveralls: 
[![Coverage Status](https://coveralls.io/builds/5649184/badge)](https://coveralls.io/builds/5649184)

Coverage remained the same at 96.947% when pulling **c650c753cb0ed34ff9e5f4a103ef8b7c56c176da on tuures:master** into **aba4f969929c5e765f1597a4706513d8b2fd42ea on yannickcr:master**.

yannickcr: Not yet, sorry. I still have a lot of triage to do in the issues first.

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..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-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..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: [