1
1
import {
2
- TSESTree ,
3
2
AST_NODE_TYPES ,
3
+ TSESLint ,
4
+ TSESTree ,
4
5
} from '@typescript-eslint/experimental-utils' ;
5
- import baseRule from 'eslint/lib/rules/require-await' ;
6
+ import {
7
+ isArrowToken ,
8
+ getFunctionNameWithKind ,
9
+ isOpeningParenToken ,
10
+ } from 'eslint-utils' ;
6
11
import * as tsutils from 'tsutils' ;
7
12
import * as ts from 'typescript' ;
8
13
import * as util from '../util' ;
9
14
10
- type Options = util . InferOptionsTypeFromRule < typeof baseRule > ;
11
- type MessageIds = util . InferMessageIdsTypeFromRule < typeof baseRule > ;
15
+ interface ScopeInfo {
16
+ upper : ScopeInfo | null ;
17
+ hasAwait : boolean ;
18
+ hasAsync : boolean ;
19
+ }
20
+ type FunctionNode =
21
+ | TSESTree . FunctionDeclaration
22
+ | TSESTree . FunctionExpression
23
+ | TSESTree . ArrowFunctionExpression ;
12
24
13
- export default util . createRule < Options , MessageIds > ( {
25
+ export default util . createRule ( {
14
26
name : 'require-await' ,
15
27
meta : {
16
28
type : 'suggestion' ,
@@ -21,58 +33,172 @@ export default util.createRule<Options, MessageIds>({
21
33
requiresTypeChecking : true ,
22
34
extendsBaseRule : true ,
23
35
} ,
24
- schema : baseRule . meta . schema ,
25
- messages : baseRule . meta . messages ,
36
+ schema : [ ] ,
37
+ messages : {
38
+ missingAwait : "{{name}} has no 'await' expression." ,
39
+ } ,
26
40
} ,
27
41
defaultOptions : [ ] ,
28
42
create ( context ) {
29
- const rules = baseRule . create ( context ) ;
30
43
const parserServices = util . getParserServices ( context ) ;
31
44
const checker = parserServices . program . getTypeChecker ( ) ;
32
45
46
+ const sourceCode = context . getSourceCode ( ) ;
47
+ let scopeInfo : ScopeInfo | null = null ;
48
+
49
+ /**
50
+ * Push the scope info object to the stack.
51
+ */
52
+ function enterFunction ( node : FunctionNode ) : void {
53
+ scopeInfo = {
54
+ upper : scopeInfo ,
55
+ hasAwait : false ,
56
+ hasAsync : node . async ,
57
+ } ;
58
+ }
59
+
60
+ /**
61
+ * Pop the top scope info object from the stack.
62
+ * Also, it reports the function if needed.
63
+ */
64
+ function exitFunction ( node : FunctionNode ) : void {
65
+ /* istanbul ignore if */ if ( ! scopeInfo ) {
66
+ // this shouldn't ever happen, as we have to exit a function after we enter it
67
+ return ;
68
+ }
69
+
70
+ if ( node . async && ! scopeInfo . hasAwait && ! isEmptyFunction ( node ) ) {
71
+ context . report ( {
72
+ node,
73
+ loc : getFunctionHeadLoc ( node , sourceCode ) ,
74
+ messageId : 'missingAwait' ,
75
+ data : {
76
+ name : util . upperCaseFirst ( getFunctionNameWithKind ( node ) ) ,
77
+ } ,
78
+ } ) ;
79
+ }
80
+
81
+ scopeInfo = scopeInfo . upper ;
82
+ }
83
+
33
84
/**
34
85
* Checks if the node returns a thenable type
35
- *
36
- * @param {ASTNode } node - The node to check
37
- * @returns {boolean }
38
86
*/
39
87
function isThenableType ( node : ts . Node ) : boolean {
40
88
const type = checker . getTypeAtLocation ( node ) ;
41
89
42
90
return tsutils . isThenableType ( checker , node , type ) ;
43
91
}
44
92
93
+ /**
94
+ * Marks the current scope as having an await
95
+ */
96
+ function markAsHasAwait ( ) : void {
97
+ if ( ! scopeInfo ) {
98
+ return ;
99
+ }
100
+
101
+ scopeInfo . hasAwait = true ;
102
+ }
103
+
45
104
return {
46
- FunctionDeclaration : rules . FunctionDeclaration ,
47
- FunctionExpression : rules . FunctionExpression ,
48
- ArrowFunctionExpression : rules . ArrowFunctionExpression ,
49
- 'ArrowFunctionExpression[async = true]' (
50
- node : TSESTree . ArrowFunctionExpression ,
105
+ FunctionDeclaration : enterFunction ,
106
+ FunctionExpression : enterFunction ,
107
+ ArrowFunctionExpression : enterFunction ,
108
+ 'FunctionDeclaration:exit' : exitFunction ,
109
+ 'FunctionExpression:exit' : exitFunction ,
110
+ 'ArrowFunctionExpression:exit' : exitFunction ,
111
+
112
+ AwaitExpression : markAsHasAwait ,
113
+ 'ForOfStatement[await = true]' : markAsHasAwait ,
114
+
115
+ // check body-less async arrow function.
116
+ // ignore `async () => await foo` because it's obviously correct
117
+ 'ArrowFunctionExpression[async = true] > :not(BlockStatement, AwaitExpression)' (
118
+ node : Exclude <
119
+ TSESTree . Node ,
120
+ TSESTree . BlockStatement | TSESTree . AwaitExpression
121
+ > ,
51
122
) : void {
52
- // If body type is not BlockStatement, we need to check the return type here
53
- if ( node . body . type !== AST_NODE_TYPES . BlockStatement ) {
54
- const expression = parserServices . esTreeNodeToTSNodeMap . get (
55
- node . body ,
56
- ) ;
57
- if ( expression && isThenableType ( expression ) ) {
58
- // tell the base rule to mark the scope as having an await so it ignores it
59
- rules . AwaitExpression ( ) ;
60
- }
123
+ const expression = parserServices . esTreeNodeToTSNodeMap . get ( node ) ;
124
+ if ( expression && isThenableType ( expression ) ) {
125
+ markAsHasAwait ( ) ;
61
126
}
62
127
} ,
63
- 'FunctionDeclaration:exit' : rules [ 'FunctionDeclaration:exit' ] ,
64
- 'FunctionExpression:exit' : rules [ 'FunctionExpression:exit' ] ,
65
- 'ArrowFunctionExpression:exit' : rules [ 'ArrowFunctionExpression:exit' ] ,
66
- AwaitExpression : rules . AwaitExpression ,
67
- ForOfStatement : rules . ForOfStatement ,
68
-
69
128
ReturnStatement ( node ) : void {
129
+ // short circuit early to avoid unnecessary type checks
130
+ if ( ! scopeInfo || scopeInfo . hasAwait || ! scopeInfo . hasAsync ) {
131
+ return ;
132
+ }
133
+
70
134
const { expression } = parserServices . esTreeNodeToTSNodeMap . get ( node ) ;
71
135
if ( expression && isThenableType ( expression ) ) {
72
- // tell the base rule to mark the scope as having an await so it ignores it
73
- rules . AwaitExpression ( ) ;
136
+ markAsHasAwait ( ) ;
74
137
}
75
138
} ,
76
139
} ;
77
140
} ,
78
141
} ) ;
142
+
143
+ function isEmptyFunction ( node : FunctionNode ) : boolean {
144
+ return (
145
+ node . body ?. type === AST_NODE_TYPES . BlockStatement &&
146
+ node . body . body . length === 0
147
+ ) ;
148
+ }
149
+
150
+ // https://github.com/eslint/eslint/blob/03a69dbe86d5b5768a310105416ae726822e3c1c/lib/rules/utils/ast-utils.js#L382-L392
151
+ /**
152
+ * Gets the `(` token of the given function node.
153
+ */
154
+ function getOpeningParenOfParams (
155
+ node : FunctionNode ,
156
+ sourceCode : TSESLint . SourceCode ,
157
+ ) : TSESTree . Token {
158
+ return util . nullThrows (
159
+ node . id
160
+ ? sourceCode . getTokenAfter ( node . id , isOpeningParenToken )
161
+ : sourceCode . getFirstToken ( node , isOpeningParenToken ) ,
162
+ util . NullThrowsReasons . MissingToken ( '(' , node . type ) ,
163
+ ) ;
164
+ }
165
+
166
+ // https://github.com/eslint/eslint/blob/03a69dbe86d5b5768a310105416ae726822e3c1c/lib/rules/utils/ast-utils.js#L1220-L1242
167
+ /**
168
+ * Gets the location of the given function node for reporting.
169
+ */
170
+ function getFunctionHeadLoc (
171
+ node : FunctionNode ,
172
+ sourceCode : TSESLint . SourceCode ,
173
+ ) : TSESTree . SourceLocation {
174
+ const parent = util . nullThrows (
175
+ node . parent ,
176
+ util . NullThrowsReasons . MissingParent ,
177
+ ) ;
178
+ let start = null ;
179
+ let end = null ;
180
+
181
+ if ( node . type === AST_NODE_TYPES . ArrowFunctionExpression ) {
182
+ const arrowToken = util . nullThrows (
183
+ sourceCode . getTokenBefore ( node . body , isArrowToken ) ,
184
+ util . NullThrowsReasons . MissingToken ( '=>' , node . type ) ,
185
+ ) ;
186
+
187
+ start = arrowToken . loc . start ;
188
+ end = arrowToken . loc . end ;
189
+ } else if (
190
+ parent . type === AST_NODE_TYPES . Property ||
191
+ parent . type === AST_NODE_TYPES . MethodDefinition
192
+ ) {
193
+ start = parent . loc . start ;
194
+ end = getOpeningParenOfParams ( node , sourceCode ) . loc . start ;
195
+ } else {
196
+ start = node . loc . start ;
197
+ end = getOpeningParenOfParams ( node , sourceCode ) . loc . start ;
198
+ }
199
+
200
+ return {
201
+ start,
202
+ end,
203
+ } ;
204
+ }
0 commit comments