diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index 17ac197794..6f5301197e 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -23,6 +23,7 @@ const DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/; const DIRECT_NEXT_PROPS_REGEX = /^nextProps\s*(\.|\[)/; const DIRECT_PREV_PROPS_REGEX = /^prevProps\s*(\.|\[)/; const LIFE_CYCLE_METHODS = ['componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'componentDidUpdate']; +const ASYNC_SAFE_LIFE_CYCLE_METHODS = ['getDerivedStateFromProps', 'UNSAFE_componentWillReceiveProps', 'UNSAFE_componentWillUpdate']; // ------------------------------------------------------------------------------ // Rule Definition @@ -61,6 +62,7 @@ module.exports = { const skipShapeProps = configuration.skipShapeProps; const customValidators = configuration.customValidators || []; const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []); + const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0'); // Used to track the type annotations in scope. // Necessary because babel's scopes do not track type annotations. @@ -91,12 +93,14 @@ module.exports = { function inLifeCycleMethod() { let scope = context.getScope(); while (scope) { - if ( - scope.block && scope.block.parent && - scope.block.parent.key && - LIFE_CYCLE_METHODS.indexOf(scope.block.parent.key.name) >= 0 - ) { - return true; + if (scope.block && scope.block.parent && scope.block.parent.key) { + const name = scope.block.parent.key.name; + + if (LIFE_CYCLE_METHODS.indexOf(name) >= 0) { + return true; + } else if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(name) >= 0) { + return true; + } } scope = scope.upper; } @@ -250,13 +254,16 @@ module.exports = { */ function isNodeALifeCycleMethod(node) { const nodeKeyName = (node.key || {}).name; - return ( - node.kind === 'constructor' || - nodeKeyName === 'componentWillReceiveProps' || - nodeKeyName === 'shouldComponentUpdate' || - nodeKeyName === 'componentWillUpdate' || - nodeKeyName === 'componentDidUpdate' - ); + + if (node.kind === 'constructor') { + return true; + } else if (LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) { + return true; + } else if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) { + return true; + } + + return false; } /** diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 5bfb270cda..7ee5149181 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -2825,6 +2825,61 @@ ruleTester.run('no-unused-prop-types', rule, { } MyComponent.propTypes = { * other() {} }; ` + }, { + // Sanity test coverage for new UNSAFE_componentWillReceiveProps lifecycles + code: [` + class Hello extends Component { + static propTypes = { + something: PropTypes.bool + }; + UNSAFE_componentWillReceiveProps (nextProps) { + const {something} = nextProps; + doSomething(something); + } + } + `].join('\n'), + settings: {react: {version: '16.3.0'}}, + parser: 'babel-eslint' + }, { + // Destructured props in the `UNSAFE_componentWillUpdate` method shouldn't throw errors + code: [` + class Hello extends Component { + static propTypes = { + something: PropTypes.bool + }; + UNSAFE_componentWillUpdate (nextProps, nextState) { + const {something} = nextProps; + return something; + } + } + `].join('\n'), + settings: {react: {version: '16.3.0'}}, + parser: 'babel-eslint' + }, { + // Simple test of new static getDerivedStateFromProps lifecycle + code: [` + class MyComponent extends React.Component { + static propTypes = { + defaultValue: 'bar' + }; + state = { + currentValue: null + }; + static getDerivedStateFromProps(nextProps, prevState) { + if (prevState.currentValue === null) { + return { + currentValue: nextProps.defaultValue, + } + } + return null; + } + render() { + return
{ this.state.currentValue }
+ } + } + `].join('\n'), + settings: {react: {version: '16.3.0'}}, + parser: 'babel-eslint' } ], @@ -4372,6 +4427,67 @@ ruleTester.run('no-unused-prop-types', rule, { errors: [{ message: '\'lastname\' PropType is defined but prop is never used' }] + }, { + code: [` + class Hello extends Component { + static propTypes = { + something: PropTypes.bool + }; + UNSAFE_componentWillReceiveProps (nextProps) { + const {something} = nextProps; + doSomething(something); + } + } + `].join('\n'), + settings: {react: {version: '16.2.0'}}, + parser: 'babel-eslint', + errors: [{ + message: '\'something\' PropType is defined but prop is never used' + }] + }, { + code: [` + class Hello extends Component { + static propTypes = { + something: PropTypes.bool + }; + UNSAFE_componentWillUpdate (nextProps, nextState) { + const {something} = nextProps; + return something; + } + } + `].join('\n'), + settings: {react: {version: '16.2.0'}}, + parser: 'babel-eslint', + errors: [{ + message: '\'something\' PropType is defined but prop is never used' + }] + }, { + code: [` + class MyComponent extends React.Component { + static propTypes = { + defaultValue: 'bar' + }; + state = { + currentValue: null + }; + static getDerivedStateFromProps(nextProps, prevState) { + if (prevState.currentValue === null) { + return { + currentValue: nextProps.defaultValue, + } + } + return null; + } + render() { + return
{ this.state.currentValue }
+ } + } + `].join('\n'), + settings: {react: {version: '16.2.0'}}, + parser: 'babel-eslint', + errors: [{ + message: '\'defaultValue\' PropType is defined but prop is never used' + }] } /* , {