From 4bd647ca5ee7f583e73331bc892677f376fa79e3 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 1 Jun 2025 16:34:42 +0300 Subject: [PATCH 1/7] initial implementation --- .../eslint-plugin/src/rules/await-thenable.ts | 61 ++++ .../src/util/isPromiseAggregatorMethod.ts | 40 +++ .../tests/rules/await-thenable.test.ts | 295 ++++++++++++++++++ 3 files changed, 396 insertions(+) create mode 100644 packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 2ba4efbd0b06..9680c9e267ae 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 000000000000..2ad4e956f02b --- /dev/null +++ b/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts @@ -0,0 +1,40 @@ +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', +]); + +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..d9e4a34ba6a1 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.all(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', + }, + ], + }, ], }); From 20ff75df927052fff2502d9210970371274d2671 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 2 Jun 2025 00:42:12 +0300 Subject: [PATCH 2/7] add promise.any --- packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts | 1 + packages/eslint-plugin/tests/rules/await-thenable.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts b/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts index 2ad4e956f02b..e5f9e67617d6 100644 --- a/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts +++ b/packages/eslint-plugin/src/util/isPromiseAggregatorMethod.ts @@ -16,6 +16,7 @@ const PROMISE_CONSTRUCTOR_ARRAY_METHODS = new Set([ 'all', 'allSettled', 'race', + 'any', ]); export function isPromiseAggregatorMethod( diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index d9e4a34ba6a1..0b0ddd6858f4 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -994,7 +994,7 @@ Promise.allSettled(x); { code: ` declare const x: Array>; -Promise.all(x); +Promise.any(x); `, errors: [ { From 921942e9c247d8700427e173023a0897a4365319 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 2 Jun 2025 11:29:41 +0300 Subject: [PATCH 3/7] add missing test --- packages/eslint-plugin/tests/rules/await-thenable.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index 0b0ddd6858f4..7fd9fb5f711f 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -510,6 +510,12 @@ Promise.all(); Promise.all(1); `, }, + { + code: ` +declare const x: Promise; +Promise.all(x); + `, + }, ], invalid: [ From e5bffbf39cba31c975de1820dfaa8e305e47b383 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 2 Jun 2025 17:11:44 +0300 Subject: [PATCH 4/7] wip --- .../eslint-plugin/src/rules/await-thenable.ts | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 9680c9e267ae..c082bf6abbd9 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -215,20 +215,16 @@ function isInvalidPromiseAggregatorInput( 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; + } + + // check for non for (const part of tsutils.unionConstituents(type)) { - if ( - tsutils.isTypeReference(part) && - tsutils.getWellKnownSymbolPropertyOfType(part, 'iterator', checker) - ) { + if (tsutils.isTypeReference(part)) { for (const typeArgument of checker.getTypeArguments(part)) { - if ( - tsutils.unionConstituents(typeArgument).some(typeArgumentPart => { - return ( - needsToBeAwaited(checker, node, typeArgumentPart) === - Awaitable.Never - ); - }) - ) { + if (isTypeNeverAwaitable(typeArgument, node, checker)) { return true; } } @@ -237,3 +233,25 @@ function isInvalidPromiseAggregatorInput( return false; } + +function isTypeNeverAwaitable( + 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), + ); +} From d46056accdc32430046a2450984db054c37530b8 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Jun 2025 17:12:49 +0300 Subject: [PATCH 5/7] refactor, fix edge cases --- .../eslint-plugin/src/rules/await-thenable.ts | 12 ++-- .../tests/rules/await-thenable.test.ts | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index c082bf6abbd9..6db5be367655 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -220,11 +220,15 @@ function isInvalidPromiseAggregatorInput( return false; } - // check for non for (const part of tsutils.unionConstituents(type)) { if (tsutils.isTypeReference(part)) { - for (const typeArgument of checker.getTypeArguments(part)) { - if (isTypeNeverAwaitable(typeArgument, node, checker)) { + // only check the first type argument of `Iterator<...>` or `Array<...>` + const typeArguments = checker.isTupleType(part) + ? part.typeArguments + : part.typeArguments?.slice(0, 1); + + for (const typeArgument of typeArguments ?? []) { + if (containsNonAwaitableType(typeArgument, node, checker)) { return true; } } @@ -234,7 +238,7 @@ function isInvalidPromiseAggregatorInput( return false; } -function isTypeNeverAwaitable( +function containsNonAwaitableType( type: ts.Type, node: ts.Node, checker: ts.TypeChecker, diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index 7fd9fb5f711f..c452449e24c6 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -496,6 +496,33 @@ 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); `, }, @@ -1079,6 +1106,40 @@ Promise.all(x); { 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: [ From ad08eb107a649d531962f9926263ccb2a0ae758a Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Jun 2025 17:53:59 +0300 Subject: [PATCH 6/7] add nested array test-case --- .../eslint-plugin/tests/rules/await-thenable.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index c452449e24c6..03b117f3c3d3 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -1140,6 +1140,17 @@ Promise.all(x); { code: ` declare const x: Array | [Promise, Promise]; +Promise.all(x); + `, + errors: [ + { + messageId: 'invalidPromiseAggregatorInput', + }, + ], + }, + { + code: ` +declare const x: Array>>; Promise.all(x); `, errors: [ From bcf18dc7350eee833a097eebaf34d09eb016b117 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Jun 2025 23:51:16 +0300 Subject: [PATCH 7/7] missing coverage --- packages/eslint-plugin/src/rules/await-thenable.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 6db5be367655..2b19c25daa69 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -222,12 +222,14 @@ function isInvalidPromiseAggregatorInput( for (const part of tsutils.unionConstituents(type)) { if (tsutils.isTypeReference(part)) { + const typeArguments = checker.getTypeArguments(part); + // only check the first type argument of `Iterator<...>` or `Array<...>` - const typeArguments = checker.isTupleType(part) - ? part.typeArguments - : part.typeArguments?.slice(0, 1); + const checkedTypeArguments = checker.isTupleType(part) + ? typeArguments + : typeArguments.slice(0, 1); - for (const typeArgument of typeArguments ?? []) { + for (const typeArgument of checkedTypeArguments) { if (containsNonAwaitableType(typeArgument, node, checker)) { return true; } 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