Skip to content

Commit

Permalink
Merge pull request #1831 from sergei-startsev/no-unsafe-rule
Browse files Browse the repository at this point in the history
Added `no-unsafe` rule
  • Loading branch information
ljharb committed Jun 21, 2018
2 parents 48e386d + 0285eef commit 7869530
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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`
Expand Down
47 changes: 47 additions & 0 deletions 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() {}
});
```
2 changes: 2 additions & 0 deletions index.js
Expand Up @@ -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'),
Expand Down Expand Up @@ -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
Expand Down
99 changes: 99 additions & 0 deletions 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
};
})
};
155 changes: 155 additions & 0 deletions 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'
}
]
}
]
});

0 comments on commit 7869530

Please sign in to comment.