diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js index 8eca5be07f..05bc08b29a 100644 --- a/lib/rules/no-unused-state.js +++ b/lib/rules/no-unused-state.js @@ -11,6 +11,7 @@ const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); +const ast = require('../util/ast'); // Descend through all wrapping TypeCastExpressions and return the expression // that was cast. @@ -41,7 +42,7 @@ function getName(node) { } function isThisExpression(node) { - return uncast(node).type === 'ThisExpression'; + return ast.unwrapTSAsExpression(uncast(node)).type === 'ThisExpression'; } function getInitialClassInfo() { @@ -62,10 +63,12 @@ function getInitialClassInfo() { } function isSetStateCall(node) { + const unwrappedCalleeNode = ast.unwrapTSAsExpression(node.callee); + return ( - node.callee.type === 'MemberExpression' && - isThisExpression(node.callee.object) && - getName(node.callee.property) === 'setState' + unwrappedCalleeNode.type === 'MemberExpression' && + isThisExpression(unwrappedCalleeNode.object) && + getName(unwrappedCalleeNode.property) === 'setState' ); } @@ -178,16 +181,18 @@ module.exports = { // Used to record used state fields and new aliases for both // AssignmentExpressions and VariableDeclarators. function handleAssignment(left, right) { + const unwrappedRight = ast.unwrapTSAsExpression(right); + switch (left.type) { case 'Identifier': - if (isStateReference(right) && classInfo.aliases) { + if (isStateReference(unwrappedRight) && classInfo.aliases) { classInfo.aliases.add(left.name); } break; case 'ObjectPattern': - if (isStateReference(right)) { + if (isStateReference(unwrappedRight)) { handleStateDestructuring(left); - } else if (isThisExpression(right) && classInfo.aliases) { + } else if (isThisExpression(unwrappedRight) && classInfo.aliases) { for (const prop of left.properties) { if (prop.type === 'Property' && getName(prop.key) === 'state') { const name = getName(prop.value); @@ -254,24 +259,30 @@ module.exports = { if (!classInfo) { return; } + + const unwrappedNode = ast.unwrapTSAsExpression(node); + const unwrappedArgumentNode = ast.unwrapTSAsExpression(unwrappedNode.arguments[0]); + // If we're looking at a `this.setState({})` invocation, record all the // properties as state fields. if ( - isSetStateCall(node) && - node.arguments.length > 0 && - node.arguments[0].type === 'ObjectExpression' + isSetStateCall(unwrappedNode) && + unwrappedNode.arguments.length > 0 && + unwrappedArgumentNode.type === 'ObjectExpression' ) { - addStateFields(node.arguments[0]); + addStateFields(unwrappedArgumentNode); } else if ( - isSetStateCall(node) && - node.arguments.length > 0 && - node.arguments[0].type === 'ArrowFunctionExpression' + isSetStateCall(unwrappedNode) && + unwrappedNode.arguments.length > 0 && + unwrappedArgumentNode.type === 'ArrowFunctionExpression' ) { - if (node.arguments[0].body.type === 'ObjectExpression') { - addStateFields(node.arguments[0].body); + const unwrappedBodyNode = ast.unwrapTSAsExpression(unwrappedArgumentNode.body); + + if (unwrappedBodyNode.type === 'ObjectExpression') { + addStateFields(unwrappedBodyNode); } - if (node.arguments[0].params.length > 0 && classInfo.aliases) { - const firstParam = node.arguments[0].params[0]; + if (unwrappedArgumentNode.params.length > 0 && classInfo.aliases) { + const firstParam = unwrappedArgumentNode.params[0]; if (firstParam.type === 'ObjectPattern') { handleStateDestructuring(firstParam); } else { @@ -287,19 +298,21 @@ module.exports = { } // If we see state being assigned as a class property using an object // expression, record all the fields of that object as state fields. + const unwrappedValueNode = ast.unwrapTSAsExpression(node.value); + if ( getName(node.key) === 'state' && !node.static && - node.value && - node.value.type === 'ObjectExpression' + unwrappedValueNode && + unwrappedValueNode.type === 'ObjectExpression' ) { - addStateFields(node.value); + addStateFields(unwrappedValueNode); } if ( !node.static && - node.value && - node.value.type === 'ArrowFunctionExpression' + unwrappedValueNode && + unwrappedValueNode.type === 'ArrowFunctionExpression' ) { // Create a new set for this.state aliases local to this method. classInfo.aliases = new Set(); @@ -364,12 +377,16 @@ module.exports = { if (!classInfo) { return; } + + const unwrappedLeft = ast.unwrapTSAsExpression(node.left); + const unwrappedRight = ast.unwrapTSAsExpression(node.right); + // Check for assignments like `this.state = {}` if ( - node.left.type === 'MemberExpression' && - isThisExpression(node.left.object) && - getName(node.left.property) === 'state' && - node.right.type === 'ObjectExpression' + unwrappedLeft.type === 'MemberExpression' && + isThisExpression(unwrappedLeft.object) && + getName(unwrappedLeft.property) === 'state' && + unwrappedRight.type === 'ObjectExpression' ) { // Find the nearest function expression containing this assignment. let fn = node; @@ -383,11 +400,11 @@ module.exports = { fn.parent.type === 'MethodDefinition' && fn.parent.kind === 'constructor' ) { - addStateFields(node.right); + addStateFields(unwrappedRight); } } else { // Check for assignments like `alias = this.state` and record the alias. - handleAssignment(node.left, node.right); + handleAssignment(unwrappedLeft, unwrappedRight); } }, @@ -402,7 +419,7 @@ module.exports = { if (!classInfo) { return; } - if (isStateReference(node.object)) { + if (isStateReference(ast.unwrapTSAsExpression(node.object))) { // If we see this.state[foo] access, give up. if (node.computed && node.property.type !== 'Literal') { classInfo = null; diff --git a/lib/util/ast.js b/lib/util/ast.js index 1111c992d9..91f77a8edd 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -185,6 +185,17 @@ function isAssignmentLHS(node) { ); } +/** + * Extracts the expression node that is wrapped inside a TS type assertion + * + * @param {ASTNode} node - potential TS node + * @returns {ASTNode} - unwrapped expression node + */ +function unwrapTSAsExpression(node) { + if (node && node.type === 'TSAsExpression') return node.expression; + return node; +} + module.exports = { findReturnStatement, getFirstNodeInLine, @@ -196,5 +207,6 @@ module.exports = { isClass, isFunction, isFunctionLikeExpression, - isNodeFirstInLine + isNodeFirstInLine, + unwrapTSAsExpression }; diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js index 478100833f..376818fae4 100755 --- a/lib/util/usedPropTypes.js +++ b/lib/util/usedPropTypes.js @@ -145,9 +145,12 @@ function isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) { * @return {boolean} */ function isSetStateUpdater(node) { - return node.parent.type === 'CallExpression' && - node.parent.callee.property && - node.parent.callee.property.name === 'setState' && + const unwrappedParentCalleeNode = node.parent.type === 'CallExpression' && + ast.unwrapTSAsExpression(node.parent.callee); + + return unwrappedParentCalleeNode && + unwrappedParentCalleeNode.property && + unwrappedParentCalleeNode.property.name === 'setState' && // Make sure we are in the updater not the callback node.parent.arguments[0] === node; } @@ -158,11 +161,14 @@ function isPropArgumentInSetStateUpdater(context, name) { } let scope = context.getScope(); while (scope) { - if ( + const unwrappedParentCalleeNode = scope.block && scope.block.parent && scope.block.parent.type === 'CallExpression' && - scope.block.parent.callee.property && - scope.block.parent.callee.property.name === 'setState' && + ast.unwrapTSAsExpression(scope.block.parent.callee); + if ( + unwrappedParentCalleeNode && + unwrappedParentCalleeNode.property && + unwrappedParentCalleeNode.property.name === 'setState' && // Make sure we are in the updater not the callback scope.block.parent.arguments[0].start === scope.block.start && scope.block.parent.arguments[0].params && @@ -187,7 +193,7 @@ function isInClassComponent(utils) { function isThisDotProps(node) { return !!node && node.type === 'MemberExpression' && - node.object.type === 'ThisExpression' && + ast.unwrapTSAsExpression(node.object).type === 'ThisExpression' && node.property.name === 'props'; } @@ -242,26 +248,28 @@ function getPropertyName(node) { * @returns {boolean} */ function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles) { + const unwrappedObjectNode = ast.unwrapTSAsExpression(node.object); + if (isInClassComponent(utils)) { // this.props.* - if (isThisDotProps(node.object)) { + if (isThisDotProps(unwrappedObjectNode)) { return true; } // props.* or prevProps.* or nextProps.* if ( - isCommonVariableNameForProps(node.object.name) && + isCommonVariableNameForProps(unwrappedObjectNode.name) && (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor()) ) { return true; } // this.setState((_, props) => props.*)) - if (isPropArgumentInSetStateUpdater(context, node.object.name)) { + if (isPropArgumentInSetStateUpdater(context, unwrappedObjectNode.name)) { return true; } return false; } // props.* in function component - return node.object.name === 'props' && !ast.isAssignmentLHS(node); + return unwrappedObjectNode.name === 'props' && !ast.isAssignmentLHS(node); } module.exports = function usedPropTypesInstructions(context, components, utils) { @@ -442,13 +450,15 @@ module.exports = function usedPropTypesInstructions(context, components, utils) return { VariableDeclarator(node) { + const unwrappedInitNode = ast.unwrapTSAsExpression(node.init); + // let props = this.props - if (isThisDotProps(node.init) && isInClassComponent(utils) && node.id.type === 'Identifier') { + if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils) && node.id.type === 'Identifier') { propVariables.set(node.id.name, []); } // Only handles destructuring - if (node.id.type !== 'ObjectPattern' || !node.init) { + if (node.id.type !== 'ObjectPattern' || !unwrappedInitNode) { return; } @@ -457,20 +467,21 @@ module.exports = function usedPropTypesInstructions(context, components, utils) property.key && (property.key.name === 'props' || property.key.value === 'props') )); - if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') { + + if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') { markPropTypesAsUsed(propsProperty.value); return; } // let {props} = this - if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') { + if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') { propVariables.set('props', []); return; } // let {firstname} = props if ( - isCommonVariableNameForProps(node.init.name) && + isCommonVariableNameForProps(unwrappedInitNode.name) && (utils.getParentStatelessComponent() || isInLifeCycleMethod(node, checkAsyncSafeLifeCycles)) ) { markPropTypesAsUsed(node.id); @@ -478,14 +489,14 @@ module.exports = function usedPropTypesInstructions(context, components, utils) } // let {firstname} = this.props - if (isThisDotProps(node.init) && isInClassComponent(utils)) { + if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils)) { markPropTypesAsUsed(node.id); return; } // let {firstname} = thing, where thing is defined by const thing = this.props.**.* - if (propVariables.get(node.init.name)) { - markPropTypesAsUsed(node.id, propVariables.get(node.init.name)); + if (propVariables.get(unwrappedInitNode.name)) { + markPropTypesAsUsed(node.id, propVariables.get(unwrappedInitNode.name)); } }, @@ -514,8 +525,9 @@ module.exports = function usedPropTypesInstructions(context, components, utils) return; } - if (propVariables.get(node.object.name)) { - markPropTypesAsUsed(node, propVariables.get(node.object.name)); + const propVariable = propVariables.get(ast.unwrapTSAsExpression(node.object).name); + if (propVariable) { + markPropTypesAsUsed(node, propVariable); } }, diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 5801ab71d6..8c216e85ea 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -3125,8 +3125,7 @@ ruleTester.run('no-unused-prop-types', rule, { } `, parser: parsers.BABEL_ESLINT - }, - { + }, { code: ` export default class Foo extends React.Component { render() { @@ -3136,6 +3135,73 @@ ruleTester.run('no-unused-prop-types', rule, { Foo.defaultProps = Object.assign({}); ` + }, { + code: ` + const Foo = (props) => { + const { foo } = props as unknown; + (props as unknown).bar as unknown; + + return <>; + }; + + Foo.propTypes = { + foo, + bar, + }; + `, + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: ` + class Foo extends React.Component { + static propTypes = { + prevProp, + nextProp, + setStateProp, + thisPropsAliasDestructProp, + thisPropsAliasProp, + thisDestructPropsAliasDestructProp, + thisDestructPropsAliasProp, + thisDestructPropsDestructProp, + thisPropsDestructProp, + thisPropsProp, + }; + + componentDidUpdate(prevProps) { + (prevProps as unknown).prevProp as unknown; + } + + shouldComponentUpdate(nextProps) { + (nextProps as unknown).nextProp as unknown; + } + + stateProps() { + ((this as unknown).setState as unknown)((_, props) => (props as unknown).setStateProp as unknown); + } + + thisPropsAlias() { + const props = (this as unknown).props as unknown; + + const { thisPropsAliasDestructProp } = props as unknown; + (props as unknown).thisPropsAliasProp as unknown; + } + + thisDestructPropsAlias() { + const { props } = this as unknown; + + const { thisDestructPropsAliasDestructProp } = props as unknown; + (props as unknown).thisDestructPropsAliasProp as unknown; + } + + render() { + const { props: { thisDestructPropsDestructProp } } = this as unknown; + const { thisPropsDestructProp } = (this as unknown).props as unknown; + ((this as unknown).props as unknown).thisPropsProp as unknown; + + return null; + } + } + `, + parser: parsers.TYPESCRIPT_ESLINT } ], @@ -5144,6 +5210,99 @@ ruleTester.run('no-unused-prop-types', rule, { errors: [{ message: '\'unUsedProp\' PropType is defined but prop is never used' }] + }, { + code: ` + const Foo = (props) => { + const { foo } = props as unknown; + (props as unknown).bar as unknown; + + return <>; + }; + + Foo.propTypes = { + fooUnused, + barUnused, + }; + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'fooUnused\' PropType is defined but prop is never used' + }, { + message: '\'barUnused\' PropType is defined but prop is never used' + }] + }, { + code: ` + class Foo extends React.Component { + static propTypes = { + prevPropUnused, + nextPropUnused, + setStatePropUnused, + thisPropsAliasDestructPropUnused, + thisPropsAliasPropUnused, + thisDestructPropsAliasDestructPropUnused, + thisDestructPropsAliasPropUnused, + thisDestructPropsDestructPropUnused, + thisPropsDestructPropUnused, + thisPropsPropUnused, + }; + + componentDidUpdate(prevProps) { + (prevProps as unknown).prevProp as unknown; + } + + shouldComponentUpdate(nextProps) { + (nextProps as unknown).nextProp as unknown; + } + + stateProps() { + ((this as unknown).setState as unknown)((_, props) => (props as unknown).setStateProp as unknown); + } + + thisPropsAlias() { + const props = (this as unknown).props as unknown; + + const { thisPropsAliasDestructProp } = props as unknown; + (props as unknown).thisPropsAliasProp as unknown; + } + + thisDestructPropsAlias() { + const { props } = this as unknown; + + const { thisDestructPropsAliasDestructProp } = props as unknown; + (props as unknown).thisDestructPropsAliasProp as unknown; + } + + render() { + const { props: { thisDestructPropsDestructProp } } = this as unknown; + const { thisPropsDestructProp } = (this as unknown).props as unknown; + ((this as unknown).props as unknown).thisPropsProp as unknown; + + return null; + } + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'prevPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'nextPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'setStatePropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisPropsAliasDestructPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisPropsAliasPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisDestructPropsAliasDestructPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisDestructPropsAliasPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisDestructPropsDestructPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisPropsDestructPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisPropsPropUnused\' PropType is defined but prop is never used' + }] } /* , { diff --git a/tests/lib/rules/no-unused-state.js b/tests/lib/rules/no-unused-state.js index cf57dd1ac4..f6ded949bd 100644 --- a/tests/lib/rules/no-unused-state.js +++ b/tests/lib/rules/no-unused-state.js @@ -742,6 +742,58 @@ eslintTester.run('no-unused-state', rule, { } }`, parser: parsers.BABEL_ESLINT + }, { + code: ` + class Foo extends Component { + state = { + thisStateAliasProp, + thisStateAliasRestProp, + thisDestructStateAliasProp, + thisDestructStateAliasRestProp, + thisDestructStateDestructRestProp, + thisSetStateProp, + thisSetStateRestProp, + } as unknown + + constructor() { + // other methods of defining state props + ((this as unknown).state as unknown) = { thisStateProp } as unknown; + ((this as unknown).setState as unknown)({ thisStateDestructProp } as unknown); + ((this as unknown).setState as unknown)(state => ({ thisDestructStateDestructProp } as unknown)); + } + + thisStateAlias() { + const state = (this as unknown).state as unknown; + + (state as unknown).thisStateAliasProp as unknown; + const { ...thisStateAliasRest } = state as unknown; + (thisStateAliasRest as unknown).thisStateAliasRestProp as unknown; + } + + thisDestructStateAlias() { + const { state } = this as unknown; + + (state as unknown).thisDestructStateAliasProp as unknown; + const { ...thisDestructStateAliasRest } = state as unknown; + (thisDestructStateAliasRest as unknown).thisDestructStateAliasRestProp as unknown; + } + + thisSetState() { + ((this as unknown).setState as unknown)(state => (state as unknown).thisSetStateProp as unknown); + ((this as unknown).setState as unknown)(({ ...thisSetStateRest }) => (thisSetStateRest as unknown).thisSetStateRestProp as unknown); + } + + render() { + ((this as unknown).state as unknown).thisStateProp as unknown; + const { thisStateDestructProp } = (this as unknown).state as unknown; + const { state: { thisDestructStateDestructProp, ...thisDestructStateDestructRest } } = this as unknown; + (thisDestructStateDestructRest as unknown).thisDestructStateDestructRestProp as unknown; + + return null; + } + } + `, + parser: parsers.TYPESCRIPT_ESLINT } ], @@ -1119,6 +1171,70 @@ eslintTester.run('no-unused-state', rule, { `, parser: parsers.BABEL_ESLINT, errors: getErrorMessages(['initial']) + }, { + code: ` + class Foo extends Component { + state = { + thisStateAliasPropUnused, + thisStateAliasRestPropUnused, + thisDestructStateAliasPropUnused, + thisDestructStateAliasRestPropUnused, + thisDestructStateDestructRestPropUnused, + thisSetStatePropUnused, + thisSetStateRestPropUnused, + } as unknown + + constructor() { + // other methods of defining state props + ((this as unknown).state as unknown) = { thisStatePropUnused } as unknown; + ((this as unknown).setState as unknown)({ thisStateDestructPropUnused } as unknown); + ((this as unknown).setState as unknown)(state => ({ thisDestructStateDestructPropUnused } as unknown)); + } + + thisStateAlias() { + const state = (this as unknown).state as unknown; + + (state as unknown).thisStateAliasProp as unknown; + const { ...thisStateAliasRest } = state as unknown; + (thisStateAliasRest as unknown).thisStateAliasRestProp as unknown; + } + + thisDestructStateAlias() { + const { state } = this as unknown; + + (state as unknown).thisDestructStateAliasProp as unknown; + const { ...thisDestructStateAliasRest } = state as unknown; + (thisDestructStateAliasRest as unknown).thisDestructStateAliasRestProp as unknown; + } + + thisSetState() { + ((this as unknown).setState as unknown)(state => (state as unknown).thisSetStateProp as unknown); + ((this as unknown).setState as unknown)(({ ...thisSetStateRest }) => (thisSetStateRest as unknown).thisSetStateRestProp as unknown); + } + + render() { + ((this as unknown).state as unknown).thisStateProp as unknown; + const { thisStateDestructProp } = (this as unknown).state as unknown; + const { state: { thisDestructStateDestructProp, ...thisDestructStateDestructRest } } = this as unknown; + (thisDestructStateDestructRest as unknown).thisDestructStateDestructRestProp as unknown; + + return null; + } + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: getErrorMessages([ + 'thisStateAliasPropUnused', + 'thisStateAliasRestPropUnused', + 'thisDestructStateAliasPropUnused', + 'thisDestructStateAliasRestPropUnused', + 'thisDestructStateDestructRestPropUnused', + 'thisSetStatePropUnused', + 'thisSetStateRestPropUnused', + 'thisStatePropUnused', + 'thisStateDestructPropUnused', + 'thisDestructStateDestructPropUnused' + ]) } ] });