From 9e35ef9af3ec51ab2dd49336699f3a94528bb4b1 Mon Sep 17 00:00:00 2001 From: kj <34977512+sena-anny@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:17:36 +0900 Subject: [PATCH] feat(eslint-plugin): [no-floating-promises] add suggestion fixer to add an 'await' (#5943) --- .../src/rules/no-floating-promises.ts | 51 ++++++- .../tests/rules/no-floating-promises.test.ts | 141 ++++++++++++++++++ 2 files changed, 190 insertions(+), 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 05f0954e305..61829743e81 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -1,7 +1,7 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'tsutils'; -import type * as ts from 'typescript'; +import * as ts from 'typescript'; import * as util from '../util'; @@ -12,7 +12,11 @@ type Options = [ }, ]; -type MessageId = 'floating' | 'floatingVoid' | 'floatingFixVoid'; +type MessageId = + | 'floating' + | 'floatingVoid' + | 'floatingFixVoid' + | 'floatingFixAwait'; export default util.createRule({ name: 'no-floating-promises', @@ -27,6 +31,7 @@ export default util.createRule({ messages: { floating: 'Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.', + floatingFixAwait: 'Add await operator.', floatingVoid: '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.', @@ -95,12 +100,54 @@ export default util.createRule({ context.report({ node, messageId: 'floating', + suggest: [ + { + messageId: 'floatingFixAwait', + fix(fixer): TSESLint.RuleFix | TSESLint.RuleFix[] { + if ( + expression.type === AST_NODE_TYPES.UnaryExpression && + expression.operator === 'void' + ) { + return fixer.replaceTextRange( + [expression.range[0], expression.range[0] + 4], + 'await', + ); + } + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + node.expression, + ); + if (isHigherPrecedenceThanAwait(tsNode)) { + return fixer.insertTextBefore(node, 'await '); + } else { + return [ + fixer.insertTextBefore(node, 'await ('), + fixer.insertTextAfterRange( + [expression.range[1], expression.range[1]], + ')', + ), + ]; + } + }, + }, + ], }); } } }, }; + function isHigherPrecedenceThanAwait(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; + } + function isAsyncIife(node: TSESTree.ExpressionStatement): boolean { if (node.expression.type !== AST_NODE_TYPES.CallExpression) { return false; 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 17ec0bb6198..a80acab989a 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -656,6 +656,147 @@ async function test() { { line: 3, messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function test() { + await Promise.resolve(); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function test() { + const promise = new Promise((resolve, reject) => resolve('value')); + promise; +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 4, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function test() { + const promise = new Promise((resolve, reject) => resolve('value')); + await promise; +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function returnsPromise() { + return 'value'; +} +void returnsPromise(); + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function returnsPromise() { + return 'value'; +} +await returnsPromise(); + `, + }, + ], + }, + ], + }, + { + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + code: ` +async function returnsPromise() { + return 'value'; +} +void /* ... */ returnsPromise(); + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function returnsPromise() { + return 'value'; +} +await /* ... */ returnsPromise(); + `, + }, + ], + }, + ], + }, + { + code: ` +async function returnsPromise() { + return 'value'; +} +1, returnsPromise(); + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function returnsPromise() { + return 'value'; +} +await (1, returnsPromise()); + `, + }, + ], + }, + ], + }, + { + code: ` +async function returnsPromise() { + return 'value'; +} +bool ? returnsPromise() : null; + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function returnsPromise() { + return 'value'; +} +await (bool ? returnsPromise() : null); + `, + }, + ], }, ], },