diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 2ba4efbd0b0..9680c9e267a 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -1,10 +1,12 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; import * as tsutils from 'ts-api-utils'; import { Awaitable, createRule, + getConstrainedTypeAtLocation, getFixOrSuggest, getParserServices, isAwaitKeyword, @@ -14,12 +16,14 @@ import { NullThrowsReasons, } from '../util'; import { getForStatementHeadLoc } from '../util/getForStatementHeadLoc'; +import { isPromiseAggregatorMethod } from '../util/isPromiseAggregatorMethod'; export type MessageId = | 'await' | 'awaitUsingOfNonAsyncDisposable' | 'convertToOrdinaryFor' | 'forAwaitOfNonAsyncIterable' + | 'invalidPromiseAggregatorInput' | 'removeAwait'; export default createRule<[], MessageId>({ @@ -39,6 +43,8 @@ export default createRule<[], MessageId>({ convertToOrdinaryFor: 'Convert to an ordinary `for...of` loop.', forAwaitOfNonAsyncIterable: 'Unexpected `for await...of` of a value that is not async iterable.', + invalidPromiseAggregatorInput: + 'Unexpected iterator of non-Promise (non-"Thenable") values passed to promise aggregator.', removeAwait: 'Remove unnecessary `await`.', }, schema: [], @@ -84,6 +90,33 @@ export default createRule<[], MessageId>({ } }, + CallExpression(node: TSESTree.CallExpression): void { + if (!isPromiseAggregatorMethod(context, services, node)) { + return; + } + + const argument = node.arguments.at(0); + + if (argument == null) { + return; + } + + const type = getConstrainedTypeAtLocation(services, argument); + + if ( + isInvalidPromiseAggregatorInput( + checker, + services.esTreeNodeToTSNodeMap.get(argument), + type, + ) + ) { + context.report({ + node: argument, + messageId: 'invalidPromiseAggregatorInput', + }); + } + }, + 'ForOfStatement[await=true]'(node: TSESTree.ForOfStatement): void { const type = services.getTypeAtLocation(node.right); if (isTypeAnyType(type)) { @@ -176,3 +209,31 @@ export default createRule<[], MessageId>({ }; }, }); + +function isInvalidPromiseAggregatorInput( + checker: ts.TypeChecker, + node: ts.Node, + type: ts.Type, +): boolean { + for (const part of tsutils.unionConstituents(type)) { + if ( + tsutils.isTypeReference(part) && + tsutils.getWellKnownSymbolPropertyOfType(part, 'iterator', checker) + ) { + for (const typeArgument of checker.getTypeArguments(part)) { + if ( + tsutils.unionConstituents(typeArgument).some(typeArgumentPart => { + return ( + needsToBeAwaited(checker, node, typeArgumentPart) === + Awaitable.Never + ); + }) + ) { + return true; + } + } + } + } + + return false; +} diff --git a/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts b/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts new file mode 100644 index 00000000000..e5f9e67617d --- /dev/null +++ b/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts @@ -0,0 +1,41 @@ +import type { + ParserServicesWithTypeInformation, + TSESTree, +} from '@typescript-eslint/utils'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +import { + getConstrainedTypeAtLocation, + isPromiseConstructorLike, +} from '@typescript-eslint/type-utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { getStaticMemberAccessValue } from './misc'; + +const PROMISE_CONSTRUCTOR_ARRAY_METHODS = new Set([ + 'all', + 'allSettled', + 'race', + 'any', +]); + +export function isPromiseAggregatorMethod( + context: RuleContext, + services: ParserServicesWithTypeInformation, + node: TSESTree.CallExpression, +): boolean { + if (node.callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const staticAccessValue = getStaticMemberAccessValue(node.callee, context); + + if (!PROMISE_CONSTRUCTOR_ARRAY_METHODS.has(staticAccessValue)) { + return false; + } + + return isPromiseConstructorLike( + services.program, + getConstrainedTypeAtLocation(services, node.callee.object), + ); +} diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index e64f0a5f1f3..0b0ddd6858f 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -339,6 +339,177 @@ class C { } `, }, + + { + code: ` +declare const x: unknown; +Promise.all(x); + `, + }, + { + code: ` +declare const x: any; +Promise.all(x); + `, + }, + + { + code: ` +declare const x: Array>; +Promise.all(x); + `, + }, + { + code: ` +declare const x: Array> | Array>; +Promise.all(x); + `, + }, + { + code: ` +declare const x: Array | Promise>; +Promise.all(x); + `, + }, + { + code: ` +function f(x: Array>) { + Promise.all(x); +} + `, + }, + { + code: ` +function f>(x: Array) { + Promise.all(x); +} + `, + }, + { + code: ` +declare const x: Array; +Promise.all(x); + `, + }, + { + code: ` +declare const x: Array; +Promise.all(x); + `, + }, + { + code: ` +declare const x: number | Array>; +Promise.all(x); + `, + }, + + { + code: ` +declare const x: [Promise, Promise]; +Promise.all(x); + `, + }, + { + code: ` +declare const x: [Promise] | [Promise]; +Promise.all(x); + `, + }, + { + code: ` +declare const x: [Promise | Promise]; +Promise.all(x); + `, + }, + { + code: ` +function f(x: [Promise]) { + Promise.all(x); +} + `, + }, + { + code: ` +function f>(x: [T]) { + Promise.all(x); +} + `, + }, + { + code: ` +declare const x: [unknown, any]; +Promise.all(x); + `, + }, + { + code: ` +declare const x: number | [Promise]; +Promise.all(x); + `, + }, + + { + code: ` +declare const x: Iterable>; +Promise.all(x); + `, + }, + { + code: ` +declare const x: Iterable> | Iterable>; +Promise.all(x); + `, + }, + { + code: ` +declare const x: Iterable>; +Promise.all(x); + `, + }, + { + code: ` +function f(x: Iterable>) { + Promise.all(x); +} + `, + }, + { + code: ` +function f>(x: Iterable) { + Promise.all(x); +} + `, + }, + { + code: ` +declare const x: Iterable; +Promise.all(x); + `, + }, + { + code: ` +declare const x: Iterable; +Promise.all(x); + `, + }, + { + code: ` +declare const x: number | Iterable>; +Promise.all(x); + `, + }, + + { + code: ` +Promise.all(); + `, + }, + { + code: ` +Promise.all(1); + `, + }, ], invalid: [ @@ -786,5 +957,129 @@ class C { }, ], }, + + { + code: ` +declare const x: Array; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Array | Array>; +Promise.race(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Array | Array; +Promise.allSettled(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Array>; +Promise.any(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + + { + code: ` +declare const x: [number]; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: [number] | [Promise]; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: [number | Promise]; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: [Promise, number]; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + + { + code: ` +declare const x: Iterable; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Iterable | Iterable>; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Iterable>; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, ], });