From 78892551647e86329de9a4c57920c604d350f61e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 1 Mar 2022 15:25:03 -0500 Subject: [PATCH 1/4] fix(eslint-plugin): [no-misused-promises] factor thenable returning function overload signatures --- .../src/rules/no-misused-promises.ts | 52 +++++++++++++++---- .../tests/rules/no-misused-promises.test.ts | 42 +++++++++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 7f08402334cc..ad4842084401 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -394,9 +394,13 @@ function voidFunctionParams( checker: ts.TypeChecker, node: ts.CallExpression | ts.NewExpression, ): Set { + const thenableReturnIndices = new Set(); const voidReturnIndices = new Set(); const type = checker.getTypeAtLocation(node.expression); + // TODO(file bug on TypeScript): checker.getResolvedSignature prefers a () => void over a () => Promise + // See https://github.com/typescript-eslint/typescript-eslint/issues/4609 (todo: comment in PR) + for (const subType of tsutils.unionTypeParts(type)) { // Standard function calls and `new` have two different types of signatures const signatures = ts.isCallExpression(node) @@ -408,13 +412,22 @@ function voidFunctionParams( parameter, node.expression, ); - if (isVoidReturningFunctionType(checker, node.expression, type)) { + if (isThenableReturningFunctionType(checker, node.expression, type)) { + thenableReturnIndices.add(index); + } else if ( + !thenableReturnIndices.has(index) && + isVoidReturningFunctionType(checker, node.expression, type) + ) { voidReturnIndices.add(index); } } } } + for (const index of thenableReturnIndices) { + voidReturnIndices.delete(index); + } + return voidReturnIndices; } @@ -424,21 +437,42 @@ function isVoidReturningFunctionType( node: ts.Node, type: ts.Type, ): boolean { - let hasVoidReturningFunction = false; - let hasThenableReturningFunction = false; for (const subType of tsutils.unionTypeParts(type)) { for (const signature of subType.getCallSignatures()) { const returnType = signature.getReturnType(); + + // If a certain positional argument accepts both thenable and void returns, + // a promise-returning function is valid + if (tsutils.isThenableType(checker, node, returnType)) { + return false; + } + if (tsutils.isTypeFlagSet(returnType, ts.TypeFlags.Void)) { - hasVoidReturningFunction = true; - } else if (tsutils.isThenableType(checker, node, returnType)) { - hasThenableReturningFunction = true; + return true; + } + } + } + return false; +} + +/** + * @returns Whether type is a void-returning function. + */ +function isThenableReturningFunctionType( + checker: ts.TypeChecker, + node: ts.Node, + type: ts.Type, +): boolean { + for (const subType of tsutils.unionTypeParts(type)) { + for (const signature of subType.getCallSignatures()) { + const returnType = signature.getReturnType(); + if (tsutils.isThenableType(checker, node, returnType)) { + return true; } } } - // If a certain positional argument accepts both thenable and void returns, - // a promise-returning function is valid - return hasVoidReturningFunction && !hasThenableReturningFunction; + + return false; } // Returns true if the expression is a function that returns a thenable diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 961a21d4c892..df786e886b4d 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -252,6 +252,30 @@ const Component: any = () => null; `, filename: 'react.tsx', }, + { + code: ` +interface ItLike { + (name: string, callback: () => Promise): void; + (name: string, callback: () => void): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + }, + { + code: ` +interface ItLike { + (name: string, callback: () => void): void; + (name: string, callback: () => Promise): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + }, ], invalid: [ @@ -687,5 +711,23 @@ const Component = (obj: O) => null; }, ], }, + { + code: ` +interface ItLike { + (name: string, callback: () => number): void; + (name: string, callback: () => void): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + errors: [ + { + line: 9, + messageId: 'voidReturnArgument', + }, + ], + }, ], }); From 0425fe816d53b114ba77a1d37747cf8cf82e682d Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 1 Mar 2022 16:17:51 -0500 Subject: [PATCH 2/4] chore: mention TypeScript issue --- packages/eslint-plugin/src/rules/no-misused-promises.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index ad4842084401..455600290e6c 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -398,8 +398,8 @@ function voidFunctionParams( const voidReturnIndices = new Set(); const type = checker.getTypeAtLocation(node.expression); - // TODO(file bug on TypeScript): checker.getResolvedSignature prefers a () => void over a () => Promise - // See https://github.com/typescript-eslint/typescript-eslint/issues/4609 (todo: comment in PR) + // We can't use checker.getResolvedSignature because it prefers an early '() => void' over a later '() => Promise' + // See https://github.com/microsoft/TypeScript/issues/48077 for (const subType of tsutils.unionTypeParts(type)) { // Standard function calls and `new` have two different types of signatures From 1911f45ac5819c04222ad2fb294e213cac65221a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 1 Mar 2022 16:26:06 -0500 Subject: [PATCH 3/4] chore: dedup and clean and fix comments --- .../src/rules/no-misused-promises.ts | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 455600290e6c..b50ad53901ef 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -431,34 +431,45 @@ function voidFunctionParams( return voidReturnIndices; } -// Returns true if given type is a void-returning function. -function isVoidReturningFunctionType( +/** + * @returns Whether any call signature of the type has a thenable return type. + */ +function anySignatureIsThenableType( checker: ts.TypeChecker, node: ts.Node, type: ts.Type, ): boolean { - for (const subType of tsutils.unionTypeParts(type)) { - for (const signature of subType.getCallSignatures()) { - const returnType = signature.getReturnType(); + for (const signature of type.getCallSignatures()) { + const returnType = signature.getReturnType(); + if (tsutils.isThenableType(checker, node, returnType)) { + return true; + } + } - // If a certain positional argument accepts both thenable and void returns, - // a promise-returning function is valid - if (tsutils.isThenableType(checker, node, returnType)) { - return false; - } + return false; +} - if (tsutils.isTypeFlagSet(returnType, ts.TypeFlags.Void)) { - return true; - } +/** + * @returns Whether type is a thenable-returning function. + */ +function isThenableReturningFunctionType( + checker: ts.TypeChecker, + node: ts.Node, + type: ts.Type, +): boolean { + for (const subType of tsutils.unionTypeParts(type)) { + if (anySignatureIsThenableType(checker, node, subType)) { + return true; } } + return false; } /** * @returns Whether type is a void-returning function. */ -function isThenableReturningFunctionType( +function isVoidReturningFunctionType( checker: ts.TypeChecker, node: ts.Node, type: ts.Type, @@ -466,26 +477,29 @@ function isThenableReturningFunctionType( for (const subType of tsutils.unionTypeParts(type)) { for (const signature of subType.getCallSignatures()) { const returnType = signature.getReturnType(); + + // If a certain positional argument accepts both thenable and void returns, + // a promise-returning function is valid if (tsutils.isThenableType(checker, node, returnType)) { + return false; + } + + if (tsutils.isTypeFlagSet(returnType, ts.TypeFlags.Void)) { return true; } } } - return false; } -// Returns true if the expression is a function that returns a thenable +/** + * @returns Whether expression is a function that returns a thenable. + */ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { const type = checker.getApparentType(checker.getTypeAtLocation(node)); - for (const subType of tsutils.unionTypeParts(type)) { - for (const signature of subType.getCallSignatures()) { - const returnType = signature.getReturnType(); - if (tsutils.isThenableType(checker, node, returnType)) { - return true; - } - } + if (anySignatureIsThenableType(checker, node, type)) { + return true; } return false; From 252e3d9d49ef480c2131be94c8544f7ffe671dda Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 2 Mar 2022 11:08:44 -0500 Subject: [PATCH 4/4] test: add a few more test cases --- .../tests/rules/no-misused-promises.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index df786e886b4d..1cc0576fb300 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -273,6 +273,34 @@ interface ItLike { declare const it: ItLike; +it('', async () => {}); + `, + }, + { + code: ` +interface ItLike { + (name: string, callback: () => void): void; +} +interface ItLike { + (name: string, callback: () => Promise): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + }, + { + code: ` +interface ItLike { + (name: string, callback: () => Promise): void; +} +interface ItLike { + (name: string, callback: () => void): void; +} + +declare const it: ItLike; + it('', async () => {}); `, }, @@ -729,5 +757,45 @@ it('', async () => {}); }, ], }, + { + code: ` +interface ItLike { + (name: string, callback: () => number): void; +} +interface ItLike { + (name: string, callback: () => void): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + errors: [ + { + line: 11, + messageId: 'voidReturnArgument', + }, + ], + }, + { + code: ` +interface ItLike { + (name: string, callback: () => void): void; +} +interface ItLike { + (name: string, callback: () => number): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + errors: [ + { + line: 11, + messageId: 'voidReturnArgument', + }, + ], + }, ], }); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy