From 2ec68c260b36c83f192b49f00d435dad5af77d65 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Fri, 3 May 2024 18:04:22 -0600 Subject: [PATCH 1/4] check lots of syntaxes --- .../src/rules/no-floating-promises.ts | 31 ++--- .../tests/rules/no-floating-promises.test.ts | 114 ++++++++++++++---- 2 files changed, 109 insertions(+), 36 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index b9fb9b1cc4dd..b2a7539800e6 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -226,6 +226,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 @@ -257,6 +261,16 @@ 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`. In any case, + // this saves us a bit of type checking, anyway. + return { isUnhandled: false }; + } + if (!isPromiseLike(checker, tsNode)) { return { isUnhandled: false }; } @@ -290,8 +304,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. @@ -300,15 +312,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) { @@ -317,10 +320,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 }; } }, }); 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 bd02ed6d5a87..7b54a56f41f1 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( () => {}, @@ -516,6 +513,29 @@ declare const myTag: (strings: TemplateStringsArray) => string; myTag\`abc\`; `, }, + { + code: ` +declare let x: any; +declare const promiseArray: Array>; +x = promiseArray; + `, + }, + { + code: ` +declare const promiseArray: Array>; +async function f() { + return promiseArray; +} + `, + }, + { + code: ` +declare const promiseArray: Array>; +async function* generator() { + yield promiseArray; +} + `, + }, ], invalid: [ @@ -972,9 +992,9 @@ async function test() { }, { code: ` -async function test() { - declare const promiseValue: Promise; +declare const promiseValue: Promise; +async function test() { promiseValue; promiseValue.then(() => {}); promiseValue.catch(); @@ -1002,9 +1022,9 @@ async function test() { }, { code: ` -async function test() { - declare const promiseUnion: Promise | number; +declare const promiseUnion: Promise | number; +async function test() { promiseUnion; } `, @@ -1017,9 +1037,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(); @@ -1227,13 +1247,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: [ @@ -1762,6 +1782,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' }], @@ -1855,5 +1885,47 @@ cursed(); `, errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }], }, + { + 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: ` +3 as Promise; + `, + errors: [{ messageId: 'floatingVoid', line: 2 }], + }, + { + code: ` +3 as Promise & number; + `, + errors: [{ messageId: 'floatingVoid', line: 2 }], + }, + { + code: ` +3 as Promise & { yolo?: string }; + `, + errors: [{ messageId: 'floatingVoid', line: 2 }], + }, + { + code: ` +>3; + `, + errors: [{ messageId: 'floatingVoid', line: 2 }], + }, ], }); From 3dd7f670f9eb349327d8c33d4e37722514476cf3 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Fri, 3 May 2024 18:12:33 -0600 Subject: [PATCH 2/4] fixup --- .../tests/rules/no-floating-promises.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 7b54a56f41f1..f4f73827522d 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -532,7 +532,14 @@ async function f() { code: ` declare const promiseArray: Array>; async function* generator() { - yield promiseArray; + yield* promiseArray; +} + `, + }, + { + code: ` +async function* generator() { + yield Promise.resolve(); } `, }, From 79fa0f663fc84b0b79a61929287949fbba79d5b7 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Fri, 3 May 2024 18:16:57 -0600 Subject: [PATCH 3/4] more test --- .../eslint-plugin/tests/rules/no-floating-promises.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 f4f73827522d..d1273cb30e98 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -522,6 +522,12 @@ x = promiseArray; }, { code: ` +declare let x: Promise; +x = Promise.resolve(2); + `, + }, + { + code: ` declare const promiseArray: Array>; async function f() { return promiseArray; From 53e7d0e96ed13ea914684447830d458cd9743a4f Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Thu, 20 Jun 2024 18:24:16 -0600 Subject: [PATCH 4/4] pr feedback --- .../eslint-plugin/src/rules/no-floating-promises.ts | 4 ++-- .../tests/rules/no-floating-promises.test.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 01abb6628618..f4013ca8f78d 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -276,8 +276,7 @@ export default createRule({ // 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`. In any case, - // this saves us a bit of type checking, anyway. + // as `Promise & number` instead of `number`. return { isUnhandled: false }; } @@ -333,6 +332,7 @@ export default createRule({ // Anything else is unhandled. return { isUnhandled: true }; } + function isPromiseArray(node: ts.Node): boolean { const type = checker.getTypeAtLocation(node); for (const ty of tsutils 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 3bfcfd28def8..9284f2d23b55 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -2244,25 +2244,26 @@ function* generator(): Generator { }, { code: ` -3 as Promise; +const value = {}; +value as Promise; `, - errors: [{ messageId: 'floatingVoid', line: 2 }], + errors: [{ messageId: 'floatingVoid', line: 3 }], }, { code: ` -3 as Promise & number; +({}) as Promise & number; `, errors: [{ messageId: 'floatingVoid', line: 2 }], }, { code: ` -3 as Promise & { yolo?: string }; +({}) as Promise & { yolo?: string }; `, errors: [{ messageId: 'floatingVoid', line: 2 }], }, { code: ` ->3; +>{}; `, errors: [{ messageId: 'floatingVoid', line: 2 }], },