Repository: yannickcr/eslint-plugin-react
Pull request #1831 (2018-06-17 21:14:34)

Added `no-unsafe` rule
Added  `no-unsafe` rule to address the corresponding issue #1830. See discussion details https://github.com/yannickcr/eslint-plugin-react/pull/1750#discussion_r194915657.


Patch:
diff --git a/README.md b/README.md
index 067fe20c..5b787b1a 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,7 @@ Enable the rules that you would like to use.
 * [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Prevent using `this` in stateless functional components
 * [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup
 * [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
+* [react/no-unsafe](docs/rules/no-unsafe.md): Prevent usage of `UNSAFE_` methods
 * [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
 * [react/no-unused-state](docs/rules/no-unused-state.md): Prevent definitions of unused state properties
 * [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of `setState` in `componentWillUpdate`
diff --git a/docs/rules/no-unsafe.md b/docs/rules/no-unsafe.md
new file mode 100644
index 00000000..87d30d7c
--- /dev/null
+++ b/docs/rules/no-unsafe.md
@@ -0,0 +1,47 @@
+# Prevent usage of `UNSAFE_` methods (react/no-unsafe)
+
+Certain legacy lifecycle methods are [unsafe for use in async React applications][async_rendering] and cause warnings in [_strict mode_][strict_mode]. These also happen to be the lifecycles that cause the most [confusion within the React community][component_lifecycle_changes].
+
+[async_rendering]: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html
+[strict_mode]: https://reactjs.org/docs/strict-mode.html#identifying-unsafe-lifecycles
+[component_lifecycle_changes]: https://reactjs.org/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes
+
+The rule checks the following methods: `UNSAFE_componentWillMount`, `UNSAFE_componentWillReceiveProps`, `UNSAFE_componentWillUpdate`.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```jsx
+class Foo extends React.Component {
+  UNSAFE_componentWillMount() {}
+  UNSAFE_componentWillReceiveProps() {}
+  UNSAFE_componentWillUpdate() {}
+}
+```
+
+```jsx
+const Foo = createReactClass({
+  UNSAFE_componentWillMount: function() {},
+  UNSAFE_componentWillReceiveProps: function() {},
+  UNSAFE_componentWillUpdate: function() {}
+});
+```
+
+The following patterns are **not** considered warnings:
+
+```jsx
+class Foo extends Bar {
+  UNSAFE_componentWillMount() {}
+  UNSAFE_componentWillReceiveProps() {}
+  UNSAFE_componentWillUpdate() {}
+}
+```
+
+```jsx
+const Foo = bar({
+  UNSAFE_componentWillMount: function() {},
+  UNSAFE_componentWillReceiveProps: function() {},
+  UNSAFE_componentWillUpdate: function() {}
+});
+```
diff --git a/index.js b/index.js
index e311f354..45a96d7d 100644
--- a/index.js
+++ b/index.js
@@ -64,6 +64,7 @@ const allRules = {
   'no-typos': require('./lib/rules/no-typos'),
   'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
   'no-unknown-property': require('./lib/rules/no-unknown-property'),
+  'no-unsafe': require('./lib/rules/no-unsafe'),
   'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
   'no-unused-state': require('./lib/rules/no-unused-state'),
   'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
@@ -139,6 +140,7 @@ module.exports = {
         'react/no-string-refs': 2,
         'react/no-unescaped-entities': 2,
         'react/no-unknown-property': 2,
+        'react/no-unsafe': 0,
         'react/prop-types': 2,
         'react/react-in-jsx-scope': 2,
         'react/require-render-return': 2
diff --git a/lib/rules/no-unsafe.js b/lib/rules/no-unsafe.js
new file mode 100644
index 00000000..006e5596
--- /dev/null
+++ b/lib/rules/no-unsafe.js
@@ -0,0 +1,99 @@
+/**
+ * @fileoverview Prevent usage of UNSAFE_ methods
+ * @author Sergei Startsev
+ */
+
+'use strict';
+
+const Components = require('../util/Components');
+const astUtil = require('../util/ast');
+const docsUrl = require('../util/docsUrl');
+const versionUtil = require('../util/version');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Prevent usage of UNSAFE_ methods',
+      category: 'Best Practices',
+      recommended: false,
+      url: docsUrl('no-unsafe')
+    },
+    schema: []
+  },
+
+  create: Components.detect((context, components, utils) => {
+    const isApplicable = versionUtil.testReactVersion(context, '16.3.0');
+    if (!isApplicable) {
+      return {};
+    }
+
+    /**
+     * Returns a list of unsafe methods
+     * @returns {Array} A list of unsafe methods
+     */
+    function getUnsafeMethods() {
+      return [
+        'UNSAFE_componentWillMount',
+        'UNSAFE_componentWillReceiveProps',
+        'UNSAFE_componentWillUpdate'
+      ];
+    }
+
+    /**
+     * Checks if a passed method is unsafe
+     * @param {string} method Life cycle method
+     * @returns {boolean} Returns true for unsafe methods, otherwise returns false
+     */
+    function isUnsafe(method) {
+      const unsafeMethods = getUnsafeMethods();
+      return unsafeMethods.indexOf(method) !== -1;
+    }
+
+    /**
+     * Reports the error for an unsafe method
+     * @param {ASTNode} node The AST node being checked
+     * @param {string} method Life cycle method
+     */
+    function checkUnsafe(node, method) {
+      if (!isUnsafe(method)) {
+        return;
+      }
+
+      context.report({
+        node: node,
+        message: `${method} is unsafe for use in async rendering, see https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html`
+      });
+    }
+
+    /**
+     * Returns life cycle methods if available
+     * @param {ASTNode} node The AST node being checked.
+     * @returns {Array} The array of methods.
+     */
+    function getLifeCycleMethods(node) {
+      const properties = astUtil.getComponentProperties(node);
+      return properties.map(property => astUtil.getPropertyName(property));
+    }
+
+    /**
+     * Checks life cycle methods
+     * @param {ASTNode} node The AST node being checked.
+     */
+    function checkLifeCycleMethods(node) {
+      if (utils.isES5Component(node) || utils.isES6Component(node)) {
+        const methods = getLifeCycleMethods(node);
+        methods.forEach(method => checkUnsafe(node, method));
+      }
+    }
+
+    return {
+      ClassDeclaration: checkLifeCycleMethods,
+      ClassExpression: checkLifeCycleMethods,
+      ObjectExpression: checkLifeCycleMethods
+    };
+  })
+};


Test patch:
diff --git a/tests/lib/rules/no-unsafe.js b/tests/lib/rules/no-unsafe.js
new file mode 100644
index 00000000..c5e26068
--- /dev/null
+++ b/tests/lib/rules/no-unsafe.js
@@ -0,0 +1,155 @@
+/**
+ * @fileoverview Prevent usage of UNSAFE_ methods
+ * @author Sergei Startsev
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-unsafe');
+const RuleTester = require('eslint').RuleTester;
+
+const parserOptions = {
+  ecmaVersion: 2018,
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+function errorMessage(method) {
+  return `${method} is unsafe for use in async rendering, see https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html`;
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('no-unsafe', rule, {
+  valid: [
+    {
+      code: `
+        class Foo extends React.Component {
+          componentDidUpdate() {}
+          render() {}
+        }
+      `,
+      settings: {react: {version: '16.4.0'}}
+    },
+    {
+      code: `
+        const Foo = createReactClass({
+          componentDidUpdate: function() {},
+          render: function() {}
+        });
+      `,
+      settings: {react: {version: '16.4.0'}}
+    },
+    {
+      code: `
+        class Foo extends Bar {
+          UNSAFE_componentWillMount() {}
+          UNSAFE_componentWillReceiveProps() {}
+          UNSAFE_componentWillUpdate() {}
+        }
+      `,
+      settings: {react: {version: '16.4.0'}}
+    },
+    {
+      code: `
+        const Foo = bar({
+          UNSAFE_componentWillMount: function() {},
+          UNSAFE_componentWillReceiveProps: function() {},
+          UNSAFE_componentWillUpdate: function() {},
+        });
+      `,
+      settings: {react: {version: '16.4.0'}}
+    },
+    {
+      code: `
+        class Foo extends React.Component {
+          UNSAFE_componentWillMount() {}
+          UNSAFE_componentWillReceiveProps() {}
+          UNSAFE_componentWillUpdate() {}
+        }
+      `,
+      settings: {react: {version: '16.2.0'}}
+    },
+    {
+      code: `
+          const Foo = createReactClass({
+            UNSAFE_componentWillMount: function() {},
+            UNSAFE_componentWillReceiveProps: function() {},
+            UNSAFE_componentWillUpdate: function() {},
+          });
+        `,
+      settings: {react: {version: '16.2.0'}}
+    }
+  ],
+
+  invalid: [
+    {
+      code: `
+      class Foo extends React.Component {
+        UNSAFE_componentWillMount() {}
+        UNSAFE_componentWillReceiveProps() {}
+        UNSAFE_componentWillUpdate() {}
+      }
+    `,
+      settings: {react: {version: '16.3.0'}},
+      errors: [
+        {
+          message: errorMessage('UNSAFE_componentWillMount'),
+          line: 2,
+          column: 7,
+          type: 'ClassDeclaration'
+        },
+        {
+          message: errorMessage('UNSAFE_componentWillReceiveProps'),
+          line: 2,
+          column: 7,
+          type: 'ClassDeclaration'
+        },
+        {
+          message: errorMessage('UNSAFE_componentWillUpdate'),
+          line: 2,
+          column: 7,
+          type: 'ClassDeclaration'
+        }
+      ]
+    },
+    {
+      code: `
+        const Foo = createReactClass({
+          UNSAFE_componentWillMount: function() {},
+          UNSAFE_componentWillReceiveProps: function() {},
+          UNSAFE_componentWillUpdate: function() {},
+        });
+      `,
+      settings: {react: {version: '16.3.0'}},
+      errors: [
+        {
+          message: errorMessage('UNSAFE_componentWillMount'),
+          line: 2,
+          column: 38,
+          type: 'ObjectExpression'
+        },
+        {
+          message: errorMessage('UNSAFE_componentWillReceiveProps'),
+          line: 2,
+          column: 38,
+          type: 'ObjectExpression'
+        },
+        {
+          message: errorMessage('UNSAFE_componentWillUpdate'),
+          line: 2,
+          column: 38,
+          type: 'ObjectExpression'
+        }
+      ]
+    }
+  ]
+});