Skip to content

Commit 2a4421c

Browse files
fix(eslint-plugin): [no-floating-promises] finally should be transparent to unhandled promises (#7092)
* finally is transparent to rejection * Unwrap object * split up test cases and put ignoreVoid: false on some * remove duplicated line
1 parent a9d7bb1 commit 2a4421c

File tree

2 files changed

+103
-36
lines changed

2 files changed

+103
-36
lines changed

‎packages/eslint-plugin/src/rules/no-floating-promises.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ export default util.createRule<Options, MessageId>({
233233
}
234234

235235
if (node.type === AST_NODE_TYPES.CallExpression) {
236-
// If the outer expression is a call, it must be either a `.then()` or
237-
// `.catch()` that handles the promise.
236+
// If the outer expression is a call, a `.catch()` or `.then()` with
237+
// rejection handler handles the promise.
238238

239239
const catchRejectionHandler = getRejectionHandlerFromCatchCall(node);
240240
if (catchRejectionHandler) {
@@ -254,10 +254,14 @@ export default util.createRule<Options, MessageId>({
254254
}
255255
}
256256

257-
if (isPromiseFinallyCallWithHandler(node)) {
258-
return { isUnhandled: false };
257+
// `x.finally()` is transparent to resolution of the promise, so check `x`.
258+
// ("object" in this context is the `x` in `x.finally()`)
259+
const promiseFinallyObject = getObjectFromFinallyCall(node);
260+
if (promiseFinallyObject) {
261+
return isUnhandledPromise(checker, promiseFinallyObject);
259262
}
260263

264+
// All other cases are unhandled.
261265
return { isUnhandled: true };
262266
} else if (node.type === AST_NODE_TYPES.ConditionalExpression) {
263267
// We must be getting the promise-like value from one of the branches of the
@@ -381,13 +385,12 @@ function getRejectionHandlerFromThenCall(
381385
}
382386
}
383387

384-
function isPromiseFinallyCallWithHandler(
388+
function getObjectFromFinallyCall(
385389
expression: TSESTree.CallExpression,
386-
): boolean {
387-
return (
388-
expression.callee.type === AST_NODE_TYPES.MemberExpression &&
390+
): TSESTree.Expression | undefined {
391+
return expression.callee.type === AST_NODE_TYPES.MemberExpression &&
389392
expression.callee.property.type === AST_NODE_TYPES.Identifier &&
390-
expression.callee.property.name === 'finally' &&
391-
expression.arguments.length >= 1
392-
);
393+
expression.callee.property.name === 'finally'
394+
? expression.callee.object
395+
: undefined;
393396
}

‎packages/eslint-plugin/tests/rules/no-floating-promises.test.ts‎

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ async function test() {
3131
.catch(() => {})
3232
.finally(() => {});
3333
Promise.resolve('value').catch(() => {});
34-
Promise.resolve('value').finally(() => {});
3534
return Promise.resolve('value');
3635
}
3736
`,
@@ -58,7 +57,6 @@ async function test() {
5857
.catch(() => {})
5958
.finally(() => {});
6059
Promise.reject(new Error('message')).catch(() => {});
61-
Promise.reject(new Error('message')).finally(() => {});
6260
return Promise.reject(new Error('message'));
6361
}
6462
`,
@@ -77,7 +75,6 @@ async function test() {
7775
.catch(() => {})
7876
.finally(() => {});
7977
(async () => true)().catch(() => {});
80-
(async () => true)().finally(() => {});
8178
return (async () => true)();
8279
}
8380
`,
@@ -97,7 +94,6 @@ async function test() {
9794
.catch(() => {})
9895
.finally(() => {});
9996
returnsPromise().catch(() => {});
100-
returnsPromise().finally(() => {});
10197
return returnsPromise();
10298
}
10399
`,
@@ -106,7 +102,6 @@ async function test() {
106102
const x = Promise.resolve();
107103
const y = x.then(() => {});
108104
y.catch(() => {});
109-
y.finally(() => {});
110105
}
111106
`,
112107
`
@@ -117,7 +112,6 @@ async function test() {
117112
`
118113
async function test() {
119114
Promise.resolve().catch(() => {}), 123;
120-
Promise.resolve().finally(() => {}), 123;
121115
123,
122116
Promise.resolve().then(
123117
() => {},
@@ -160,7 +154,6 @@ async function test() {
160154
.catch(() => {})
161155
.finally(() => {});
162156
promiseValue.catch(() => {});
163-
promiseValue.finally(() => {});
164157
return promiseValue;
165158
}
166159
`,
@@ -193,12 +186,7 @@ async function test() {
193186
() => {},
194187
);
195188
promiseIntersection.then(() => {}).catch(() => {});
196-
promiseIntersection
197-
.then(() => {})
198-
.catch(() => {})
199-
.finally(() => {});
200189
promiseIntersection.catch(() => {});
201-
promiseIntersection.finally(() => {});
202190
return promiseIntersection;
203191
}
204192
`,
@@ -218,7 +206,6 @@ async function test() {
218206
.catch(() => {})
219207
.finally(() => {});
220208
canThen.catch(() => {});
221-
canThen.finally(() => {});
222209
return canThen;
223210
}
224211
`,
@@ -315,7 +302,6 @@ async function test() {
315302
.catch(() => {})
316303
.finally(() => {});
317304
promise.catch(() => {});
318-
promise.finally(() => {});
319305
return promise;
320306
}
321307
`,
@@ -333,7 +319,6 @@ async function test() {
333319
?.then(() => {})
334320
?.catch(() => {});
335321
returnsPromise()?.catch(() => {});
336-
returnsPromise()?.finally(() => {});
337322
return returnsPromise();
338323
}
339324
`,
@@ -465,6 +450,31 @@ Promise.reject().catch(definitelyCallable);
465450
`,
466451
options: [{ ignoreVoid: false }],
467452
},
453+
{
454+
code: `
455+
Promise.reject()
456+
.catch(() => {})
457+
.finally(() => {});
458+
`,
459+
},
460+
{
461+
code: `
462+
Promise.reject()
463+
.catch(() => {})
464+
.finally(() => {})
465+
.finally(() => {});
466+
`,
467+
options: [{ ignoreVoid: false }],
468+
},
469+
{
470+
code: `
471+
Promise.reject()
472+
.catch(() => {})
473+
.finally(() => {})
474+
.finally(() => {})
475+
.finally(() => {});
476+
`,
477+
},
468478
],
469479

470480
invalid: [
@@ -612,7 +622,6 @@ async function test() {
612622
(async () => true)();
613623
(async () => true)().then(() => {});
614624
(async () => true)().catch();
615-
(async () => true)().finally();
616625
}
617626
`,
618627
errors: [
@@ -628,10 +637,6 @@ async function test() {
628637
line: 5,
629638
messageId: 'floatingVoid',
630639
},
631-
{
632-
line: 6,
633-
messageId: 'floatingVoid',
634-
},
635640
],
636641
},
637642
{
@@ -940,7 +945,6 @@ async function test() {
940945
promiseIntersection;
941946
promiseIntersection.then(() => {});
942947
promiseIntersection.catch();
943-
promiseIntersection.finally();
944948
}
945949
`,
946950
errors: [
@@ -956,10 +960,6 @@ async function test() {
956960
line: 7,
957961
messageId: 'floatingVoid',
958962
},
959-
{
960-
line: 8,
961-
messageId: 'floatingVoid',
962-
},
963963
],
964964
},
965965
{
@@ -1587,5 +1587,69 @@ Promise.reject() || 3;
15871587
},
15881588
],
15891589
},
1590+
{
1591+
code: `
1592+
Promise.reject().finally(() => {});
1593+
`,
1594+
errors: [{ line: 2, messageId: 'floatingVoid' }],
1595+
},
1596+
{
1597+
code: `
1598+
Promise.reject()
1599+
.finally(() => {})
1600+
.finally(() => {});
1601+
`,
1602+
options: [{ ignoreVoid: false }],
1603+
errors: [{ line: 2, messageId: 'floating' }],
1604+
},
1605+
{
1606+
code: `
1607+
Promise.reject()
1608+
.finally(() => {})
1609+
.finally(() => {})
1610+
.finally(() => {});
1611+
`,
1612+
errors: [{ line: 2, messageId: 'floatingVoid' }],
1613+
},
1614+
{
1615+
code: `
1616+
Promise.reject()
1617+
.then(() => {})
1618+
.finally(() => {});
1619+
`,
1620+
errors: [{ line: 2, messageId: 'floatingVoid' }],
1621+
},
1622+
{
1623+
code: `
1624+
declare const returnsPromise: () => Promise<void> | null;
1625+
returnsPromise()?.finally(() => {});
1626+
`,
1627+
errors: [{ line: 3, messageId: 'floatingVoid' }],
1628+
},
1629+
{
1630+
code: `
1631+
const promiseIntersection: Promise<number> & number;
1632+
promiseIntersection.finally(() => {});
1633+
`,
1634+
errors: [{ line: 3, messageId: 'floatingVoid' }],
1635+
},
1636+
{
1637+
code: `
1638+
Promise.resolve().finally(() => {}), 123;
1639+
`,
1640+
errors: [{ line: 2, messageId: 'floatingVoid' }],
1641+
},
1642+
{
1643+
code: `
1644+
(async () => true)().finally();
1645+
`,
1646+
errors: [{ line: 2, messageId: 'floatingVoid' }],
1647+
},
1648+
{
1649+
code: `
1650+
Promise.reject(new Error('message')).finally(() => {});
1651+
`,
1652+
errors: [{ line: 2, messageId: 'floatingVoid' }],
1653+
},
15901654
],
15911655
});

0 commit comments

Comments
 (0)