diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index c1b50766e3a..76fff982be8 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -1,5 +1,7 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import { @@ -8,6 +10,7 @@ import { getParserServices, isAwaitKeyword, isTypeAnyType, + isTypeReferenceType, isTypeUnknownType, nullThrows, NullThrowsReasons, @@ -19,6 +22,7 @@ type MessageId = | 'awaitUsingOfNonAsyncDisposable' | 'convertToOrdinaryFor' | 'forAwaitOfNonAsyncIterable' + | 'notPromises' | 'removeAwait'; export default createRule<[], MessageId>({ @@ -38,6 +42,7 @@ 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.', + notPromises: 'Unexpected non-promise input to Promise.{methodName}.', removeAwait: 'Remove unnecessary `await`.', }, schema: [], @@ -167,6 +172,53 @@ export default createRule<[], MessageId>({ } } }, + + // Check for e.g. `Promise.all(nonPromises)` + CallExpression(node): void { + if ( + node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.object.type === AST_NODE_TYPES.Identifier && + node.callee.object.name === 'Promise' && + node.callee.property.type === AST_NODE_TYPES.Identifier && + (node.callee.property.name === 'all' || + node.callee.property.name === 'allSettled' || + node.callee.property.name === 'race') + ) { + // Get the type of the thing being used in the method call. + const argument = node.arguments[0]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (argument === undefined) { + return; + } + + const tsNode = services.esTreeNodeToTSNodeMap.get(argument); + const type = checker.getTypeAtLocation(tsNode); + if (!isTypeReferenceType(type) || type.typeArguments === undefined) { + return; + } + + const typeArg = type.typeArguments[0]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (typeArg === undefined) { + return; + } + + const typeName = getTypeNameSimple(type); + const typeArgName = getTypeNameSimple(typeArg); + + if (typeName === 'Array' && typeArgName !== 'Promise') { + context.report({ + loc: node.loc, + messageId: 'notPromises', + }); + } + } + }, }; }, }); + +/** This is a simplified version of the `getTypeName` utility function. */ +function getTypeNameSimple(type: ts.Type): string | undefined { + return type.getSymbol()?.escapedName as string | undefined; +} diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index 0739334cf13..62a03e6e82e 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -278,6 +278,22 @@ async function iterateUsing(arr: Array) { } `, }, + { + code: ` +declare const promises: Array>; +await Promise.all(promises); +await Promise.allSettled(promises); +await Promise.race(promises); + `, + }, + { + code: ` +declare const promises: Iterable>; +await Promise.all(promises); +await Promise.allSettled(promises); +await Promise.race(promises); + `, + }, ], invalid: [ @@ -639,5 +655,24 @@ async function foo() { }, ], }, + { + code: ` +declare const booleans: boolean[]; +await Promise.all(booleans); +await Promise.allSettled(booleans); +await Promise.race(booleans); + `, + errors: [ + { + messageId: 'notPromises', + }, + { + messageId: 'notPromises', + }, + { + messageId: 'notPromises', + }, + ], + }, ], });