diff --git a/lib/util/ast.js b/lib/util/ast.js index edf4c38237..96402f5422 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -261,6 +261,12 @@ function isTSTypeQuery(node) { return nodeType === 'TSTypeQuery'; } +function isTSTypeParameterInstantiation(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSTypeParameterInstantiation'; +} + module.exports = { findReturnStatement, getFirstNodeInLine, @@ -283,5 +289,6 @@ module.exports = { isTSTypeAliasDeclaration, isTSParenthesizedType, isTSFunctionType, - isTSTypeQuery + isTSTypeQuery, + isTSTypeParameterInstantiation }; diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 50179dd647..1d32ea84c9 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -502,27 +502,20 @@ module.exports = function propTypesInstructions(context, components, utils) { this.referenceNameMap = new Set(); this.sourceCode = context.getSourceCode(); this.shouldIgnorePropTypes = false; - this.startWithTSTypeAnnotation(); + this.visitTSNode(this.propTypes); this.endAndStructDeclaredPropTypes(); } - startWithTSTypeAnnotation() { - if (astUtil.isTSTypeAnnotation(this.propTypes)) { - const typeAnnotation = this.propTypes.typeAnnotation; - this.visitTSNode(typeAnnotation); - } else { - // weird cases such as TSTypeFunction - this.shouldIgnorePropTypes = true; - } - } - /** * The node will be distribute to different function. * @param {ASTNode} node */ visitTSNode(node) { if (!node) return; - if (astUtil.isTSTypeReference(node)) { + if (astUtil.isTSTypeAnnotation(node)) { + const typeAnnotation = node.typeAnnotation; + this.visitTSNode(typeAnnotation); + } else if (astUtil.isTSTypeReference(node)) { this.searchDeclarationByName(node); } else if (astUtil.isTSInterfaceHeritage(node)) { this.searchDeclarationByName(node); @@ -535,12 +528,10 @@ module.exports = function propTypesInstructions(context, components, utils) { this.convertIntersectionTypeToPropTypes(node); } else if (astUtil.isTSParenthesizedType(node)) { const typeAnnotation = node.typeAnnotation; - if (astUtil.isTSTypeLiteral(typeAnnotation)) { - // Check node is an object literal - if (Array.isArray(node.typeAnnotation.members)) { - this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList - .concat(node.typeAnnotation.members); - } + this.visitTSNode(typeAnnotation); + } else if (astUtil.isTSTypeParameterInstantiation(node)) { + if (Array.isArray(node.params)) { + node.params.forEach(this.visitTSNode, this); } } else { this.shouldIgnorePropTypes = true; @@ -671,6 +662,19 @@ module.exports = function propTypesInstructions(context, components, utils) { switch (res.type) { case 'ObjectExpression': iterateProperties(context, res.properties, (key, value, propNode) => { + if (propNode && propNode.argument && propNode.argument.type === 'CallExpression') { + if (propNode.argument.typeParameters) { + this.visitTSNode(propNode.argument.typeParameters); + } else { + // Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types. + this.shouldIgnorePropTypes = true; + return; + } + } + if (!value) { + this.shouldIgnorePropTypes = true; + return; + } const types = buildReactDeclarationTypes(value, key); types.fullName = key; types.name = key; @@ -679,6 +683,14 @@ module.exports = function propTypesInstructions(context, components, utils) { this.declaredPropTypes[key] = types; }); break; + case 'CallExpression': + if (res.typeParameters) { + this.visitTSNode(res.typeParameters); + } else { + // Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types. + this.shouldIgnorePropTypes = true; + } + break; default: } } @@ -689,15 +701,13 @@ module.exports = function propTypesInstructions(context, components, utils) { // Handle ReturnType<()=>returnType> if (astUtil.isTSFunctionType(returnType)) { if (astUtil.isTSTypeAnnotation(returnType.returnType)) { - const returnTypeAnnotation = returnType.returnType.typeAnnotation; - this.visitTSNode(returnTypeAnnotation); + this.visitTSNode(returnType.returnType); return; } // This line is trying to handle typescript-eslint-parser // typescript-eslint-parser TSFunction name returnType as typeAnnotation if (astUtil.isTSTypeAnnotation(returnType.typeAnnotation)) { - const returnTypeAnnotation = returnType.typeAnnotation.typeAnnotation; - this.visitTSNode(returnTypeAnnotation); + this.visitTSNode(returnType.typeAnnotation); return; } } diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 33f94a758b..7eebf7b33e 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -3763,6 +3763,89 @@ ruleTester.run('no-unused-prop-types', rule, { }; `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + // Issue: #2795 + { + code: ` + type ConnectedProps = DispatchProps & + StateProps + + const Component = ({ prop1, prop2, prop3 }: ConnectedProps) => { + // Do stuff + return ( + ... + ) + } + + const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + ...bindActionCreators>( + { prop1: importedAction, prop2: anotherImportedAction }, + dispatch, + ), + }) + + const mapStateToProps = (state: State) => ({ + prop3: Selector.value(state), + }) + + type StateProps = ReturnType + type DispatchProps = ReturnType`, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + // Issue: #2795 + { + code: ` + type ConnectedProps = DispatchProps & + StateProps + + const Component = ({ prop1, prop2, prop3 }: ConnectedProps) => { + // Do stuff + return ( + ... + ) + } + + const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + ...bindActionCreators( + { prop1: importedAction, prop2: anotherImportedAction }, + dispatch, + ), + }) + + const mapStateToProps = (state: State) => ({ + prop3: Selector.value(state), + }) + + type StateProps = ReturnType + type DispatchProps = ReturnType`, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + // Issue: #2795 + { + code: ` + type ConnectedProps = DispatchProps & + StateProps + + const Component = ({ prop1, prop2, prop3 }: ConnectedProps) => { + // Do stuff + return ( + ... + ) + } + + const mapDispatchToProps = (dispatch: ThunkDispatch) => + bindActionCreators( + { prop1: importedAction, prop2: anotherImportedAction }, + dispatch, + ) + + const mapStateToProps = (state: State) => ({ + prop3: Selector.value(state), + }) + + type StateProps = ReturnType + type DispatchProps = ReturnType`, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) ), @@ -6765,7 +6848,38 @@ ruleTester.run('no-unused-prop-types', rule, { }, { message: '\'username\' PropType is defined but prop is never used' }] - }]) + }]), + // Issue: #2795 + { + code: ` + type ConnectedProps = DispatchProps & + StateProps + + const Component = ({ prop2, prop3 }: ConnectedProps) => { + // Do stuff + return ( + ... + ) + } + + const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + ...bindActionCreators<{prop1: ()=>void,prop2: ()=>string}>( + { prop1: importedAction, prop2: anotherImportedAction }, + dispatch, + ), + }) + + const mapStateToProps = (state: State) => ({ + prop3: Selector.value(state), + }) + + type StateProps = ReturnType + type DispatchProps = ReturnType`, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'prop1\' PropType is defined but prop is never used' + }] + } /* , { // Enable this when the following issue is fixed diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index ab40f13bd3..b68dafe534 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3026,6 +3026,89 @@ ruleTester.run('prop-types', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + // Issue: #2795 + { + code: ` + type ConnectedProps = DispatchProps & + StateProps + + const Component = ({ prop1, prop2, prop3 }: ConnectedProps) => { + // Do stuff + return ( + ... + ) + } + + const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + ...bindActionCreators<{prop1: ()=>void,prop2: ()=>string}>( + { prop1: importedAction, prop2: anotherImportedAction }, + dispatch, + ), + }) + + const mapStateToProps = (state: State) => ({ + prop3: Selector.value(state), + }) + + type StateProps = ReturnType + type DispatchProps = ReturnType`, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + // Issue: #2795 + { + code: ` + type ConnectedProps = DispatchProps & + StateProps + + const Component = ({ prop1, prop2, prop3 }: ConnectedProps) => { + // Do stuff + return ( + ... + ) + } + + const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + ...bindActionCreators>( + { prop1: importedAction, prop2: anotherImportedAction }, + dispatch, + ), + }) + + const mapStateToProps = (state: State) => ({ + prop3: Selector.value(state), + }) + + type StateProps = ReturnType + type DispatchProps = ReturnType`, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + // Issue: #2795 + { + code: ` + type ConnectedProps = DispatchProps & + StateProps + + const Component = ({ prop1, prop2, prop3 }: ConnectedProps) => { + // Do stuff + return ( + ... + ) + } + + const mapDispatchToProps = (dispatch: ThunkDispatch) => + bindActionCreators<{prop1: ()=>void,prop2: ()=>string}>( + { prop1: importedAction, prop2: anotherImportedAction }, + dispatch, + ) + + const mapStateToProps = (state: State) => ({ + prop3: Selector.value(state), + }) + + type StateProps = ReturnType + type DispatchProps = ReturnType`, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) ),