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

Issue:
RFC: Rule for legacy return value of ReactDOM.render
This is a RFC for creating an eslint rule to flag the return value of `ReactDOM.render` as a deprecated feature.

The documentation for this is in progress of being updated at: https://github.com/facebook/react/pull/6400

While there is no timeline on when this deprecation will be removed and made mainstream, I think it would be beneficial to the community. This deprecation in particular is something that cannot be done at runtime since it depends on knowledge outside of the callsite, which makes it a perfect candidate for an eslint rule.

There are two preferred recommendations to work with this:

```js
// Bad
const inst = ReactDOM.render(<App />, document.body);
doSomethingWithInst(inst);

// Good
ReactDOM.render(<App ref={doSomethingWithInst} />, document.body);
ReactDOM.render(<App />, document.body, doSomethingWithInst);
```

cc @jimfb @gaearon 

Pull request:
no-render-return-value. closes #531
As documented initially in https://github.com/facebook/react/pull/6400 and on the documentation website at http://facebook.github.io/react/docs/top-level-api.html#reactdom.render

The return value of `ReactDOM.render` is considered legacy. It's rather hard to deprecate the return value of an element with runtime code, but fairly straight-forward with static analysis so this is intended to support the communities education and eventual migration from using that pattern.

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..9eb3fd43 100644
--- a/README.md
+++ b/README.md
@@ -103,16 +103,17 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
 * [jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX (fixable)
 * [jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes (fixable)
 * [jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Enforce or disallow spaces around equal signs in JSX attributes (fixable)
+* [jsx-filename-extension](docs/rules/jsx-filename-extension.md): Restrict file extensions that may contain JSX
 * [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/docs/rules/jsx-filename-extension.md b/docs/rules/jsx-filename-extension.md
new file mode 100644
index 00000000..ec8c62ec
--- /dev/null
+++ b/docs/rules/jsx-filename-extension.md
@@ -0,0 +1,35 @@
+# Restrict file extensions that may contain JSX (jsx-filename-extension)
+
+## Rule Details
+
+The following pattern is considered a warning:
+
+```jsx
+// filename: MyComponent.js
+function MyComponent() {
+  return <div />;
+}
+```
+
+The following pattern is not considered a warning:
+
+```jsx
+// filename: MyComponent.jsx
+function MyComponent() {
+  return <div />;
+}
+```
+
+## Rule Options
+
+The set of allowed extensions is configurable. By default '.jsx' is allowed. If you wanted to allow both '.jsx' and '.js', the configuration would be:
+
+```js
+"rules": {
+  "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
+}
+```
+
+## When Not To Use It
+
+If you don't care about restricting the file extensions that may contain JSX.
diff --git a/index.js b/index.js
index 19693475..b8c5cecc 100644
--- a/index.js
+++ b/index.js
@@ -14,6 +14,7 @@ module.exports = {
     'no-deprecated': require('./lib/rules/no-deprecated'),
     'no-did-mount-set-state': require('./lib/rules/no-did-mount-set-state'),
     'no-did-update-set-state': require('./lib/rules/no-did-update-set-state'),
+    'no-render-return-value': require('./lib/rules/no-render-return-value'),
     'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'),
     'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'),
     'jsx-handler-names': require('./lib/rules/jsx-handler-names'),
@@ -44,7 +45,8 @@ module.exports = {
     'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'),
     'require-render-return': require('./lib/rules/require-render-return'),
     'jsx-first-prop-new-line': require('./lib/rules/jsx-first-prop-new-line'),
-    'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank')
+    'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
+    'jsx-filename-extension': require('./lib/rules/jsx-filename-extension')
   },
   configs: {
     recommended: {
@@ -66,6 +68,7 @@ module.exports = {
         'react/no-direct-mutation-state': 2,
         'react/no-is-mounted': 2,
         'react/no-unknown-property': 2,
+        'react/no-render-return-value': 2,
         'react/prop-types': 2,
         'react/react-in-jsx-scope': 2
       }
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-filename-extension.js b/lib/rules/jsx-filename-extension.js
new file mode 100644
index 00000000..6fa7e1dc
--- /dev/null
+++ b/lib/rules/jsx-filename-extension.js
@@ -0,0 +1,68 @@
+/**
+ * @fileoverview Restrict file extensions that may contain JSX
+ * @author Joe Lencioni
+ */
+'use strict';
+
+var path = require('path');
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+var DEFAULTS = {
+  extensions: ['.jsx']
+};
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = function(context) {
+
+
+  function getExtensionsConfig() {
+    return context.options[0] && context.options[0].extensions || DEFAULTS.extensions;
+  }
+
+  // --------------------------------------------------------------------------
+  // Public
+  // --------------------------------------------------------------------------
+
+  return {
+
+    JSXElement: function(node) {
+      var allowedExtensions = getExtensionsConfig();
+      var filename = context.getFilename();
+
+      var isAllowedExtension = allowedExtensions.some(function (extension) {
+        return filename.slice(-extension.length) === extension;
+      });
+
+      if (isAllowedExtension) {
+        return;
+      }
+
+      var extension = path.extname(filename);
+
+      context.report({
+        node: node,
+        message: 'JSX not allowed in files with extension \'' + extension + '\''
+      });
+    }
+  };
+
+};
+
+module.exports.schema = [{
+  type: 'object',
+  properties: {
+    extensions: {
+      type: 'array',
+      items: {
+        type: 'string'
+      }
+    }
+  },
+  additionalProperties: false
+}];
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/no-render-return-value.js b/lib/rules/no-render-return-value.js
new file mode 100644
index 00000000..cdc54ddb
--- /dev/null
+++ b/lib/rules/no-render-return-value.js
@@ -0,0 +1,62 @@
+/**
+ * @fileoverview Prevent usage of the return value of React.render
+ * @author Dustan Kasten
+ */
+'use strict';
+
+var versionUtil = require('../util/version');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = function(context) {
+
+  // --------------------------------------------------------------------------
+  // Public
+  // --------------------------------------------------------------------------
+
+  return {
+
+    CallExpression: function(node) {
+      var callee = node.callee;
+      var parent = node.parent;
+      if (callee.type !== 'MemberExpression') {
+        return;
+      }
+
+      var calleeObjectName = /^ReactDOM$/;
+      if (versionUtil.test(context, '15.0.0')) {
+        calleeObjectName = /^ReactDOM$/;
+      } else if (versionUtil.test(context, '0.14.0')) {
+        calleeObjectName = /^React(DOM)?$/;
+      } else if (versionUtil.test(context, '0.13.0')) {
+        calleeObjectName = /^React$/;
+      }
+
+      if (
+        callee.object.type !== 'Identifier' ||
+        !calleeObjectName.test(callee.object.name) ||
+        callee.property.name !== 'render'
+      ) {
+        return;
+      }
+
+      if (
+        parent.type === 'VariableDeclarator' ||
+        parent.type === 'Property' ||
+        parent.type === 'ReturnStatement' ||
+        parent.type === 'ArrowFunctionExpression'
+      ) {
+        context.report({
+          node: callee,
+          message: 'Do not depend on the return value from ' + callee.object.name + '.render'
+        });
+      }
+    }
+  };
+
+};
+
+module.exports.schema = [];
+
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-filename-extension.js b/tests/lib/rules/jsx-filename-extension.js
new file mode 100644
index 00000000..f40e3d5c
--- /dev/null
+++ b/tests/lib/rules/jsx-filename-extension.js
@@ -0,0 +1,66 @@
+/**
+ * @fileoverview Restrict file extensions that may contain JSX
+ * @author Joe Lencioni
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/jsx-filename-extension');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+  ecmaVersion: 6,
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Code Snippets
+// ------------------------------------------------------------------------------
+
+var withJSX = 'module.exports = function MyComponent() { return <div />; }';
+var withoutJSX = 'module.exports = {}';
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('jsx-filename-extension', rule, {
+
+  valid: [
+    {
+      filename: 'MyComponent.jsx',
+      code: withJSX,
+      parserOptions: parserOptions
+    }, {
+      filename: 'MyComponent.js',
+      options: [{extensions: ['.js', '.jsx']}],
+      code: withJSX,
+      parserOptions: parserOptions
+    }, {
+      filename: 'notAComponent.js',
+      code: withoutJSX
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'MyComponent.js',
+      code: withJSX,
+      parserOptions: parserOptions,
+      errors: [{message: 'JSX not allowed in files with extension \'.js\''}]
+    }, {
+      filename: 'MyComponent.jsx',
+      code: withJSX,
+      parserOptions: parserOptions,
+      options: [{extensions: ['.js']}],
+      errors: [{message: 'JSX not allowed in files with extension \'.jsx\''}]
+    }
+  ]
+
+});
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/no-render-return-value.js b/tests/lib/rules/no-render-return-value.js
new file mode 100644
index 00000000..eb8a5c78
--- /dev/null
+++ b/tests/lib/rules/no-render-return-value.js
@@ -0,0 +1,134 @@
+/**
+ * @fileoverview Prevent usage of setState
+ * @author Mark Dalgleish
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-render-return-value');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+  ecmaVersion: 6,
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('no-render-return-value', rule, {
+
+  valid: [{
+    code: [
+      'ReactDOM.render(<div />, document.body);'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'let node;',
+      'ReactDOM.render(<div ref={ref => node = ref}/>, document.body);'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: 'ReactDOM.render(<div ref={ref => this.node = ref}/>, document.body);',
+    parserOptions: parserOptions,
+    settings: {
+      react: {
+        version: '0.14.0'
+      }
+    }
+  }, {
+    code: 'React.render(<div ref={ref => this.node = ref}/>, document.body);',
+    parserOptions: parserOptions,
+    settings: {
+      react: {
+        version: '0.14.0'
+      }
+    }
+  }, {
+    code: 'React.render(<div ref={ref => this.node = ref}/>, document.body);',
+    parserOptions: parserOptions,
+    settings: {
+      react: {
+        version: '0.13.0'
+      }
+    }
+  }
+  ],
+
+  invalid: [{
+    code: [
+      'var Hello = ReactDOM.render(<div />, document.body);'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    errors: [{
+      message: 'Do not depend on the return value from ReactDOM.render'
+    }]
+  }, {
+    code: [
+      'var o = {',
+      '  inst: ReactDOM.render(<div />, document.body)',
+      '};'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    errors: [{
+      message: 'Do not depend on the return value from ReactDOM.render'
+    }]
+  }, {
+    code: [
+      'function render () {',
+      '  return ReactDOM.render(<div />, document.body)',
+      '}'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    errors: [{
+      message: 'Do not depend on the return value from ReactDOM.render'
+    }]
+  }, {
+    code: 'var render = (a, b) => ReactDOM.render(a, b)',
+    parserOptions: parserOptions,
+    errors: [{
+      message: 'Do not depend on the return value from ReactDOM.render'
+    }]
+  }, {
+    code: 'var inst = React.render(<div />, document.body);',
+    parserOptions: parserOptions,
+    settings: {
+      react: {
+        version: '0.14.0'
+      }
+    },
+    errors: [{
+      message: 'Do not depend on the return value from React.render'
+    }]
+  }, {
+    code: 'var inst = ReactDOM.render(<div />, document.body);',
+    parserOptions: parserOptions,
+    settings: {
+      react: {
+        version: '0.14.0'
+      }
+    },
+    errors: [{
+      message: 'Do not depend on the return value from ReactDOM.render'
+    }]
+  }, {
+    code: 'var inst = React.render(<div />, document.body);',
+    parserOptions: parserOptions,
+    settings: {
+      react: {
+        version: '0.13.0'
+      }
+    },
+    errors: [{
+      message: 'Do not depend on the return value from React.render'
+    }]
+  }]
+});
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: [