diff --git a/CHANGELOG.md b/CHANGELOG.md index 153e4e21af..f5dff5ab25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,12 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`prop-types`]: Detect JSX returned by sequential expression ([#2801][] @mikol) * [`jsx-props-no-multi-spaces`]: "Expected no line gap between" false positive ([#2792][] @karolina-benitez) * [`no-unknown-property`]: check attributes with any input case ([#2790][] @julienw) +* [`prop-types`]/[`no-unused-prop-types`]: handle CallExpression in ReturnType ([#2802][] @hank121314) ### Changed * [Tests] [`jsx-one-expression-per-line`]: add passing tests ([#2799][] @TaLeaMonet) +[#2802]: https://github.com/yannickcr/eslint-plugin-react/pull/2802 [#2801]: https://github.com/yannickcr/eslint-plugin-react/pull/2801 [#2799]: https://github.com/yannickcr/eslint-plugin-react/pull/2799 [#2796]: https://github.com/yannickcr/eslint-plugin-react/pull/2796 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..7ae9947f20 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -3645,7 +3645,7 @@ ruleTester.run('no-unused-prop-types', rule, { type User = { user: string; } - + type Props = User; export default (props: Props) => { @@ -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'] } ]) ), @@ -6308,11 +6391,11 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { z: string; } - + interface Bar extends Foo { y: string; } - + const Baz = ({ x, y }: Bar) => ( {x} @@ -6334,11 +6417,11 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { z: string; } - + interface Bar extends Foo { y: string; } - + const Baz = ({ x, y }: Bar) => ( {x} @@ -6356,11 +6439,11 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { x: number; } - + interface Bar extends Foo { y: string; } - + const Baz = ({ x }: Bar) => ( {x} @@ -6377,7 +6460,7 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { x: number; } - + interface Bar { y: string; } @@ -6385,7 +6468,7 @@ ruleTester.run('no-unused-prop-types', rule, { interface Baz { z:string; } - + const Baz = ({ x }: Bar & Foo & Baz) => ( {x} @@ -6404,7 +6487,7 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { x: number; } - + interface Bar { y: string; } @@ -6412,7 +6495,7 @@ ruleTester.run('no-unused-prop-types', rule, { interface Baz { z:string; } - + const Baz = ({ x }: Bar & Foo & Baz) => ( {x} @@ -6435,11 +6518,11 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { z: string; } - + interface Bar extends Foo { y: string; } - + const Baz = ({ x }: Bar) => ( {x} @@ -6460,11 +6543,11 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { z: string; } - + interface Bar extends Foo { y: string; } - + const Baz = ({ x }: Bar) => ( {x} @@ -6485,11 +6568,11 @@ ruleTester.run('no-unused-prop-types', rule, { interface Foo { z: string; } - + interface Foo { y: string; } - + const Baz = ({ x }: Foo) => ( {x} @@ -6513,22 +6596,22 @@ ruleTester.run('no-unused-prop-types', rule, { type AgeProps = { age: number; } - + type BirthdayProps = { birthday: string; } - + type intersectionUserProps = AgeProps & BirthdayProps; - + type Props = User & UserProps & intersectionUserProps; export default (props: Props) => { const { userId, user } = props; - + if (userId === 0) { return

userId is 0

; } - + return null; }; `, @@ -6552,22 +6635,22 @@ ruleTester.run('no-unused-prop-types', rule, { type AgeProps = { age: number; } - + type BirthdayProps = { birthday: string; } - + type intersectionUserProps = AgeProps & BirthdayProps; - + type Props = User & UserProps & intersectionUserProps; export default (props: Props) => { const { userId, user } = props; - + if (userId === 0) { return

userId is 0

; } - + return null; }; `, @@ -6583,10 +6666,10 @@ ruleTester.run('no-unused-prop-types', rule, { const mapStateToProps = state => ({ books: state.books }); - + interface InfoLibTableProps extends ReturnType { } - + const App = (props: InfoLibTableProps) => { return
; } @@ -6601,10 +6684,10 @@ ruleTester.run('no-unused-prop-types', rule, { const mapStateToProps = state => ({ books: state.books }); - + interface InfoLibTableProps extends ReturnType { } - + const App = (props: InfoLibTableProps) => { return
; } @@ -6619,11 +6702,11 @@ ruleTester.run('no-unused-prop-types', rule, { const mapStateToProps = state => ({ books: state.books, }); - + interface BooksTable extends ReturnType { - username: string; + username: string; } - + const App = (props: BooksTable) => { return
; } @@ -6640,11 +6723,11 @@ ruleTester.run('no-unused-prop-types', rule, { const mapStateToProps = state => ({ books: state.books, }); - + interface BooksTable extends ReturnType { - username: string; + username: string; } - + const App = (props: BooksTable) => { return
; } @@ -6659,9 +6742,9 @@ ruleTester.run('no-unused-prop-types', rule, { { code: ` interface BooksTable extends ReturnType<() => {books:Array}> { - username: string; + username: string; } - + const App = (props: BooksTable) => { return
; } @@ -6676,9 +6759,9 @@ ruleTester.run('no-unused-prop-types', rule, { { code: ` interface BooksTable extends ReturnType<() => {books:Array}> { - username: string; + username: string; } - + const App = (props: BooksTable) => { return
; } @@ -6693,9 +6776,9 @@ ruleTester.run('no-unused-prop-types', rule, { { code: ` type BooksTable = ReturnType<() => {books:Array}> & { - username: string; + username: string; } - + const App = (props: BooksTable) => { return
; } @@ -6710,9 +6793,9 @@ ruleTester.run('no-unused-prop-types', rule, { { code: ` type BooksTable = ReturnType<() => {books:Array}> & { - username: string; + username: string; } - + const App = (props: BooksTable) => { return
; } @@ -6729,11 +6812,11 @@ ruleTester.run('no-unused-prop-types', rule, { type mapStateToProps = ReturnType<() => {books:Array}>; type Props = { - username: string; + username: string; } type BooksTable = mapStateToProps & Props; - + const App = (props: BooksTable) => { return
; } @@ -6750,11 +6833,11 @@ ruleTester.run('no-unused-prop-types', rule, { type mapStateToProps = ReturnType<() => {books:Array}>; type Props = { - username: string; + username: string; } type BooksTable = mapStateToProps & Props; - + const App = (props: BooksTable) => { return
; } @@ -6765,6 +6848,37 @@ 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' + }] }]) /* , { 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'] } ]) ),