Skip to content

Commit 717e718

Browse files
authored
feat(eslint-plugin): [prom-func-async] add automatic fix (#2845)
1 parent 1ef0d64 commit 717e718

File tree

3 files changed

+191
-9
lines changed

3 files changed

+191
-9
lines changed

Diff for: packages/eslint-plugin/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
164164
| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :heavy_check_mark: | | :thought_balloon: |
165165
| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | | :wrench: | :thought_balloon: |
166166
| [`@typescript-eslint/prefer-ts-expect-error`](./docs/rules/prefer-ts-expect-error.md) | Recommends using `@ts-expect-error` over `@ts-ignore` | | :wrench: | |
167-
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: |
167+
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | :wrench: | :thought_balloon: |
168168
| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Requires `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: |
169169
| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | :heavy_check_mark: | | :thought_balloon: |
170170
| [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | :heavy_check_mark: | | :thought_balloon: |

Diff for: packages/eslint-plugin/src/rules/promise-function-async.ts

+36-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
AST_NODE_TYPES,
33
TSESTree,
44
} from '@typescript-eslint/experimental-utils';
5+
import * as ts from 'typescript';
56
import * as util from '../util';
67

78
type Options = [
@@ -20,6 +21,7 @@ export default util.createRule<Options, MessageIds>({
2021
name: 'promise-function-async',
2122
meta: {
2223
type: 'suggestion',
24+
fixable: 'code',
2325
docs: {
2426
description:
2527
'Requires any function or method that returns a Promise to be marked async',
@@ -94,9 +96,7 @@ export default util.createRule<Options, MessageIds>({
9496
node:
9597
| TSESTree.ArrowFunctionExpression
9698
| TSESTree.FunctionDeclaration
97-
| TSESTree.FunctionExpression
98-
| TSESTree.MethodDefinition
99-
| TSESTree.TSAbstractMethodDefinition,
99+
| TSESTree.FunctionExpression,
100100
): void {
101101
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
102102
const signatures = checker
@@ -114,6 +114,12 @@ export default util.createRule<Options, MessageIds>({
114114
allAllowedPromiseNames,
115115
)
116116
) {
117+
// Return type is not a promise
118+
return;
119+
}
120+
121+
if (node.parent?.type === AST_NODE_TYPES.TSAbstractMethodDefinition) {
122+
// Abstract method can't be async
117123
return;
118124
}
119125

@@ -123,12 +129,34 @@ export default util.createRule<Options, MessageIds>({
123129
node.parent.type === AST_NODE_TYPES.MethodDefinition) &&
124130
(node.parent.kind === 'get' || node.parent.kind === 'set')
125131
) {
132+
// Getters and setters can't be async
126133
return;
127134
}
128135

136+
if (
137+
util.isTypeFlagSet(returnType, ts.TypeFlags.Any | ts.TypeFlags.Unknown)
138+
) {
139+
// Report without auto fixer because the return type is unknown
140+
return context.report({
141+
messageId: 'missingAsync',
142+
node,
143+
});
144+
}
145+
129146
context.report({
130147
messageId: 'missingAsync',
131148
node,
149+
fix: fixer => {
150+
if (
151+
node.parent &&
152+
(node.parent.type === AST_NODE_TYPES.MethodDefinition ||
153+
(node.parent.type === AST_NODE_TYPES.Property &&
154+
node.parent.method))
155+
) {
156+
return fixer.insertTextBefore(node.parent.key, 'async ');
157+
}
158+
return fixer.insertTextBefore(node, 'async ');
159+
},
132160
});
133161
}
134162

@@ -152,13 +180,15 @@ export default util.createRule<Options, MessageIds>({
152180
): void {
153181
if (
154182
node.parent &&
155-
'kind' in node.parent &&
183+
node.parent.type === AST_NODE_TYPES.MethodDefinition &&
156184
node.parent.kind === 'method'
157185
) {
158186
if (checkMethodDeclarations) {
159-
validateNode(node.parent);
187+
validateNode(node);
160188
}
161-
} else if (checkFunctionExpressions) {
189+
return;
190+
}
191+
if (checkFunctionExpressions) {
162192
validateNode(node);
163193
}
164194
},

Diff for: packages/eslint-plugin/tests/rules/promise-function-async.test.ts

+154-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import rule from '../../src/rules/promise-function-async';
2-
import { RuleTester, getFixturesRootDir } from '../RuleTester';
2+
import { getFixturesRootDir, RuleTester } from '../RuleTester';
33

44
const rootDir = getFixturesRootDir();
55
const messageId = 'missingAsync';
@@ -141,6 +141,18 @@ const foo = (options: Options): Return => {
141141
code: `
142142
function foo(): Promise<string> | boolean {
143143
return Math.random() > 0.5 ? Promise.resolve('value') : false;
144+
}
145+
`,
146+
},
147+
{
148+
code: `
149+
abstract class Test {
150+
abstract test1(): Promise<number>;
151+
152+
// abstract method with body is always an error but it still parses into valid AST
153+
abstract test2(): Promise<number> {
154+
return Promise.resolve(1);
155+
}
144156
}
145157
`,
146158
},
@@ -191,6 +203,11 @@ const nonAsyncPromiseFunctionExpressionA = function (p: Promise<void>) {
191203
messageId,
192204
},
193205
],
206+
output: `
207+
const nonAsyncPromiseFunctionExpressionA = async function (p: Promise<void>) {
208+
return p;
209+
};
210+
`,
194211
},
195212
{
196213
code: `
@@ -203,6 +220,11 @@ const nonAsyncPromiseFunctionExpressionB = function () {
203220
messageId,
204221
},
205222
],
223+
output: `
224+
const nonAsyncPromiseFunctionExpressionB = async function () {
225+
return new Promise<void>();
226+
};
227+
`,
206228
},
207229
{
208230
code: `
@@ -215,6 +237,11 @@ function nonAsyncPromiseFunctionDeclarationA(p: Promise<void>) {
215237
messageId,
216238
},
217239
],
240+
output: `
241+
async function nonAsyncPromiseFunctionDeclarationA(p: Promise<void>) {
242+
return p;
243+
}
244+
`,
218245
},
219246
{
220247
code: `
@@ -227,6 +254,11 @@ function nonAsyncPromiseFunctionDeclarationB() {
227254
messageId,
228255
},
229256
],
257+
output: `
258+
async function nonAsyncPromiseFunctionDeclarationB() {
259+
return new Promise<void>();
260+
}
261+
`,
230262
},
231263
{
232264
code: `
@@ -237,6 +269,9 @@ const nonAsyncPromiseArrowFunctionA = (p: Promise<void>) => p;
237269
messageId,
238270
},
239271
],
272+
output: `
273+
const nonAsyncPromiseArrowFunctionA = async (p: Promise<void>) => p;
274+
`,
240275
},
241276
{
242277
code: `
@@ -247,6 +282,31 @@ const nonAsyncPromiseArrowFunctionB = () => new Promise<void>();
247282
messageId,
248283
},
249284
],
285+
output: `
286+
const nonAsyncPromiseArrowFunctionB = async () => new Promise<void>();
287+
`,
288+
},
289+
{
290+
code: `
291+
const functions = {
292+
nonAsyncPromiseMethod() {
293+
return Promise.resolve(1);
294+
},
295+
};
296+
`,
297+
errors: [
298+
{
299+
line: 3,
300+
messageId,
301+
},
302+
],
303+
output: `
304+
const functions = {
305+
async nonAsyncPromiseMethod() {
306+
return Promise.resolve(1);
307+
},
308+
};
309+
`,
250310
},
251311
{
252312
code: `
@@ -255,7 +315,7 @@ class Test {
255315
return p;
256316
}
257317
258-
public nonAsyncPromiseMethodB() {
318+
public static nonAsyncPromiseMethodB() {
259319
return new Promise<void>();
260320
}
261321
}
@@ -270,6 +330,17 @@ class Test {
270330
messageId,
271331
},
272332
],
333+
output: `
334+
class Test {
335+
public async nonAsyncPromiseMethodA(p: Promise<void>) {
336+
return p;
337+
}
338+
339+
public static async nonAsyncPromiseMethodB() {
340+
return new Promise<void>();
341+
}
342+
}
343+
`,
273344
},
274345
{
275346
code: `
@@ -308,6 +379,23 @@ class Test {
308379
messageId,
309380
},
310381
],
382+
output: `
383+
const nonAsyncPromiseFunctionExpression = async function (p: Promise<void>) {
384+
return p;
385+
};
386+
387+
async function nonAsyncPromiseFunctionDeclaration(p: Promise<void>) {
388+
return p;
389+
}
390+
391+
const nonAsyncPromiseArrowFunction = (p: Promise<void>) => p;
392+
393+
class Test {
394+
public async nonAsyncPromiseMethod(p: Promise<void>) {
395+
return p;
396+
}
397+
}
398+
`,
311399
},
312400
{
313401
code: `
@@ -346,6 +434,23 @@ class Test {
346434
messageId,
347435
},
348436
],
437+
output: `
438+
const nonAsyncPromiseFunctionExpression = async function (p: Promise<void>) {
439+
return p;
440+
};
441+
442+
function nonAsyncPromiseFunctionDeclaration(p: Promise<void>) {
443+
return p;
444+
}
445+
446+
const nonAsyncPromiseArrowFunction = async (p: Promise<void>) => p;
447+
448+
class Test {
449+
public async nonAsyncPromiseMethod(p: Promise<void>) {
450+
return p;
451+
}
452+
}
453+
`,
349454
},
350455
{
351456
code: `
@@ -384,6 +489,23 @@ class Test {
384489
messageId,
385490
},
386491
],
492+
output: `
493+
const nonAsyncPromiseFunctionExpression = function (p: Promise<void>) {
494+
return p;
495+
};
496+
497+
async function nonAsyncPromiseFunctionDeclaration(p: Promise<void>) {
498+
return p;
499+
}
500+
501+
const nonAsyncPromiseArrowFunction = async (p: Promise<void>) => p;
502+
503+
class Test {
504+
public async nonAsyncPromiseMethod(p: Promise<void>) {
505+
return p;
506+
}
507+
}
508+
`,
387509
},
388510
{
389511
code: `
@@ -422,6 +544,23 @@ class Test {
422544
messageId,
423545
},
424546
],
547+
output: `
548+
const nonAsyncPromiseFunctionExpression = async function (p: Promise<void>) {
549+
return p;
550+
};
551+
552+
async function nonAsyncPromiseFunctionDeclaration(p: Promise<void>) {
553+
return p;
554+
}
555+
556+
const nonAsyncPromiseArrowFunction = async (p: Promise<void>) => p;
557+
558+
class Test {
559+
public nonAsyncPromiseMethod(p: Promise<void>) {
560+
return p;
561+
}
562+
}
563+
`,
425564
},
426565
{
427566
code: `
@@ -440,6 +579,11 @@ const returnAllowedType = () => new PromiseType();
440579
messageId,
441580
},
442581
],
582+
output: `
583+
class PromiseType {}
584+
585+
const returnAllowedType = async () => new PromiseType();
586+
`,
443587
},
444588
{
445589
code: `
@@ -461,6 +605,14 @@ function foo(): Promise<string> | SPromise<boolean> {
461605
messageId,
462606
},
463607
],
608+
output: `
609+
interface SPromise<T> extends Promise<T> {}
610+
async function foo(): Promise<string> | SPromise<boolean> {
611+
return Math.random() > 0.5
612+
? Promise.resolve('value')
613+
: Promise.resolve(false);
614+
}
615+
`,
464616
},
465617
],
466618
});

0 commit comments

Comments
 (0)