Repository: yannickcr/eslint-plugin-react
Pull request #604 (2016-05-17 01:48:55)

Alternative Rule for `jsx-curly-spacing`
Fixes https://github.com/yannickcr/eslint-plugin-react/issues/388 and https://github.com/yannickcr/eslint-plugin-react/issues/211


Patch:
diff --git a/README.md b/README.md
index abf3161c..66ec3f69 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,7 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
 * [no-direct-mutation-state](docs/rules/no-direct-mutation-state.md): Prevent direct mutation of `this.state`
 * [no-is-mounted](docs/rules/no-is-mounted.md): Prevent usage of `isMounted`
 * [no-multi-comp](docs/rules/no-multi-comp.md): Prevent multiple component definition per file
+* [no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of `React.render`
 * [no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState`
 * [no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute.
 * [no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
@@ -91,6 +92,7 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
 * [prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition
 * [react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX
 * [require-extension](docs/rules/require-extension.md): Restrict file extensions that may be required
+* [require-optimization](docs/rules/require-optimization.md): Enforce React components to have a shouldComponentUpdate method
 * [require-render-return](docs/rules/require-render-return.md): Enforce ES5 or ES6 class for returning value in render function
 * [self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children
 * [sort-comp](docs/rules/sort-comp.md): Enforce component methods order
@@ -103,6 +105,7 @@ 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](docs/rules/jsx-indent.md): Validate JSX indentation
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/docs/rules/no-render-return-value.md b/docs/rules/no-render-return-value.md
new file mode 100644
index 00000000..d028ec09
--- /dev/null
+++ b/docs/rules/no-render-return-value.md
@@ -0,0 +1,24 @@
+# Prevent usage of the return value of React.render (no-render-return-value)
+
+> `ReactDOM.render()` currently returns a reference to the root `ReactComponent` instance. However, using this return value is legacy and should be avoided because future versions of React may render components asynchronously in some cases. If you need a reference to the root `ReactComponent` instance, the preferred solution is to attach a [callback ref](http://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute) to the root element.
+
+Source: [React Top-Level API documentation](http://facebook.github.io/react/docs/top-level-api.html#reactdom.render)
+
+## Rule Details
+
+This rule will warn you if you try to use the `ReactDOM.render()` return value.
+
+The following pattern is considered warning:
+
+```js
+const inst = ReactDOM.render(<App />, document.body);
+doSomethingWithInst(inst);
+```
+
+The following patterns are not considered warning:
+
+```js
+ReactDOM.render(<App ref={doSomethingWithInst} />, document.body);
+
+ReactDOM.render(<App />, document.body, doSomethingWithInst);
+```
diff --git a/docs/rules/require-optimization.md b/docs/rules/require-optimization.md
new file mode 100644
index 00000000..62a45a3b
--- /dev/null
+++ b/docs/rules/require-optimization.md
@@ -0,0 +1,65 @@
+# Enforce React components to have a shouldComponentUpdate method (require-optimization)
+
+This rule prevents you from creating React components without declaring a `shouldComponentUpdate` method.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```js
+class YourComponent extends React.Component {
+
+}
+```
+
+```js
+React.createClass({
+});
+```
+
+The following patterns are not considered warnings:
+
+```js
+class YourComponent extends React.Component {
+	shouldComponentUpdate () {
+		return false;
+	}
+}
+```
+
+```js
+React.createClass({
+	shouldComponentUpdate: function () {
+		return false;
+	}
+});
+```
+
+```js
+React.createClass({
+	mixins: [PureRenderMixin]
+});
+```
+
+```js
+@reactMixin.decorate(PureRenderMixin)
+React.createClass({
+
+});
+```
+
+## Rule Options
+
+```js
+...
+"require-optimization": [<enabled>]
+...
+```
+
+### Example
+
+```js
+...
+"require-optimization": 2
+...
+```
diff --git a/docs/rules/self-closing-comp.md b/docs/rules/self-closing-comp.md
index 3f12ea53..41d1e9dd 100644
--- a/docs/rules/self-closing-comp.md
+++ b/docs/rules/self-closing-comp.md
@@ -17,5 +17,56 @@ var contentContainer = <div className="content"></div>;
 
 var HelloJohn = <Hello name="John" />;
 
-var Profile = <Hello name="John"><img src="picture.png" /></Hello>; 
+var Profile = <Hello name="John"><img src="picture.png" /></Hello>;
+```
+
+## Rule Options
+
+The rule can take one argument to select types of tags, which should be self-closed when this is possible. By default only custom components tags should be self-closed.
+
+```js
+...
+"self-closing-comp": ["error", {
+  "component": true,
+  "html": false
+}]
+...
+```
+
+### `component`
+
+When `true`, custom components tags should be self-closed.
+
+The following patterns are considered warnings:
+
+```js
+var HelloJohn = <Hello name="John"></Hello>;
+```
+
+The following patterns are not considered warnings:
+
+```js
+var contentContainer = <div className="content"></div>;
+
+var HelloJohn = <Hello name="John" />;
+
+var Profile = <Hello name="John"><img src="picture.png" /></Hello>;
+```
+
+### `html`
+
+When `true`, html components tags should be self-closed.
+
+The following patterns are considered warnings:
+
+```js
+var contentContainer = <div className="content"></div>;
+```
+
+The following patterns are not considered warnings:
+
+```js
+var contentContainer = <div className="content" />;
+
+var contentContainer = <div className="content"><div /></div>;
 ```
diff --git a/index.js b/index.js
index 19693475..683ea715 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,9 @@ 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'),
+    'require-optimization': require('./lib/rules/require-optimization')
   },
   configs: {
     recommended: {
@@ -66,6 +69,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/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-curly-spacing.js b/lib/rules/jsx-curly-spacing.js
index a580dd90..5c0eb555 100644
--- a/lib/rules/jsx-curly-spacing.js
+++ b/lib/rules/jsx-curly-spacing.js
@@ -19,6 +19,7 @@ module.exports = function(context) {
   var sourceCode = context.getSourceCode();
   var spaced = context.options[0] === 'always';
   var multiline = context.options[1] ? context.options[1].allowMultiline : true;
+  var alternative = context.options[1] ? context.options[1].alternative : false;
 
   // --------------------------------------------------------------------------
   // Helpers
@@ -149,18 +150,43 @@ module.exports = function(context) {
    * @param {Token} last The last token to check (should be closing brace)
    * @returns {void}
    */
-  function validateBraceSpacing(node, first, second, penultimate, last) {
+  function validateBraceSpacing(node, first, second, third, antepenultimate, penultimate, last) {
     if (spaced) {
-      if (!sourceCode.isSpaceBetweenTokens(first, second)) {
-        reportRequiredBeginningSpace(node, first);
-      } else if (!multiline && isMultiline(first, second)) {
-        reportNoBeginningNewline(node, first);
-      }
-
-      if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
-        reportRequiredEndingSpace(node, last);
-      } else if (!multiline && isMultiline(penultimate, last)) {
-        reportNoEndingNewline(node, last);
+      if (!alternative) {
+        if (!sourceCode.isSpaceBetweenTokens(first, second)) {
+          reportRequiredBeginningSpace(node, first);
+        } else if (!multiline && isMultiline(first, second)) {
+          reportNoBeginningNewline(node, first);
+        }
+
+        if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
+          reportRequiredEndingSpace(node, last);
+        } else if (!multiline && isMultiline(penultimate, last)) {
+          reportNoEndingNewline(node, last);
+        }
+
+      // Object literal
+      } else if (first.value === second.value) {
+        if (sourceCode.isSpaceBetweenTokens(first, second)) {
+          reportNoBeginningSpace(node, first);
+        }
+        if (sourceCode.isSpaceBetweenTokens(penultimate, last)) {
+          reportNoEndingSpace(node, last);
+        }
+        if (!sourceCode.isSpaceBetweenTokens(second, third)) {
+          reportRequiredBeginningSpace(node, second);
+        }
+        if (!sourceCode.isSpaceBetweenTokens(antepenultimate, penultimate)) {
+          reportRequiredEndingSpace(node, penultimate);
+        }
+
+      } else {
+        if (!sourceCode.isSpaceBetweenTokens(first, second)) {
+          reportRequiredBeginningSpace(node, first);
+        }
+        if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
+          reportRequiredEndingSpace(node, last);
+        }
       }
 
       return;
@@ -186,8 +212,10 @@ module.exports = function(context) {
       var last = sourceCode.getLastToken(node);
       var second = context.getTokenAfter(first);
       var penultimate = sourceCode.getTokenBefore(last);
+      var third = context.getTokenAfter(second);
+      var antepenultimate = sourceCode.getTokenBefore(penultimate);
 
-      validateBraceSpacing(node, first, second, penultimate, last);
+      validateBraceSpacing(node, first, second, third, antepenultimate, penultimate, last);
     }
   };
 };
@@ -199,6 +227,9 @@ module.exports.schema = [{
   properties: {
     allowMultiline: {
       type: 'boolean'
+    },
+    alternative: {
+      type: 'boolean'
     }
   },
   additionalProperties: false
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-optimization.js b/lib/rules/require-optimization.js
new file mode 100644
index 00000000..a64afef7
--- /dev/null
+++ b/lib/rules/require-optimization.js
@@ -0,0 +1,150 @@
+/**
+ * @fileoverview Enforce React components to have a shouldComponentUpdate method
+ * @author Evgueni Naverniouk
+ */
+'use strict';
+
+var Components = require('../util/Components');
+
+module.exports = Components.detect(function (context, components) {
+  var MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.';
+
+  /**
+   * Checks to see if our component is decorated by PureRenderMixin via reactMixin
+   * @param {ASTNode} node The AST node being checked.
+   * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not.
+   */
+  var hasPureRenderDecorator = function (node) {
+    if (node.decorators && node.decorators.length) {
+      for (var i = 0, l = node.decorators.length; i < l; i++) {
+        if (
+          node.decorators[i].expression &&
+          node.decorators[i].expression.callee &&
+          node.decorators[i].expression.callee.object &&
+          node.decorators[i].expression.callee.object.name === 'reactMixin' &&
+          node.decorators[i].expression.callee.property &&
+          node.decorators[i].expression.callee.property.name === 'decorate' &&
+          node.decorators[i].expression.arguments &&
+          node.decorators[i].expression.arguments.length &&
+          node.decorators[i].expression.arguments[0].name === 'PureRenderMixin'
+        ) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  };
+
+  /**
+   * Checks if we are declaring a shouldComponentUpdate method
+   * @param {ASTNode} node The AST node being checked.
+   * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not.
+   */
+  var isSCUDeclarеd = function (node) {
+    return Boolean(
+      node &&
+      node.name === 'shouldComponentUpdate'
+    );
+  };
+
+  /**
+   * Checks if we are declaring a PureRenderMixin mixin
+   * @param {ASTNode} node The AST node being checked.
+   * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not.
+   */
+  var isPureRenderDeclared = function (node) {
+    var hasPR = false;
+    if (node.value && node.value.elements) {
+      for (var i = 0, l = node.value.elements.length; i < l; i++) {
+        if (node.value.elements[i].name === 'PureRenderMixin') {
+          hasPR = true;
+          break;
+        }
+      }
+    }
+
+    return Boolean(
+        node &&
+        node.key.name === 'mixins' &&
+        hasPR
+      );
+  };
+
+  /**
+   * Mark shouldComponentUpdate as declared
+   * @param {ASTNode} node The AST node being checked.
+   */
+  var markSCUAsDeclared = function (node) {
+    components.set(node, {
+      hasSCU: true
+    });
+  };
+
+  /**
+   * Reports missing optimization for a given component
+   * @param {Object} component The component to process
+   */
+  var reportMissingOptimization = function (component) {
+    context.report({
+      node: component.node,
+      message: MISSING_MESSAGE,
+      data: {
+        component: component.name
+      }
+    });
+  };
+
+  return {
+    ArrowFunctionExpression: function (node) {
+      // Stateless Functional Components cannot be optimized (yet)
+      markSCUAsDeclared(node);
+    },
+
+    ClassDeclaration: function (node) {
+      if (!hasPureRenderDecorator(node)) {
+        return;
+      }
+      markSCUAsDeclared(node);
+    },
+
+    FunctionExpression: function (node) {
+      // Stateless Functional Components cannot be optimized (yet)
+      markSCUAsDeclared(node);
+    },
+
+    MethodDefinition: function (node) {
+      if (!isSCUDeclarеd(node.key)) {
+        return;
+      }
+      markSCUAsDeclared(node);
+    },
+
+    ObjectExpression: function (node) {
+      // Search for the shouldComponentUpdate declaration
+      for (var i = 0, l = node.properties.length; i < l; i++) {
+        if (
+          !node.properties[i].key || (
+            !isSCUDeclarеd(node.properties[i].key) &&
+            !isPureRenderDeclared(node.properties[i])
+          )
+        ) {
+          continue;
+        }
+        markSCUAsDeclared(node);
+      }
+    },
+
+    'Program:exit': function () {
+      var list = components.list();
+
+      // Report missing shouldComponentUpdate for all components
+      for (var component in list) {
+        if (!list.hasOwnProperty(component) || list[component].hasSCU) {
+          continue;
+        }
+        reportMissingOptimization(list[component]);
+      }
+    }
+  };
+});
diff --git a/lib/rules/self-closing-comp.js b/lib/rules/self-closing-comp.js
index 39719136..8e4f38c8 100644
--- a/lib/rules/self-closing-comp.js
+++ b/lib/rules/self-closing-comp.js
@@ -30,6 +30,14 @@ module.exports = function(context) {
     return true;
   }
 
+  function isShouldBeSelfClosed(node) {
+    var configuration = context.options[0] || {component: true};
+    return (
+      configuration.component && isComponent(node) ||
+      configuration.html && isTagName(node.name.name)
+    ) && !node.selfClosing && !hasChildren(node);
+  }
+
   // --------------------------------------------------------------------------
   // Public
   // --------------------------------------------------------------------------
@@ -37,7 +45,8 @@ module.exports = function(context) {
   return {
 
     JSXOpeningElement: function(node) {
-      if (!isComponent(node) || node.selfClosing || hasChildren(node)) {
+
+      if (!isShouldBeSelfClosed(node)) {
         return;
       }
       context.report({
@@ -49,4 +58,17 @@ module.exports = function(context) {
 
 };
 
-module.exports.schema = [];
+module.exports.schema = [{
+  type: 'object',
+  properties: {
+    component: {
+      default: true,
+      type: 'boolean'
+    },
+    html: {
+      default: false,
+      type: 'boolean'
+    }
+  },
+  additionalProperties: false
+}];
diff --git a/lib/util/Components.js b/lib/util/Components.js
index ca6e3460..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');
 
@@ -160,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
      *
diff --git a/package.json b/package.json
index 1eb5b612..08091238 100644
--- a/package.json
+++ b/package.json
@@ -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/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-curly-spacing.js b/tests/lib/rules/jsx-curly-spacing.js
index 0c6d1a7c..084196bc 100644
--- a/tests/lib/rules/jsx-curly-spacing.js
+++ b/tests/lib/rules/jsx-curly-spacing.js
@@ -83,6 +83,22 @@ ruleTester.run('jsx-curly-spacing', rule, {
     options: ['never'],
     parserOptions: parserOptions,
     parser: 'babel-eslint'
+  }, {
+    code: '<App foo={ bar } />;',
+    options: ['always', {alternative: true}],
+    parserOptions: parserOptions
+  }, {
+    code: '<App foo={{ bar: true, baz: true }} />;',
+    options: ['always', {alternative: true}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App foo={',
+      'bar',
+      '} />;'
+    ].join('\n'),
+    options: ['always', {alternative: true}],
+    parserOptions: parserOptions
   }],
 
   invalid: [{
@@ -185,5 +201,41 @@ ruleTester.run('jsx-curly-spacing', rule, {
       message: 'There should be no newline before \'}\''
     }],
     parserOptions: parserOptions
+  }, {
+    code: '<App foo={bar} />;',
+    options: ['always', {alternative: true}],
+    errors: [{
+      message: 'A space is required after \'{\''
+    }, {
+      message: 'A space is required before \'}\''
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: '<App foo={ bar} />;',
+    options: ['always', {alternative: true}],
+    errors: [{
+      message: 'A space is required before \'}\''
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: '<App foo={bar } />;',
+    options: ['always', {alternative: true}],
+    errors: [{
+      message: 'A space is required after \'{\''
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: '<App foo={ {bar: true, baz: true} } />;',
+    options: ['always', {alternative: true}],
+    errors: [{
+      message: 'There should be no space after \'{\''
+    }, {
+      message: 'A space is required after \'{\''
+    }, {
+      message: 'A space is required before \'}\''
+    }, {
+      message: 'There should be no space before \'}\''
+    }],
+    parserOptions: parserOptions
   }]
 });
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 8c254e4c..8ab3292b 100644
--- a/tests/lib/rules/prop-types.js
+++ b/tests/lib/rules/prop-types.js
@@ -1274,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-optimization.js b/tests/lib/rules/require-optimization.js
new file mode 100644
index 00000000..a973d613
--- /dev/null
+++ b/tests/lib/rules/require-optimization.js
@@ -0,0 +1,133 @@
+/**
+ * @fileoverview Enforce React components to have a shouldComponentUpdate method
+ * @author Evgueni Naverniouk
+ */
+'use strict';
+
+var rule = require('../../../lib/rules/require-optimization');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+  ecmaVersion: 6,
+  sourceType: 'module'
+};
+
+var MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.';
+
+var ruleTester = new RuleTester();
+ruleTester.run('react-require-optimization', rule, {
+  valid: [{
+    code: [
+      'class A {}'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'import React from "react";' +
+      'class YourComponent extends React.Component {' +
+      'shouldComponentUpdate () {}' +
+      '}'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'import React, {Component} from "react";' +
+      'class YourComponent extends Component {' +
+      'shouldComponentUpdate () {}' +
+      '}'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'import React from "react";' +
+      'React.createClass({' +
+      'shouldComponentUpdate: function () {}' +
+      '})'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'import React from "react";' +
+      'React.createClass({' +
+      'mixins: [PureRenderMixin]' +
+      '})'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '@reactMixin.decorate(PureRenderMixin)',
+      'class DecoratedComponent extends Component {' +
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'const FunctionalComponent = function (props) {' +
+      'return <div />;' +
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'const FunctionalComponent = (props) => {' +
+      'return <div />;' +
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    parserOptions: parserOptions
+  }],
+
+  invalid: [{
+    code: [
+      'import React from "react";' +
+      'class YourComponent extends React.Component {}'
+    ].join('\n'),
+    errors: [{
+      message: MESSAGE
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'import React, {Component} from "react";' +
+      'class YourComponent extends Component {}'
+    ].join('\n'),
+    errors: [{
+      message: MESSAGE
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'import React from "react";' +
+      'React.createClass({' +
+      '})'
+    ].join('\n'),
+    errors: [{
+      message: MESSAGE
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'import React from "react";' +
+      'React.createClass({' +
+      'mixins: [RandomMixin]' +
+      '})'
+    ].join('\n'),
+    errors: [{
+      message: MESSAGE
+    }],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '@reactMixin.decorate(SomeOtherMixin)',
+      'class DecoratedComponent extends Component {' +
+      '}'
+    ].join('\n'),
+    errors: [{
+      message: MESSAGE
+    }],
+    parser: 'babel-eslint',
+    parserOptions: parserOptions
+  }]
+});
diff --git a/tests/lib/rules/self-closing-comp.js b/tests/lib/rules/self-closing-comp.js
index df3a6828..b038fac8 100644
--- a/tests/lib/rules/self-closing-comp.js
+++ b/tests/lib/rules/self-closing-comp.js
@@ -50,6 +50,64 @@ ruleTester.run('self-closing-comp', rule, {
     }, {
       code: 'var HelloJohn = <Hello name="John">&nbsp;</Hello>;',
       parserOptions: parserOptions
+    }, {
+      code: 'var contentContainer = <div className="content"></div>;',
+      options: [],
+      parserOptions: parserOptions
+    }, {
+      code: 'var HelloJohn = <Hello name="John" />;',
+      options: [],
+      parserOptions: parserOptions
+    }, {
+      code: 'var Profile = <Hello name="John"><img src="picture.png" /></Hello>;',
+      options: [],
+      parserOptions: parserOptions
+    }, {
+      code: '\
+      <Hello>\
+        <Hello name="John" />\
+      </Hello>',
+      options: [],
+      parserOptions: parserOptions
+    }, {
+      code: 'var HelloJohn = <div>&nbsp;</div>;',
+      options: [],
+      parserOptions: parserOptions
+    }, {
+      code: 'var HelloJohn = <div>{\' \'}</div>;',
+      options: [],
+      parserOptions: parserOptions
+    }, {
+      code: 'var HelloJohn = <Hello name="John">&nbsp;</Hello>;',
+      options: [],
+      parserOptions: parserOptions
+    }, {
+      code: 'var HelloJohn = <Hello name="John"></Hello>;',
+      options: [{component: false}],
+      parserOptions: parserOptions
+    }, {
+      code: 'var HelloJohn = <Hello name="John">\n</Hello>;',
+      options: [{component: false}],
+      parserOptions: parserOptions
+    }, {
+      code: 'var HelloJohn = <Hello name="John"> </Hello>;',
+      options: [{component: false}],
+      parserOptions: parserOptions
+    }, {
+      code: 'var contentContainer = <div className="content" />;',
+      options: [{html: true}],
+      parserOptions: parserOptions
+    }, {
+      code: 'var contentContainer = <div className="content"><img src="picture.png" /></div>;',
+      options: [{html: true}],
+      parserOptions: parserOptions
+    }, {
+      code: '\
+      <div>\
+        <div className="content" />\
+      </div>',
+      options: [{html: true}],
+      parserOptions: parserOptions
     }
   ],
 
@@ -72,6 +130,49 @@ ruleTester.run('self-closing-comp', rule, {
       errors: [{
         message: 'Empty components are self-closing'
       }]
+    },
+    {
+      code: 'var HelloJohn = <Hello name="John"></Hello>;',
+      options: [],
+      parserOptions: parserOptions,
+      errors: [{
+        message: 'Empty components are self-closing'
+      }]
+    }, {
+      code: 'var HelloJohn = <Hello name="John">\n</Hello>;',
+      options: [],
+      parserOptions: parserOptions,
+      errors: [{
+        message: 'Empty components are self-closing'
+      }]
+    }, {
+      code: 'var HelloJohn = <Hello name="John"> </Hello>;',
+      options: [],
+      parserOptions: parserOptions,
+      errors: [{
+        message: 'Empty components are self-closing'
+      }]
+    }, {
+      code: 'var contentContainer = <div className="content"></div>;',
+      options: [{html: true}],
+      parserOptions: parserOptions,
+      errors: [{
+        message: 'Empty components are self-closing'
+      }]
+    }, {
+      code: 'var contentContainer = <div className="content">\n</div>;',
+      options: [{html: true}],
+      parserOptions: parserOptions,
+      errors: [{
+        message: 'Empty components are self-closing'
+      }]
+    }, {
+      code: 'var contentContainer = <div className="content"> </div>;',
+      options: [{html: true}],
+      parserOptions: parserOptions,
+      errors: [{
+        message: 'Empty components are self-closing'
+      }]
     }
   ]
 });