Skip to content

Commit 45ae0b9

Browse files
authored
fix(eslint-plugin): [require-await] improve performance (typescript-eslint#1536)
1 parent 39929b2 commit 45ae0b9

File tree

3 files changed

+325
-73
lines changed

3 files changed

+325
-73
lines changed
Lines changed: 159 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import {
2-
TSESTree,
32
AST_NODE_TYPES,
3+
TSESLint,
4+
TSESTree,
45
} 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';
611
import * as tsutils from 'tsutils';
712
import * as ts from 'typescript';
813
import * as util from '../util';
914

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;
1224

13-
export default util.createRule<Options, MessageIds>({
25+
export default util.createRule({
1426
name: 'require-await',
1527
meta: {
1628
type: 'suggestion',
@@ -21,58 +33,172 @@ export default util.createRule<Options, MessageIds>({
2133
requiresTypeChecking: true,
2234
extendsBaseRule: true,
2335
},
24-
schema: baseRule.meta.schema,
25-
messages: baseRule.meta.messages,
36+
schema: [],
37+
messages: {
38+
missingAwait: "{{name}} has no 'await' expression.",
39+
},
2640
},
2741
defaultOptions: [],
2842
create(context) {
29-
const rules = baseRule.create(context);
3043
const parserServices = util.getParserServices(context);
3144
const checker = parserServices.program.getTypeChecker();
3245

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+
3384
/**
3485
* Checks if the node returns a thenable type
35-
*
36-
* @param {ASTNode} node - The node to check
37-
* @returns {boolean}
3886
*/
3987
function isThenableType(node: ts.Node): boolean {
4088
const type = checker.getTypeAtLocation(node);
4189

4290
return tsutils.isThenableType(checker, node, type);
4391
}
4492

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+
45104
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+
>,
51122
): 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();
61126
}
62127
},
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-
69128
ReturnStatement(node): void {
129+
// short circuit early to avoid unnecessary type checks
130+
if (!scopeInfo || scopeInfo.hasAwait || !scopeInfo.hasAsync) {
131+
return;
132+
}
133+
70134
const { expression } = parserServices.esTreeNodeToTSNodeMap.get(node);
71135
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();
74137
}
75138
},
76139
};
77140
},
78141
});
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

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy