diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 1ae5e602ae0e..f4013ca8f78d 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -236,6 +236,10 @@ export default createRule({ nonFunctionHandler?: boolean; promiseArray?: boolean; } { + if (node.type === AST_NODE_TYPES.AssignmentExpression) { + return { isUnhandled: false }; + } + // First, check expressions whose resulting types may not be promise-like if (node.type === AST_NODE_TYPES.SequenceExpression) { // Any child in a comma expression could return a potentially unhandled @@ -267,6 +271,15 @@ export default createRule({ return { isUnhandled: true, promiseArray: true }; } + // await expression addresses promises, but not promise arrays. + if (node.type === AST_NODE_TYPES.AwaitExpression) { + // you would think this wouldn't be strictly necessary, since we're + // anyway checking the type of the expression, but, unfortunately TS + // reports the result of `await (promise as Promise & number)` + // as `Promise & number` instead of `number`. + return { isUnhandled: false }; + } + if (!isPromiseLike(tsNode)) { return { isUnhandled: false }; } @@ -300,8 +313,6 @@ export default createRule({ // All other cases are unhandled. return { isUnhandled: true }; - } else if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) { - 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 // ternary. Check them directly. @@ -310,15 +321,6 @@ export default createRule({ return alternateResult; } return isUnhandledPromise(checker, node.consequent); - } else if ( - node.type === AST_NODE_TYPES.MemberExpression || - node.type === AST_NODE_TYPES.Identifier || - node.type === AST_NODE_TYPES.NewExpression - ) { - // If it is just a property access chain or a `new` call (e.g. `foo.bar` or - // `new Promise()`), the promise is not handled because it doesn't have the - // necessary then/catch call at the end of the chain. - return { isUnhandled: true }; } else if (node.type === AST_NODE_TYPES.LogicalExpression) { const leftResult = isUnhandledPromise(checker, node.left); if (leftResult.isUnhandled) { @@ -327,10 +329,8 @@ export default createRule({ return isUnhandledPromise(checker, node.right); } - // We conservatively return false for all other types of expressions because - // we don't want to accidentally fail if the promise is handled internally but - // we just can't tell. - return { isUnhandled: false }; + // Anything else is unhandled. + return { isUnhandled: true }; } function isPromiseArray(node: ts.Node): boolean { 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..9284f2d23b55 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -140,9 +140,8 @@ async function test() { } `, ` +declare const promiseValue: Promise; async function test() { - declare const promiseValue: Promise; - await promiseValue; promiseValue.then( () => {}, @@ -158,9 +157,8 @@ async function test() { } `, ` +declare const promiseUnion: Promise | number; async function test() { - declare const promiseUnion: Promise | number; - await promiseUnion; promiseUnion.then( () => {}, @@ -177,9 +175,8 @@ async function test() { } `, ` +declare const promiseIntersection: Promise & number; async function test() { - declare const promiseIntersection: Promise & number; - await promiseIntersection; promiseIntersection.then( () => {}, @@ -210,12 +207,12 @@ async function test() { } `, ` +declare const intersectionPromise: Promise & number; async function test() { await (Math.random() > 0.5 ? numberPromise : 0); await (Math.random() > 0.5 ? foo : 0); await (Math.random() > 0.5 ? bar : 0); - declare const intersectionPromise: Promise & number; await intersectionPromise; } `, @@ -308,8 +305,8 @@ async function test() { // optional chaining ` +declare const returnsPromise: () => Promise | null; async function test() { - declare const returnsPromise: () => Promise | null; await returnsPromise?.(); returnsPromise()?.then( () => {}, @@ -712,6 +709,42 @@ myTag\`abc\`; }, { code: ` +declare let x: any; +declare const promiseArray: Array>; +x = promiseArray; + `, + }, + { + code: ` +declare let x: Promise; +x = Promise.resolve(2); + `, + }, + { + code: ` +declare const promiseArray: Array>; +async function f() { + return promiseArray; +} + `, + }, + { + code: ` +declare const promiseArray: Array>; +async function* generator() { + yield* promiseArray; +} + `, + }, + { + code: ` +async function* generator() { + yield Promise.resolve(); +} + `, + }, + { + code: ` interface SafeThenable { then( onfulfilled?: @@ -1189,9 +1222,9 @@ async function test() { }, { code: ` -async function test() { - declare const promiseValue: Promise; +declare const promiseValue: Promise; +async function test() { promiseValue; promiseValue.then(() => {}); promiseValue.catch(); @@ -1219,9 +1252,9 @@ async function test() { }, { code: ` -async function test() { - declare const promiseUnion: Promise | number; +declare const promiseUnion: Promise | number; +async function test() { promiseUnion; } `, @@ -1234,9 +1267,9 @@ async function test() { }, { code: ` -async function test() { - declare const promiseIntersection: Promise & number; +declare const promiseIntersection: Promise & number; +async function test() { promiseIntersection; promiseIntersection.then(() => {}); promiseIntersection.catch(); @@ -1444,13 +1477,13 @@ async function test() { }, { code: ` - (async function () { - declare const promiseIntersection: Promise & number; - promiseIntersection; - promiseIntersection.then(() => {}); - promiseIntersection.catch(); - promiseIntersection.finally(); - })(); +declare const promiseIntersection: Promise & number; +(async function () { + promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); + promiseIntersection.finally(); +})(); `, options: [{ ignoreIIFE: true }], errors: [ @@ -1979,6 +2012,16 @@ void promiseArray; }, { code: ` +declare const promiseArray: Array>; +async function f() { + await promiseArray; +} + `, + options: [{ ignoreVoid: false }], + errors: [{ line: 4, messageId: 'floatingPromiseArray' }], + }, + { + code: ` [1, 2, Promise.reject(), 3]; `, errors: [{ line: 2, messageId: 'floatingPromiseArrayVoid' }], @@ -2181,5 +2224,48 @@ myTag\`abc\`; options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], errors: [{ line: 4, messageId: 'floatingVoid' }], }, + { + code: ` +declare const x: any; +function* generator(): Generator> { + yield x; +} + `, + errors: [{ messageId: 'floatingVoid' }], + }, + { + code: ` +declare const x: Generator, void>; +function* generator(): Generator { + yield* x; +} + `, + errors: [{ messageId: 'floatingVoid' }], + }, + { + code: ` +const value = {}; +value as Promise; + `, + errors: [{ messageId: 'floatingVoid', line: 3 }], + }, + { + code: ` +({}) as Promise & number; + `, + errors: [{ messageId: 'floatingVoid', line: 2 }], + }, + { + code: ` +({}) as Promise & { yolo?: string }; + `, + errors: [{ messageId: 'floatingVoid', line: 2 }], + }, + { + code: ` +>{}; + `, + errors: [{ messageId: 'floatingVoid', line: 2 }], + }, ], });