diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index c1b50766e3ae..6fb49227470a 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -3,12 +3,13 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import { + Awaitable, createRule, getFixOrSuggest, getParserServices, isAwaitKeyword, isTypeAnyType, - isTypeUnknownType, + needsToBeAwaited, nullThrows, NullThrowsReasons, } from '../util'; @@ -51,13 +52,11 @@ export default createRule<[], MessageId>({ return { AwaitExpression(node): void { const type = services.getTypeAtLocation(node.argument); - if (isTypeAnyType(type) || isTypeUnknownType(type)) { - return; - } const originalNode = services.esTreeNodeToTSNodeMap.get(node); + const certainty = needsToBeAwaited(checker, originalNode, type); - if (!tsutils.isThenableType(checker, originalNode.expression, type)) { + if (certainty === Awaitable.Never) { context.report({ node, messageId: 'await', diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index 7795894a9584..18cc6e9e420a 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -1,17 +1,16 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; import { + Awaitable, createRule, getFixOrSuggest, getParserServices, isAwaitExpression, isAwaitKeyword, - isTypeAnyType, - isTypeUnknownType, + needsToBeAwaited, nullThrows, } from '../util'; import { getOperatorPrecedence } from '../util/getOperatorPrecedence'; @@ -304,14 +303,13 @@ export default createRule({ } const type = checker.getTypeAtLocation(child); - const isThenable = tsutils.isThenableType(checker, expression, type); + const certainty = needsToBeAwaited(checker, expression, type); // handle awaited _non_thenables - if (!isThenable) { + if (certainty !== Awaitable.Always) { if (isAwait) { - // any/unknown could be thenable; do not enforce whether they are `await`ed. - if (isTypeAnyType(type) || isTypeUnknownType(type)) { + if (certainty === Awaitable.May) { return; } context.report({ diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 3f1e4b5cb2dc..3aa935a51a95 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -20,6 +20,7 @@ export * from './isUndefinedIdentifier'; export * from './misc'; export * from './needsPrecedingSemiColon'; export * from './objectIterators'; +export * from './needsToBeAwaited'; export * from './scopeUtils'; export * from './types'; diff --git a/packages/eslint-plugin/src/util/needsToBeAwaited.ts b/packages/eslint-plugin/src/util/needsToBeAwaited.ts new file mode 100644 index 000000000000..e6d675cc321e --- /dev/null +++ b/packages/eslint-plugin/src/util/needsToBeAwaited.ts @@ -0,0 +1,43 @@ +import type * as ts from 'typescript'; + +import { + isTypeAnyType, + isTypeUnknownType, +} from '@typescript-eslint/type-utils'; +import * as tsutils from 'ts-api-utils'; + +export enum Awaitable { + Always, + Never, + May, +} + +export function needsToBeAwaited( + checker: ts.TypeChecker, + node: ts.Node, + type: ts.Type, +): Awaitable { + // can't use `getConstrainedTypeAtLocation` directly since it's bugged for + // unconstrained generics. + const constrainedType = !tsutils.isTypeParameter(type) + ? type + : checker.getBaseConstraintOfType(type); + + // unconstrained generic types should be treated as unknown + if (constrainedType == null) { + return Awaitable.May; + } + + // `any` and `unknown` types may need to be awaited + if (isTypeAnyType(constrainedType) || isTypeUnknownType(constrainedType)) { + return Awaitable.May; + } + + // 'thenable' values should always be be awaited + if (tsutils.isThenableType(checker, node, constrainedType)) { + return Awaitable.Always; + } + + // anything else should not be awaited + return Awaitable.Never; +} diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index 0739334cf130..1860db956c3e 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -275,6 +275,68 @@ async function foo() { async function iterateUsing(arr: Array) { for (await using foo of arr) { } +} + `, + }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + }, + { + code: ` +async function wrapper>(value: T) { + return await value; +} + `, + }, + { + code: ` +async function wrapper>(value: T) { + return await value; +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } } `, }, @@ -639,5 +701,91 @@ async function foo() { }, ], }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + errors: [ + { + column: 10, + endColumn: 21, + endLine: 3, + line: 3, + messageId: 'await', + suggestions: [ + { + messageId: 'removeAwait', + output: ` +async function wrapper(value: T) { + return value; +} + `, + }, + ], + }, + ], + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + errors: [ + { + column: 12, + endColumn: 23, + endLine: 4, + line: 4, + messageId: 'await', + suggestions: [ + { + messageId: 'removeAwait', + output: ` +class C { + async wrapper(value: T) { + return value; + } +} + `, + }, + ], + }, + ], + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + errors: [ + { + column: 12, + endColumn: 23, + endLine: 4, + line: 4, + messageId: 'await', + suggestions: [ + { + messageId: 'removeAwait', + output: ` +class C { + async wrapper(value: T) { + return value; + } +} + `, + }, + ], + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/return-await.test.ts b/packages/eslint-plugin/tests/rules/return-await.test.ts index e7610eaba6ee..9a44a5954d66 100644 --- a/packages/eslint-plugin/tests/rules/return-await.test.ts +++ b/packages/eslint-plugin/tests/rules/return-await.test.ts @@ -445,6 +445,54 @@ return Promise.resolve(42); { using foo = 1 as any; return Promise.resolve(42); +} + `, + }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } } `, }, @@ -1567,6 +1615,68 @@ async function outerFunction() { }; const innerFunction = async () => asyncFn(); +} + `, + }, + { + code: ` +async function wrapper(value: T) { + return await value; +} + `, + errors: [ + { + line: 3, + messageId: 'nonPromiseAwait', + }, + ], + output: ` +async function wrapper(value: T) { + return value; +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + errors: [ + { + line: 4, + messageId: 'nonPromiseAwait', + }, + ], + output: ` +class C { + async wrapper(value: T) { + return value; + } +} + `, + }, + { + code: ` +class C { + async wrapper(value: T) { + return await value; + } +} + `, + errors: [ + { + line: 4, + messageId: 'nonPromiseAwait', + }, + ], + output: ` +class C { + async wrapper(value: T) { + return value; + } } `, },