diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 40ea3254093b..b0a34e03340b 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -233,8 +233,8 @@ export default util.createRule({ } if (node.type === AST_NODE_TYPES.CallExpression) { - // If the outer expression is a call, it must be either a `.then()` or - // `.catch()` that handles the promise. + // If the outer expression is a call, a `.catch()` or `.then()` with + // rejection handler handles the promise. const catchRejectionHandler = getRejectionHandlerFromCatchCall(node); if (catchRejectionHandler) { @@ -254,10 +254,14 @@ export default util.createRule({ } } - if (isPromiseFinallyCallWithHandler(node)) { - return { isUnhandled: false }; + // `x.finally()` is transparent to resolution of the promise, so check `x`. + // ("object" in this context is the `x` in `x.finally()`) + const promiseFinallyObject = getObjectFromFinallyCall(node); + if (promiseFinallyObject) { + return isUnhandledPromise(checker, promiseFinallyObject); } + // All other cases are unhandled. return { isUnhandled: true }; } else if (node.type === AST_NODE_TYPES.ConditionalExpression) { // We must be getting the promise-like value from one of the branches of the @@ -381,13 +385,12 @@ function getRejectionHandlerFromThenCall( } } -function isPromiseFinallyCallWithHandler( +function getObjectFromFinallyCall( expression: TSESTree.CallExpression, -): boolean { - return ( - expression.callee.type === AST_NODE_TYPES.MemberExpression && +): TSESTree.Expression | undefined { + return expression.callee.type === AST_NODE_TYPES.MemberExpression && expression.callee.property.type === AST_NODE_TYPES.Identifier && - expression.callee.property.name === 'finally' && - expression.arguments.length >= 1 - ); + expression.callee.property.name === 'finally' + ? expression.callee.object + : undefined; } diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index a8701d74b6e8..c89e4316dd94 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -31,7 +31,6 @@ async function test() { .catch(() => {}) .finally(() => {}); Promise.resolve('value').catch(() => {}); - Promise.resolve('value').finally(() => {}); return Promise.resolve('value'); } `, @@ -58,7 +57,6 @@ async function test() { .catch(() => {}) .finally(() => {}); Promise.reject(new Error('message')).catch(() => {}); - Promise.reject(new Error('message')).finally(() => {}); return Promise.reject(new Error('message')); } `, @@ -77,7 +75,6 @@ async function test() { .catch(() => {}) .finally(() => {}); (async () => true)().catch(() => {}); - (async () => true)().finally(() => {}); return (async () => true)(); } `, @@ -97,7 +94,6 @@ async function test() { .catch(() => {}) .finally(() => {}); returnsPromise().catch(() => {}); - returnsPromise().finally(() => {}); return returnsPromise(); } `, @@ -106,7 +102,6 @@ async function test() { const x = Promise.resolve(); const y = x.then(() => {}); y.catch(() => {}); - y.finally(() => {}); } `, ` @@ -117,7 +112,6 @@ async function test() { ` async function test() { Promise.resolve().catch(() => {}), 123; - Promise.resolve().finally(() => {}), 123; 123, Promise.resolve().then( () => {}, @@ -160,7 +154,6 @@ async function test() { .catch(() => {}) .finally(() => {}); promiseValue.catch(() => {}); - promiseValue.finally(() => {}); return promiseValue; } `, @@ -193,12 +186,7 @@ async function test() { () => {}, ); promiseIntersection.then(() => {}).catch(() => {}); - promiseIntersection - .then(() => {}) - .catch(() => {}) - .finally(() => {}); promiseIntersection.catch(() => {}); - promiseIntersection.finally(() => {}); return promiseIntersection; } `, @@ -218,7 +206,6 @@ async function test() { .catch(() => {}) .finally(() => {}); canThen.catch(() => {}); - canThen.finally(() => {}); return canThen; } `, @@ -315,7 +302,6 @@ async function test() { .catch(() => {}) .finally(() => {}); promise.catch(() => {}); - promise.finally(() => {}); return promise; } `, @@ -333,7 +319,6 @@ async function test() { ?.then(() => {}) ?.catch(() => {}); returnsPromise()?.catch(() => {}); - returnsPromise()?.finally(() => {}); return returnsPromise(); } `, @@ -465,6 +450,31 @@ Promise.reject().catch(definitelyCallable); `, options: [{ ignoreVoid: false }], }, + { + code: ` +Promise.reject() + .catch(() => {}) + .finally(() => {}); + `, + }, + { + code: ` +Promise.reject() + .catch(() => {}) + .finally(() => {}) + .finally(() => {}); + `, + options: [{ ignoreVoid: false }], + }, + { + code: ` +Promise.reject() + .catch(() => {}) + .finally(() => {}) + .finally(() => {}) + .finally(() => {}); + `, + }, ], invalid: [ @@ -612,7 +622,6 @@ async function test() { (async () => true)(); (async () => true)().then(() => {}); (async () => true)().catch(); - (async () => true)().finally(); } `, errors: [ @@ -628,10 +637,6 @@ async function test() { line: 5, messageId: 'floatingVoid', }, - { - line: 6, - messageId: 'floatingVoid', - }, ], }, { @@ -940,7 +945,6 @@ async function test() { promiseIntersection; promiseIntersection.then(() => {}); promiseIntersection.catch(); - promiseIntersection.finally(); } `, errors: [ @@ -956,10 +960,6 @@ async function test() { line: 7, messageId: 'floatingVoid', }, - { - line: 8, - messageId: 'floatingVoid', - }, ], }, { @@ -1587,5 +1587,69 @@ Promise.reject() || 3; }, ], }, + { + code: ` +Promise.reject().finally(() => {}); + `, + errors: [{ line: 2, messageId: 'floatingVoid' }], + }, + { + code: ` +Promise.reject() + .finally(() => {}) + .finally(() => {}); + `, + options: [{ ignoreVoid: false }], + errors: [{ line: 2, messageId: 'floating' }], + }, + { + code: ` +Promise.reject() + .finally(() => {}) + .finally(() => {}) + .finally(() => {}); + `, + errors: [{ line: 2, messageId: 'floatingVoid' }], + }, + { + code: ` +Promise.reject() + .then(() => {}) + .finally(() => {}); + `, + errors: [{ line: 2, messageId: 'floatingVoid' }], + }, + { + code: ` +declare const returnsPromise: () => Promise | null; +returnsPromise()?.finally(() => {}); + `, + errors: [{ line: 3, messageId: 'floatingVoid' }], + }, + { + code: ` +const promiseIntersection: Promise & number; +promiseIntersection.finally(() => {}); + `, + errors: [{ line: 3, messageId: 'floatingVoid' }], + }, + { + code: ` +Promise.resolve().finally(() => {}), 123; + `, + errors: [{ line: 2, messageId: 'floatingVoid' }], + }, + { + code: ` +(async () => true)().finally(); + `, + errors: [{ line: 2, messageId: 'floatingVoid' }], + }, + { + code: ` +Promise.reject(new Error('message')).finally(() => {}); + `, + errors: [{ line: 2, messageId: 'floatingVoid' }], + }, ], });