From d63ef18a3b7ec3ca2c167ab3b1e5a0b978d75cea Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 30 Sep 2025 13:13:32 +0300 Subject: [PATCH 1/2] initial implementation --- .../src/rules/no-misused-promises.ts | 30 +++++++++++++++++++ .../tests/rules/no-misused-promises.test.ts | 7 +++++ 2 files changed, 37 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index aa225d4e0aa2..dc3cdd13d9a8 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -6,10 +6,13 @@ import * as ts from 'typescript'; import { createRule, + getConstrainedTypeAtLocation, getFunctionHeadLoc, getParserServices, + getStaticMemberAccessValue, isArrayMethodCallWithPredicate, isFunction, + isPromiseLike, isRestParameterDeclaration, nullThrows, NullThrowsReasons, @@ -360,6 +363,13 @@ export default createRule({ function checkArguments( node: TSESTree.CallExpression | TSESTree.NewExpression, ): void { + if ( + node.type === AST_NODE_TYPES.CallExpression && + isPromiseFinallyMethod(node) + ) { + return; + } + const tsNode = services.esTreeNodeToTSNodeMap.get(node); const voidArgs = voidFunctionArguments(checker, tsNode); if (voidArgs.size === 0) { @@ -563,6 +573,26 @@ export default createRule({ } } + function isPromiseFinallyMethod(node: TSESTree.CallExpression): boolean { + if (node.callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const staticAccessValue = getStaticMemberAccessValue( + node.callee, + context, + ); + + if (staticAccessValue !== 'finally') { + return false; + } + + return isPromiseLike( + services.program, + getConstrainedTypeAtLocation(services, node.callee.object), + ); + } + function checkClassLikeOrInterfaceNode( node: | TSESTree.ClassDeclaration diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 5f7f1faf8bb4..dc671e8869d6 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -1096,6 +1096,13 @@ declare const useCallback: unknown>( ) => T; useCallback(async () => {}); `, + ` +Promise.reject(3).finally(async () => {}); + `, + ` +const f = 'finally'; +Promise.reject(3)[f](async () => {}); + `, ], invalid: [ From be919d8a46efbf4c4df170d03adeec57a01c7c37 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Thu, 2 Oct 2025 23:03:53 +0300 Subject: [PATCH 2/2] use 'parseFinallyCall' to simplify implementation --- .../src/rules/no-misused-promises.ts | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index dc3cdd13d9a8..b791fe150584 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -9,7 +9,6 @@ import { getConstrainedTypeAtLocation, getFunctionHeadLoc, getParserServices, - getStaticMemberAccessValue, isArrayMethodCallWithPredicate, isFunction, isPromiseLike, @@ -17,6 +16,7 @@ import { nullThrows, NullThrowsReasons, } from '../util'; +import { parseFinallyCall } from '../util/promiseUtils'; export type Options = [ { @@ -574,22 +574,14 @@ export default createRule({ } function isPromiseFinallyMethod(node: TSESTree.CallExpression): boolean { - if (node.callee.type !== AST_NODE_TYPES.MemberExpression) { - return false; - } - - const staticAccessValue = getStaticMemberAccessValue( - node.callee, - context, - ); - - if (staticAccessValue !== 'finally') { - return false; - } - - return isPromiseLike( - services.program, - getConstrainedTypeAtLocation(services, node.callee.object), + const promiseFinallyCall = parseFinallyCall(node, context); + + return ( + promiseFinallyCall != null && + isPromiseLike( + services.program, + getConstrainedTypeAtLocation(services, promiseFinallyCall.object), + ) ); }