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

Issue:
Rule Proposal: Require "shouldComponentUpdate" or PureRenderMixin to be applied
On our team, we want to ensure all components are optimized. This means either PureRenderMixin must be applied on the component, or the component must have a `shouldComponentUpdate` method defined. It would be great if ESLint could warn developers when they have forgotten to optimize.

Pull request description:
Adds new require-optimization rule.
Fixes #240. Adds a new rule called `require-optimization` which ensures all components either have a `shouldComponentUpdate` method or a `PureRenderMixin`.

Tested on several large React project repos without any issues using a beta release of this rule here (https://travis-ci.org/Eight-Solutions/eslint-plugin-cumul8). General feedback from users of this rule so far have been very positive.

Patch:
diff --git a/README.md b/README.md
index abf3161c..64086954 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,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
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/index.js b/index.js
index 19693475..b75a3a9e 100644
--- a/index.js
+++ b/index.js
@@ -44,7 +44,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'),
+    'require-optimization': require('./lib/rules/require-optimization')
   },
   configs: {
     recommended: {
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]);
+      }
+    }
+  };
+});


Test patch:
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
+  }]
+});