diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 2ba4efbd0b06..1fc5a8a8a459 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 iterable 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,73 @@ export default createRule<[], MessageId>({ }; }, }); + +function isInvalidPromiseAggregatorInput( + checker: ts.TypeChecker, + node: ts.Node, + type: ts.Type, +): boolean { + // non array/tuple/iterable types already show up as a type error + if (!isIterable(type, checker)) { + return false; + } + + for (const part of tsutils.unionConstituents(type)) { + const valueTypes = getValueTypesOfArrayLike(part, checker); + + if (valueTypes != null) { + for (const typeArgument of valueTypes) { + if (containsNonAwaitableType(typeArgument, node, checker)) { + return true; + } + } + } + } + + return false; +} + +function getValueTypesOfArrayLike(type: ts.Type, checker: ts.TypeChecker) { + if (checker.isTupleType(type)) { + return checker.getTypeArguments(type); + } + + if (checker.isArrayLikeType(type)) { + const indexType = type.getNumberIndexType(); + + if (indexType != null) { + return [indexType]; + } + + return null; + } + + // `Iterable<...>` + if (tsutils.isTypeReference(type)) { + return checker.getTypeArguments(type).slice(0, 1); + } + + return null; +} + +function containsNonAwaitableType( + type: ts.Type, + node: ts.Node, + checker: ts.TypeChecker, +): boolean { + return tsutils + .unionConstituents(type) + .some( + typeArgumentPart => + needsToBeAwaited(checker, node, typeArgumentPart) === Awaitable.Never, + ); +} + +function isIterable(type: ts.Type, checker: ts.TypeChecker): boolean { + return tsutils + .unionConstituents(type) + .every( + part => + !!tsutils.getWellKnownSymbolPropertyOfType(part, 'iterator', checker), + ); +} diff --git a/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts b/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts new file mode 100644 index 000000000000..e5f9e67617d6 --- /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 e64f0a5f1f3c..d87975d5f9af 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -339,6 +339,218 @@ 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: ` +declare const x: Iterable, number>; +Promise.all(x); + `, + }, + + { + code: ` +declare const x: Iterable> | Array>; +Promise.all(x); + `, + }, + { + code: ` +declare const x: + | Iterable> + | [Promise, Promise]; +Promise.all(x); + `, + }, + { + code: ` +declare const x: Array> | [Promise, Promise]; +Promise.all(x); + `, + }, + + { + code: ` +Promise.all(); + `, + }, + { + code: ` +Promise.all(1); + `, + }, + { + code: ` +declare const x: Promise; +Promise.all(x); + `, + }, + { + code: ` +interface MyArray extends Array {} +declare const x: MyArray>; + +Promise.all(x); + `, + }, ], invalid: [ @@ -786,5 +998,187 @@ 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', + }, + ], + }, + + { + code: ` +declare const x: Iterable | Array>; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Iterable> | [string, Promise]; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Array | [Promise, Promise]; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Array>>; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +interface MyArray extends Array {} +declare const x: MyArray, null>; + +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, ], }); 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