Skip to content

feat(eslint-plugin): [no-floating-promises] add suggestion fixer to add an 'await' #5943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Dec 16, 2022
Merged
51 changes: 49 additions & 2 deletions packages/eslint-plugin/src/rules/no-floating-promises.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -12,7 +12,11 @@ type Options = [
},
];

type MessageId = 'floating' | 'floatingVoid' | 'floatingFixVoid';
type MessageId =
| 'floating'
| 'floatingVoid'
| 'floatingFixVoid'
| 'floatingFixAwait';

export default util.createRule<Options, MessageId>({
name: 'no-floating-promises',
Expand All @@ -27,6 +31,7 @@ export default util.createRule<Options, MessageId>({
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.',
Expand Down Expand Up @@ -95,12 +100,54 @@ export default util.createRule<Options, MessageId>({
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;
Expand Down
141 changes: 141 additions & 0 deletions packages/eslint-plugin/tests/rules/no-floating-promises.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
`,
},
],
},
],
},
Expand Down