Skip to content
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

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

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
@@ -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
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();
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
`,
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