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; + } } `, }, 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