From 74cf4b71ac553bf88cecd45fd621dee8194e100d Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:03:01 -0500 Subject: [PATCH 1/3] fix: await-thenable --- .../eslint-plugin/src/rules/await-thenable.ts | 53 ++++++++++++++++++- .../tests/rules/await-thenable.test.ts | 35 ++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index d71efd65139..784ee11c46f 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -1,12 +1,14 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; - +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; +import type * as ts from 'typescript'; import { createRule, getParserServices, isAwaitKeyword, isTypeAnyType, + isTypeReferenceType, isTypeUnknownType, nullThrows, NullThrowsReasons, @@ -17,7 +19,8 @@ type MessageId = | 'await' | 'convertToOrdinaryFor' | 'forAwaitOfNonThenable' - | 'removeAwait'; + | 'removeAwait' + | 'notPromises'; export default createRule<[], MessageId>({ name: 'await-thenable', @@ -35,6 +38,7 @@ export default createRule<[], MessageId>({ forAwaitOfNonThenable: 'Unexpected `for await...of` of a value that is not async iterable.', removeAwait: 'Remove unnecessary `await`.', + notPromises: 'Unexpected non-promise input to Promise.{methodName}.', }, schema: [], }, @@ -112,6 +116,51 @@ 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]; + const tsNode = services.esTreeNodeToTSNodeMap.get(argument); + const type = checker.getTypeAtLocation(tsNode); + if (!isTypeReferenceType(type) || type.typeArguments === undefined) { + return; + } + + const firstTypeArg = type.typeArguments[0]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (firstTypeArg === undefined) { + return; + } + + const typeName = getTypeNameSimple(type); + if (typeName !== 'Array') { + return; + } + + const firstTypeArgName = getTypeNameSimple(firstTypeArg); + if (firstTypeArgName !== '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 c91542819bb..a019370ff2f 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -227,6 +227,22 @@ for await (const s of asyncIter) { } `, }, + { + 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: [ @@ -475,5 +491,24 @@ for (const value of yieldNumberPromises()) { }, ], }, + { + code: ` +declare const booleans: boolean[]; +await Promise.all(booleans); +await Promise.allSettled(booleans); +await Promise.race(booleans); + `, + errors: [ + { + messageId: 'notPromises', + }, + { + messageId: 'notPromises', + }, + { + messageId: 'notPromises', + }, + ], + }, ], }); From da7cdb5fdff1ac6a10ff4d189264e87d82aab005 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:05:55 -0500 Subject: [PATCH 2/3] update --- .../eslint-plugin/src/rules/await-thenable.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 784ee11c46f..ff1e97dafb9 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -130,25 +130,27 @@ export default createRule<[], MessageId>({ ) { // 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 firstTypeArg = type.typeArguments[0]; + const typeArg = type.typeArguments[0]; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (firstTypeArg === undefined) { + if (typeArg === undefined) { return; } const typeName = getTypeNameSimple(type); - if (typeName !== 'Array') { - return; - } + const typeArgName = getTypeNameSimple(typeArg); - const firstTypeArgName = getTypeNameSimple(firstTypeArg); - if (firstTypeArgName !== 'Promise') { + if (typeName === 'Array' && typeArgName !== 'Promise') { context.report({ loc: node.loc, messageId: 'notPromises', From 676d2a20270a3a2033ec379ebe5a85f4a2a51dcc Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:52:44 -0500 Subject: [PATCH 3/3] update --- packages/eslint-plugin/src/rules/await-thenable.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 312b7517d36..76fff982be8 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -1,7 +1,8 @@ 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 type * as ts from 'typescript'; import { createRule, @@ -21,8 +22,8 @@ type MessageId = | 'awaitUsingOfNonAsyncDisposable' | 'convertToOrdinaryFor' | 'forAwaitOfNonAsyncIterable' - | 'removeAwait' - | 'notPromises'; + | 'notPromises' + | 'removeAwait'; export default createRule<[], MessageId>({ name: 'await-thenable', @@ -41,8 +42,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.', - removeAwait: 'Remove unnecessary `await`.', notPromises: 'Unexpected non-promise input to Promise.{methodName}.', + removeAwait: 'Remove unnecessary `await`.', }, schema: [], }, @@ -171,7 +172,7 @@ export default createRule<[], MessageId>({ } } }, - + // Check for e.g. `Promise.all(nonPromises)` CallExpression(node): void { if (