|
1 | 1 | import type { TSESTree } from '@typescript-eslint/utils';
|
2 |
| -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; |
| 2 | +import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; |
| 3 | +import type { AST, RuleFix } from '@typescript-eslint/utils/ts-eslint'; |
3 | 4 | import * as tsutils from 'ts-api-utils';
|
4 | 5 | import type * as ts from 'typescript';
|
5 | 6 |
|
|
8 | 9 | getFunctionHeadLoc,
|
9 | 10 | getFunctionNameWithKind,
|
10 | 11 | getParserServices,
|
| 12 | + isStartOfExpressionStatement, |
| 13 | + needsPrecedingSemicolon, |
| 14 | + nullThrows, |
11 | 15 | upperCaseFirst,
|
12 | 16 | } from '../util';
|
13 | 17 |
|
@@ -37,7 +41,9 @@ export default createRule({
|
37 | 41 | schema: [],
|
38 | 42 | messages: {
|
39 | 43 | missingAwait: "{{name}} has no 'await' expression.",
|
| 44 | + removeAsync: "Remove 'async'.", |
40 | 45 | },
|
| 46 | + hasSuggestions: true, |
41 | 47 | },
|
42 | 48 | defaultOptions: [],
|
43 | 49 | create(context) {
|
@@ -75,13 +81,128 @@ export default createRule({
|
75 | 81 | !isEmptyFunction(node) &&
|
76 | 82 | !(scopeInfo.isGen && scopeInfo.isAsyncYield)
|
77 | 83 | ) {
|
| 84 | + // If the function belongs to a method definition or |
| 85 | + // property, then the function's range may not include the |
| 86 | + // `async` keyword and we should look at the parent instead. |
| 87 | + const nodeWithAsyncKeyword = |
| 88 | + (node.parent.type === AST_NODE_TYPES.MethodDefinition && |
| 89 | + node.parent.value === node) || |
| 90 | + (node.parent.type === AST_NODE_TYPES.Property && |
| 91 | + node.parent.method && |
| 92 | + node.parent.value === node) |
| 93 | + ? node.parent |
| 94 | + : node; |
| 95 | + |
| 96 | + const asyncToken = nullThrows( |
| 97 | + context.sourceCode.getFirstToken( |
| 98 | + nodeWithAsyncKeyword, |
| 99 | + token => token.value === 'async', |
| 100 | + ), |
| 101 | + 'The node is an async function, so it must have an "async" token.', |
| 102 | + ); |
| 103 | + |
| 104 | + const asyncRange: Readonly<AST.Range> = [ |
| 105 | + asyncToken.range[0], |
| 106 | + nullThrows( |
| 107 | + context.sourceCode.getTokenAfter(asyncToken, { |
| 108 | + includeComments: true, |
| 109 | + }), |
| 110 | + 'There will always be a token after the "async" keyword.', |
| 111 | + ).range[0], |
| 112 | + ] as const; |
| 113 | + |
| 114 | + // Removing the `async` keyword can cause parsing errors if the |
| 115 | + // current statement is relying on automatic semicolon insertion. |
| 116 | + // If ASI is currently being used, then we should replace the |
| 117 | + // `async` keyword with a semicolon. |
| 118 | + const nextToken = nullThrows( |
| 119 | + context.sourceCode.getTokenAfter(asyncToken), |
| 120 | + 'There will always be a token after the "async" keyword.', |
| 121 | + ); |
| 122 | + const addSemiColon = |
| 123 | + nextToken.type === AST_TOKEN_TYPES.Punctuator && |
| 124 | + (nextToken.value === '[' || nextToken.value === '(') && |
| 125 | + (nodeWithAsyncKeyword.type === AST_NODE_TYPES.MethodDefinition || |
| 126 | + isStartOfExpressionStatement(nodeWithAsyncKeyword)) && |
| 127 | + needsPrecedingSemicolon(context.sourceCode, nodeWithAsyncKeyword); |
| 128 | + |
| 129 | + const changes = [ |
| 130 | + { range: asyncRange, replacement: addSemiColon ? ';' : undefined }, |
| 131 | + ]; |
| 132 | + |
| 133 | + // If there's a return type annotation and it's a |
| 134 | + // `Promise<T>`, we can also change the return type |
| 135 | + // annotation to just `T` as part of the suggestion. |
| 136 | + // Alternatively, if the function is a generator and |
| 137 | + // the return type annotation is `AsyncGenerator<T>`, |
| 138 | + // then we can change it to `Generator<T>`. |
| 139 | + if ( |
| 140 | + node.returnType?.typeAnnotation.type === |
| 141 | + AST_NODE_TYPES.TSTypeReference |
| 142 | + ) { |
| 143 | + if (scopeInfo.isGen) { |
| 144 | + if (hasTypeName(node.returnType.typeAnnotation, 'AsyncGenerator')) { |
| 145 | + changes.push({ |
| 146 | + range: node.returnType.typeAnnotation.typeName.range, |
| 147 | + replacement: 'Generator', |
| 148 | + }); |
| 149 | + } |
| 150 | + } else if ( |
| 151 | + hasTypeName(node.returnType.typeAnnotation, 'Promise') && |
| 152 | + node.returnType.typeAnnotation.typeArguments != null |
| 153 | + ) { |
| 154 | + const openAngle = nullThrows( |
| 155 | + context.sourceCode.getFirstToken( |
| 156 | + node.returnType.typeAnnotation, |
| 157 | + token => |
| 158 | + token.type === AST_TOKEN_TYPES.Punctuator && |
| 159 | + token.value === '<', |
| 160 | + ), |
| 161 | + 'There are type arguments, so the angle bracket will exist.', |
| 162 | + ); |
| 163 | + const closeAngle = nullThrows( |
| 164 | + context.sourceCode.getLastToken( |
| 165 | + node.returnType.typeAnnotation, |
| 166 | + token => |
| 167 | + token.type === AST_TOKEN_TYPES.Punctuator && |
| 168 | + token.value === '>', |
| 169 | + ), |
| 170 | + 'There are type arguments, so the angle bracket will exist.', |
| 171 | + ); |
| 172 | + changes.push( |
| 173 | + // Remove the closing angled bracket. |
| 174 | + { range: closeAngle.range, replacement: undefined }, |
| 175 | + // Remove the "Promise" identifier |
| 176 | + // and the opening angled bracket. |
| 177 | + { |
| 178 | + range: [ |
| 179 | + node.returnType.typeAnnotation.typeName.range[0], |
| 180 | + openAngle.range[1], |
| 181 | + ], |
| 182 | + replacement: undefined, |
| 183 | + }, |
| 184 | + ); |
| 185 | + } |
| 186 | + } |
| 187 | + |
78 | 188 | context.report({
|
79 | 189 | node,
|
80 | 190 | loc: getFunctionHeadLoc(node, context.sourceCode),
|
81 | 191 | messageId: 'missingAwait',
|
82 | 192 | data: {
|
83 | 193 | name: upperCaseFirst(getFunctionNameWithKind(node)),
|
84 | 194 | },
|
| 195 | + suggest: [ |
| 196 | + { |
| 197 | + messageId: 'removeAsync', |
| 198 | + fix: (fixer): RuleFix[] => |
| 199 | + changes.map(change => |
| 200 | + change.replacement !== undefined |
| 201 | + ? fixer.replaceTextRange(change.range, change.replacement) |
| 202 | + : fixer.removeRange(change.range), |
| 203 | + ), |
| 204 | + }, |
| 205 | + ], |
85 | 206 | });
|
86 | 207 | }
|
87 | 208 |
|
@@ -200,3 +321,13 @@ function expandUnionOrIntersectionType(type: ts.Type): ts.Type[] {
|
200 | 321 | }
|
201 | 322 | return [type];
|
202 | 323 | }
|
| 324 | + |
| 325 | +function hasTypeName( |
| 326 | + typeReference: TSESTree.TSTypeReference, |
| 327 | + typeName: string, |
| 328 | +): boolean { |
| 329 | + return ( |
| 330 | + typeReference.typeName.type === AST_NODE_TYPES.Identifier && |
| 331 | + typeReference.typeName.name === typeName |
| 332 | + ); |
| 333 | +} |
0 commit comments