From 7c3f94e88bfa371db0430e4eae1986cde1f6d531 Mon Sep 17 00:00:00 2001 From: islandryu Date: Fri, 20 Jan 2023 09:26:52 +0900 Subject: [PATCH 1/3] feat(eslint-plugin) [no-floating-promises] Error on logical expression --- .../src/rules/no-floating-promises.ts | 36 +- .../tests/rules/no-floating-promises.test.ts | 310 ++++++++++++++++++ 2 files changed, 335 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 61829743e81b..57454ae409fd 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -4,6 +4,7 @@ import * as tsutils from 'tsutils'; import * as ts from 'typescript'; import * as util from '../util'; +import { OperatorPrecedence } from '../util'; type Options = [ { @@ -88,10 +89,21 @@ export default util.createRule({ suggest: [ { messageId: 'floatingFixVoid', - fix(fixer): TSESLint.RuleFix { - let code = sourceCode.getText(node); - code = `void ${code}`; - return fixer.replaceText(node, code); + fix(fixer): TSESLint.RuleFix | TSESLint.RuleFix[] { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + node.expression, + ); + if (isHigherPrecedenceThanUnary(tsNode)) { + return fixer.insertTextBefore(node, 'void '); + } else { + return [ + fixer.insertTextBefore(node, 'void ('), + fixer.insertTextAfterRange( + [expression.range[1], expression.range[1]], + ')', + ), + ]; + } }, }, ], @@ -116,7 +128,7 @@ export default util.createRule({ const tsNode = parserServices.esTreeNodeToTSNodeMap.get( node.expression, ); - if (isHigherPrecedenceThanAwait(tsNode)) { + if (isHigherPrecedenceThanUnary(tsNode)) { return fixer.insertTextBefore(node, 'await '); } else { return [ @@ -136,16 +148,13 @@ export default util.createRule({ }, }; - function isHigherPrecedenceThanAwait(node: ts.Node): boolean { + function isHigherPrecedenceThanUnary(node: ts.Node): boolean { const operator = tsutils.isBinaryExpression(node) ? node.operatorToken.kind : ts.SyntaxKind.Unknown; const nodePrecedence = util.getOperatorPrecedence(node.kind, operator); - const awaitPrecedence = util.getOperatorPrecedence( - ts.SyntaxKind.AwaitExpression, - ts.SyntaxKind.Unknown, - ); - return nodePrecedence > awaitPrecedence; + const unaryPrecedence = OperatorPrecedence.Unary; + return nodePrecedence > unaryPrecedence; } function isAsyncIife(node: TSESTree.ExpressionStatement): boolean { @@ -214,6 +223,11 @@ export default util.createRule({ // `new Promise()`), the promise is not handled because it doesn't have the // necessary then/catch call at the end of the chain. return true; + } else if (node.type === AST_NODE_TYPES.LogicalExpression) { + return ( + isUnhandledPromise(checker, node.left) || + isUnhandledPromise(checker, node.right) + ); } // We conservatively return false for all other types of expressions because 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 a80acab989ae..070cef91e9a6 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -405,6 +405,57 @@ void doSomething(); `, options: [{ ignoreIIFE: true }], }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + void (condition && myPromise()); +} + `, + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + await (condition && myPromise()); +} + `, + options: [{ ignoreVoid: false }], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + condition && void myPromise(); +} + `, + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + condition && (await myPromise()); +} + `, + options: [{ ignoreVoid: false }], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + let condition = false; + condition && myPromise(); + condition = true; + condition || myPromise(); + condition ?? myPromise(); +} + `, + options: [{ ignoreVoid: false }], + }, ], invalid: [ @@ -1117,5 +1168,264 @@ async function test() { }, ], }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + + void condition || myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + + void (void condition || myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + + (await condition) && myPromise(); +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 6, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + + await ((await condition) && myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + + condition && myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + + void (condition && myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + condition || myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + void (condition || myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = null; + + condition ?? myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = null; + + void (condition ?? myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = true; + condition && myPromise; +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = true; + await (condition && myPromise); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = false; + condition || myPromise; +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = false; + await (condition || myPromise); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = null; + condition ?? myPromise; +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = null; + await (condition ?? myPromise); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + condition || condition || myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + void (condition || condition || myPromise()); +} + `, + }, + ], + }, + ], + }, ], }); From 899966b56a15f1da5e3a23329bc26b4e54979556 Mon Sep 17 00:00:00 2001 From: SHIMA RYUHEI <65934663+islandryu@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:58:50 +0900 Subject: [PATCH 2/3] Update packages/eslint-plugin/src/rules/no-floating-promises.ts Co-authored-by: Josh Goldberg --- packages/eslint-plugin/src/rules/no-floating-promises.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 57454ae409fd..0cab5b16e585 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -153,8 +153,7 @@ export default util.createRule({ ? node.operatorToken.kind : ts.SyntaxKind.Unknown; const nodePrecedence = util.getOperatorPrecedence(node.kind, operator); - const unaryPrecedence = OperatorPrecedence.Unary; - return nodePrecedence > unaryPrecedence; + return nodePrecedence > OperatorPrecedence.Unary; } function isAsyncIife(node: TSESTree.ExpressionStatement): boolean { From ab1bf4c1e32281f5695283536c5d19efdfd4a282 Mon Sep 17 00:00:00 2001 From: islandryu Date: Fri, 3 Feb 2023 23:06:44 +0900 Subject: [PATCH 3/3] remove sourceCode Variable --- packages/eslint-plugin/src/rules/no-floating-promises.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 0cab5b16e585..c4ce3db8e1cc 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -67,7 +67,6 @@ export default util.createRule({ create(context, [options]) { const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); - const sourceCode = context.getSourceCode(); return { ExpressionStatement(node): void {