Repository: yannickcr/eslint-plugin-react
Pull request #360 (2015-12-14 00:44:12)

Add no-deprecated rule


Patch:
diff --git a/README.md b/README.md
index f3d761e0..48acbcad 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ Finally, enable all of the rules that you would like to use.
     "react/jsx-uses-react": 1,
     "react/jsx-uses-vars": 1,
     "react/no-danger": 1,
+    "react/no-deprecated": 1,
     "react/no-did-mount-set-state": 1,
     "react/no-did-update-set-state": 1,
     "react/no-direct-mutation-state": 1,
@@ -105,6 +106,7 @@ Finally, enable all of the rules that you would like to use.
 * [jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be incorrectly marked as unused
 * [jsx-uses-vars](docs/rules/jsx-uses-vars.md): Prevent variables used in JSX to be incorrectly marked as unused
 * [no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX properties
+* [no-deprecated](docs/rules/no-deprecated.md): Prevent usage of deprecated methods
 * [no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of `setState` in `componentDidMount`
 * [no-did-update-set-state](docs/rules/no-did-update-set-state.md): Prevent usage of `setState` in `componentDidUpdate`
 * [no-direct-mutation-state](docs/rules/no-direct-mutation-state.md): Prevent direct mutation of `this.state`
@@ -119,16 +121,6 @@ Finally, enable all of the rules that you would like to use.
 * [sort-comp](docs/rules/sort-comp.md): Enforce component methods order
 * [wrap-multilines](docs/rules/wrap-multilines.md): Prevent missing parentheses around multilines JSX
 
-## To Do
-
-* no-deprecated: Prevent usage of deprecated methods ([React 0.12 Updated API](http://facebook.github.io/react/blog/2014/10/28/react-v0.12.html#new-terminology-amp-updated-apis))
-* no-classic: Prevent usage of "classic" methods ([#2700](https://github.com/facebook/react/pull/2700))
-* [Implement relevant rules from David Chang's React Style Guide](https://reactjsnews.com/react-style-guide-patterns-i-like)
-* [Implement relevant rules from John Cobb's best practices and conventions](http://web-design-weekly.com/2015/01/29/opinionated-guide-react-js-best-practices-conventions/)
-* [Implement relevant rules from Alexander Early's tips and best practices](http://aeflash.com/2015-02/react-tips-and-best-practices.html)
-
-[Any rule idea is welcome !](https://github.com/yannickcr/eslint-plugin-react/issues)
-
 # License
 
 ESLint-plugin-React is licensed under the [MIT License](http://www.opensource.org/licenses/mit-license.php).
diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md
new file mode 100644
index 00000000..c01e41f1
--- /dev/null
+++ b/docs/rules/no-deprecated.md
@@ -0,0 +1,38 @@
+# Prevent usage of deprecated methods (no-deprecated)
+
+Several methods are deprecated between React versions. This rule will warn you if you try to use a deprecated method.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```js
+React.render(<MyComponent />, root);
+
+React.unmountComponentAtNode(root);
+
+React.findDOMNode(this.refs.foo);
+
+React.renderToString(<MyComponent />);
+
+React.renderToStaticMarkup(<MyComponent />);
+```
+
+The following patterns are not considered warnings:
+
+```js
+ReactDOM.render(<MyComponent />, root);
+
+// When [1, {"react": "0.13.0"}]
+ReactDOM.findDOMNode(this.refs.foo);
+```
+
+## Rule Options
+
+By default this rule will warn to every methods marked as deprecated. You can limit it to an older React version if you are not using the latest one:
+
+```js
+"rules": {
+  "react/no-deprecated": [1, {"react": "0.12.0"}] // Will warn for every deprecated methods in React 0.12.0 and below
+}
+```
diff --git a/index.js b/index.js
index 8de54d22..88a3d002 100644
--- a/index.js
+++ b/index.js
@@ -10,6 +10,7 @@ module.exports = {
     'self-closing-comp': require('./lib/rules/self-closing-comp'),
     'no-danger': require('./lib/rules/no-danger'),
     'no-set-state': require('./lib/rules/no-set-state'),
+    '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'),
     'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'),
@@ -43,6 +44,7 @@ module.exports = {
     'display-name': 0,
     'wrap-multilines': 0,
     'self-closing-comp': 0,
+    'no-deprecated': 0,
     'no-danger': 0,
     'no-set-state': 0,
     'no-did-mount-set-state': 0,
diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js
new file mode 100644
index 00000000..4002581f
--- /dev/null
+++ b/lib/rules/no-deprecated.js
@@ -0,0 +1,101 @@
+/**
+ * @fileoverview Prevent usage of deprecated methods
+ * @author Yannick Croissant
+ * @author Scott Feeney
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+var DEPRECATED_MESSAGE = '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}';
+
+var DEPRECATED = {
+  MemberExpression: {
+    // 0.12.0
+    'React.renderComponent': ['0.12.0', 'React.render'],
+    'React.renderComponentToString': ['0.12.0', 'React.renderToString'],
+    'React.renderComponentToStaticMarkup': ['0.12.0', 'React.renderToStaticMarkup'],
+    'React.isValidComponent': ['0.12.0', 'React.isValidElement'],
+    'React.PropTypes.component': ['0.12.0', 'React.PropTypes.element'],
+    'React.PropTypes.renderable': ['0.12.0', 'React.PropTypes.node'],
+    'React.isValidClass': ['0.12.0'],
+    'this.transferPropsTo': ['0.12.0', 'spread operator ({...})'],
+    // 0.13.0
+    'React.addons.classSet': ['0.13.0', 'the npm module classnames'],
+    'React.addons.cloneWithProps': ['0.13.0', 'React.cloneElement'],
+    // 0.14.0
+    'React.render': ['0.14.0', 'ReactDOM.render'],
+    'React.unmountComponentAtNode': ['0.14.0', 'ReactDOM.unmountComponentAtNode'],
+    'React.findDOMNode': ['0.14.0', 'ReactDOM.findDOMNode'],
+    'React.renderToString': ['0.14.0', 'ReactDOMServer.renderToString'],
+    'React.renderToStaticMarkup': ['0.14.0', 'ReactDOMServer.renderToStaticMarkup']
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = function(context) {
+
+  // Validate React version passed in options
+  // (append the patch version if missing, allowing shorthands like 0.12 for React 0.12.0)
+  var optVer = context.options[0] ? context.options[0].react : '999.999.999';
+  optVer = /^[0-9]+\.[0-9]+$/.test(optVer) ? optVer + '.0' : optVer;
+  optVer = optVer.split('.').map(function(part) {
+    return Number(part);
+  });
+
+  function checkVersion(methodVer) {
+    methodVer = methodVer.split('.').map(function(part) {
+      return Number(part);
+    });
+    var higherMajor = methodVer[0] < optVer[0];
+    var higherMinor = methodVer[0] === optVer[0] && methodVer[1] < optVer[1];
+    var higherOrEqualPatch = methodVer[0] === optVer[0] && methodVer[1] === optVer[1] && methodVer[2] <= optVer[2];
+
+    return higherMajor || higherMinor || higherOrEqualPatch;
+  }
+
+  function isDeprecated(type, method) {
+    return (
+      DEPRECATED[type] &&
+      DEPRECATED[type][method] &&
+      checkVersion(DEPRECATED[type][method][0])
+    );
+  }
+
+  // --------------------------------------------------------------------------
+  // Public
+  // --------------------------------------------------------------------------
+
+  return {
+
+    MemberExpression: function(node) {
+      var method = context.getSource(node);
+      if (!isDeprecated(node.type, method)) {
+        return;
+      }
+      context.report(node, DEPRECATED_MESSAGE, {
+        oldMethod: method,
+        version: DEPRECATED[node.type][method][0],
+        newMethod: DEPRECATED[node.type][method][1] ? ', use ' + DEPRECATED[node.type][method][1] + ' instead' : ''
+      });
+    }
+
+  };
+
+};
+
+module.exports.schema = [{
+  type: 'object',
+  properties: {
+    react: {
+      type: 'string',
+      pattern: '^[0-9]+\.[0-9]+(\.[0-9]+)?$'
+    }
+  },
+  additionalProperties: false
+}];


Test patch:
diff --git a/tests/lib/rules/no-deprecated.js b/tests/lib/rules/no-deprecated.js
new file mode 100644
index 00000000..b2472f01
--- /dev/null
+++ b/tests/lib/rules/no-deprecated.js
@@ -0,0 +1,87 @@
+/**
+ * @fileoverview Prevent usage of deprecated methods
+ * @author Yannick Croissant
+ * @author Scott Feeney
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-deprecated');
+var RuleTester = require('eslint').RuleTester;
+
+require('babel-eslint');
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('no-deprecated', rule, {
+
+  valid: [
+    // Not deprecated
+    'var MyClass = React.createClass({});',
+    'var element = React.createElement(\'p\', {}, null);',
+    'var clone = React.cloneElement(element);',
+    'ReactDOM.render(element, container);',
+    'ReactDOM.unmountComponentAtNode(container);',
+    'ReactDOM.findDOMNode(instance);',
+    'ReactDOMServer.renderToString(element);',
+    'ReactDOMServer.renderToStaticMarkup(element);',
+    // Deprecated in a later version
+    {code: 'React.renderComponent()', options: [{react: '0.11.0'}]}
+  ],
+
+  invalid: [{
+    code: 'React.renderComponent()',
+    options: [{react: '0.12.0'}],
+    errors: [{
+      message: 'React.renderComponent is deprecated since React 0.12.0, use React.render instead'
+    }]
+  }, {
+    code: 'this.transferPropsTo()',
+    errors: [{
+      message: 'this.transferPropsTo is deprecated since React 0.12.0, use spread operator ({...}) instead'
+    }]
+  }, {
+    code: 'React.addons.classSet()',
+    errors: [{
+      message: 'React.addons.classSet is deprecated since React 0.13.0, use the npm module classnames instead'
+    }]
+  }, {
+    code: 'React.render(element, container);',
+    errors: [{
+      message: 'React.render is deprecated since React 0.14.0, use ReactDOM.render instead'
+    }]
+  }, {
+    code: 'React.unmountComponentAtNode(container);',
+    errors: [{
+      message: (
+        'React.unmountComponentAtNode is deprecated since React 0.14.0, ' +
+        'use ReactDOM.unmountComponentAtNode instead'
+      )
+    }]
+  }, {
+    code: 'React.findDOMNode(instance);',
+    errors: [{
+      message: 'React.findDOMNode is deprecated since React 0.14.0, use ReactDOM.findDOMNode instead'
+    }]
+  }, {
+    code: 'React.renderToString(element);',
+    errors: [{
+      message: 'React.renderToString is deprecated since React 0.14.0, use ReactDOMServer.renderToString instead'
+    }]
+  }, {
+    code: 'React.renderToStaticMarkup(element);',
+    errors: [{
+      message: (
+        'React.renderToStaticMarkup is deprecated since React 0.14.0, ' +
+        'use ReactDOMServer.renderToStaticMarkup instead'
+      )
+    }]
+  }]
+
+});