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']
}
])
),