diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx
index e3f047e6e4f0..f2cb066ceb2d 100644
--- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx
+++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx
@@ -84,6 +84,55 @@ await Promise.all([1, 2, 3].map(async x => x + 1));
## Options
+### `checkThenables`
+
+A ["Thenable"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables) value is an object which has a `then` method, such as a `Promise`.
+Other Thenables include TypeScript's built-in `PromiseLike` interface and any custom object that happens to have a `.then()`.
+
+The `checkThenables` option triggers `no-floating-promises` to also consider all values that satisfy the Thenable shape (a `.then()` method that takes two callback parameters), not just Promises.
+This can be useful if your code works with older `Promise` polyfills instead of the native `Promise` class.
+
+
+
+
+```ts option='{"checkThenables": true}'
+declare function createPromiseLike(): PromiseLike;
+
+createPromiseLike();
+
+interface MyThenable {
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
+}
+
+declare function createMyThenable(): MyThenable;
+
+createMyThenable();
+```
+
+
+
+
+```ts option='{"checkThenables": true}'
+declare function createPromiseLike(): PromiseLike;
+
+await createPromiseLike();
+
+interface MyThenable {
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
+}
+
+declare function createMyThenable(): MyThenable;
+
+await createMyThenable();
+```
+
+
+
+
+:::info
+This option is enabled by default in v7 but will be turned off by default in v8.
+:::
+
### `ignoreVoid`
This option, which is `true` by default, allows you to stop the rule reporting promises consumed with void operator.
diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts
index 1ae5e602ae0e..b49516184317 100644
--- a/packages/eslint-plugin/src/rules/no-floating-promises.ts
+++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts
@@ -8,6 +8,7 @@ import {
createRule,
getOperatorPrecedence,
getParserServices,
+ isBuiltinSymbolLike,
OperatorPrecedence,
readonlynessOptionsDefaults,
readonlynessOptionsSchema,
@@ -16,9 +17,10 @@ import {
type Options = [
{
- ignoreVoid?: boolean;
- ignoreIIFE?: boolean;
allowForKnownSafePromises?: TypeOrValueSpecifier[];
+ checkThenables?: boolean;
+ ignoreIIFE?: boolean;
+ ignoreVoid?: boolean;
},
];
@@ -75,6 +77,12 @@ export default createRule({
{
type: 'object',
properties: {
+ allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow,
+ checkThenables: {
+ description:
+ 'Whether to check all "Thenable"s, not just the built-in Promise type.',
+ type: 'boolean',
+ },
ignoreVoid: {
description: 'Whether to ignore `void` expressions.',
type: 'boolean',
@@ -84,7 +92,6 @@ export default createRule({
'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).',
type: 'boolean',
},
- allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow,
},
additionalProperties: false,
},
@@ -93,15 +100,18 @@ export default createRule({
},
defaultOptions: [
{
+ allowForKnownSafePromises: readonlynessOptionsDefaults.allow,
+ checkThenables: true,
ignoreVoid: true,
ignoreIIFE: false,
- allowForKnownSafePromises: readonlynessOptionsDefaults.allow,
},
],
create(context, [options]) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
+ const { checkThenables } = options;
+
// TODO: #5439
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const allowForKnownSafePromises = options.allowForKnownSafePromises!;
@@ -356,14 +366,10 @@ export default createRule({
return false;
}
- // Modified from tsutils.isThenable() to only consider thenables which can be
- // rejected/caught via a second parameter. Original source (MIT licensed):
- //
- // https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125
function isPromiseLike(node: ts.Node, type?: ts.Type): boolean {
type ??= checker.getTypeAtLocation(node);
- // Ignore anything specified by `allowForKnownSafePromises` option.
+ // The highest priority is to allow anything allowlisted
if (
allowForKnownSafePromises.some(allowedType =>
typeMatchesSpecifier(type, allowedType, services.program),
@@ -372,7 +378,26 @@ export default createRule({
return false;
}
- for (const ty of tsutils.unionTypeParts(checker.getApparentType(type))) {
+ // Otherwise, we always consider the built-in Promise to be Promise-like...
+ const typeParts = tsutils.unionTypeParts(checker.getApparentType(type));
+ if (
+ typeParts.some(typePart =>
+ isBuiltinSymbolLike(services.program, typePart, 'Promise'),
+ )
+ ) {
+ return true;
+ }
+
+ // ...and only check all Thenables if explicitly told to
+ if (!checkThenables) {
+ return false;
+ }
+
+ // Modified from tsutils.isThenable() to only consider thenables which can be
+ // rejected/caught via a second parameter. Original source (MIT licensed):
+ //
+ // https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125
+ for (const ty of typeParts) {
const then = ty.getProperty('then');
if (then === undefined) {
continue;
diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot
index 44e303fc3131..5dafc5366a02 100644
--- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot
+++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot
@@ -50,6 +50,44 @@ await Promise.all([1, 2, 3].map(async x => x + 1));
`;
exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 3`] = `
+"Incorrect
+Options: {"checkThenables": true}
+
+declare function createPromiseLike(): PromiseLike;
+
+createPromiseLike();
+~~~~~~~~~~~~~~~~~~~~ Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the \`void\` operator.
+
+interface MyThenable {
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
+}
+
+declare function createMyThenable(): MyThenable;
+
+createMyThenable();
+~~~~~~~~~~~~~~~~~~~ Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the \`void\` operator.
+"
+`;
+
+exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 4`] = `
+"Correct
+Options: {"checkThenables": true}
+
+declare function createPromiseLike(): PromiseLike;
+
+await createPromiseLike();
+
+interface MyThenable {
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
+}
+
+declare function createMyThenable(): MyThenable;
+
+await createMyThenable();
+"
+`;
+
+exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 5`] = `
"Options: { "ignoreVoid": true }
async function returnsPromise() {
@@ -61,7 +99,7 @@ void Promise.reject('value');
"
`;
-exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 4`] = `
+exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 6`] = `
"Options: { "ignoreIIFE": true }
await (async function () {
@@ -74,7 +112,7 @@ await (async function () {
"
`;
-exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 5`] = `
+exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 7`] = `
"Incorrect
Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}
@@ -91,7 +129,7 @@ returnsPromise();
"
`;
-exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 6`] = `
+exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 8`] = `
"Correct
Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}
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 e6087e512265..7077b27bc39a 100644
--- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts
+++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts
@@ -733,6 +733,41 @@ promise().then(() => {});
},
],
},
+ {
+ code: `
+interface SafePromise extends Promise {
+ brand: 'safe';
+}
+
+declare const createSafePromise: () => SafePromise;
+createSafePromise();
+ `,
+ options: [
+ {
+ allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }],
+ checkThenables: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const createPromise: () => PromiseLike;
+createPromise();
+ `,
+ options: [{ checkThenables: false }],
+ },
+ {
+ code: `
+interface MyThenable {
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
+}
+
+declare function createMyThenable(): MyThenable;
+
+createMyThenable();
+ `,
+ options: [{ checkThenables: false }],
+ },
],
invalid: [
@@ -2181,5 +2216,54 @@ myTag\`abc\`;
options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
errors: [{ line: 4, messageId: 'floatingVoid' }],
},
+ {
+ code: `
+declare const createPromise: () => PromiseLike;
+createPromise();
+ `,
+ errors: [{ line: 3, messageId: 'floatingVoid' }],
+ options: [{ checkThenables: true }],
+ },
+ {
+ code: `
+interface MyThenable {
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
+}
+
+declare function createMyThenable(): MyThenable;
+
+createMyThenable();
+ `,
+ errors: [{ line: 8, messageId: 'floatingVoid' }],
+ options: [{ checkThenables: true }],
+ },
+ {
+ code: `
+declare const createPromise: () => Promise;
+createPromise();
+ `,
+ errors: [{ line: 3, messageId: 'floatingVoid' }],
+ options: [{ checkThenables: false }],
+ },
+ {
+ code: `
+class MyPromise extends Promise {}
+declare const createMyPromise: () => MyPromise;
+createMyPromise();
+ `,
+ errors: [{ line: 4, messageId: 'floatingVoid' }],
+ options: [{ checkThenables: false }],
+ },
+ {
+ code: `
+class MyPromise extends Promise {
+ additional: string;
+}
+declare const createMyPromise: () => MyPromise;
+createMyPromise();
+ `,
+ errors: [{ line: 6, messageId: 'floatingVoid' }],
+ options: [{ checkThenables: false }],
+ },
],
});
diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot
index a708c7001d5b..fc91d05a7b53 100644
--- a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot
+++ b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot
@@ -102,6 +102,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos
},
"type": "array"
},
+ "checkThenables": {
+ "description": "Whether to check all \\"Thenable\\"s, not just the built-in Promise type.",
+ "type": "boolean"
+ },
"ignoreIIFE": {
"description": "Whether to ignore async IIFEs (Immediately Invoked Function Expressions).",
"type": "boolean"
@@ -137,6 +141,8 @@ type Options = [
}
| string
)[];
+ /** Whether to check all "Thenable"s, not just the built-in Promise type. */
+ checkThenables?: boolean;
/** Whether to ignore async IIFEs (Immediately Invoked Function Expressions). */
ignoreIIFE?: boolean;
/** Whether to ignore \`void\` expressions. */