Skip to content

Commit 3a4eaf9

Browse files
authored
feat: add suggestion to require-await to remove async keyword (#18716)
* feat: add suggestion to require-await to remove async keyword Fixes #18713 * use tokens to find async keyword * check value of parent when finding async keyword * only remove async keyword plus trailing space * remove all whitespace after async token * do not offer suggestion if ASI is currently in use before async keyword * replace async keyword with semicolon when ASI is used. * Use isStartOfExpressionStatement to determine if semicolon is needed. * Updated documentation for needsPrecedingSemicolon. * always create suggestion * do not add semicolon between methods.
1 parent 48117b2 commit 3a4eaf9

3 files changed

Lines changed: 159 additions & 16 deletions

File tree

lib/rules/require-await.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ module.exports = {
4242
schema: [],
4343

4444
messages: {
45-
missingAwait: "{{name}} has no 'await' expression."
46-
}
45+
missingAwait: "{{name}} has no 'await' expression.",
46+
removeAsync: "Remove 'async'."
47+
},
48+
49+
hasSuggestions: true
4750
},
4851

4952
create(context) {
@@ -69,6 +72,33 @@ module.exports = {
6972
*/
7073
function exitFunction(node) {
7174
if (!node.generator && node.async && !scopeInfo.hasAwait && !astUtils.isEmptyFunction(node)) {
75+
76+
/*
77+
* If the function belongs to a method definition or
78+
* property, then the function's range may not include the
79+
* `async` keyword and we should look at the parent instead.
80+
*/
81+
const nodeWithAsyncKeyword =
82+
(node.parent.type === "MethodDefinition" && node.parent.value === node) ||
83+
(node.parent.type === "Property" && node.parent.method && node.parent.value === node)
84+
? node.parent
85+
: node;
86+
87+
const asyncToken = sourceCode.getFirstToken(nodeWithAsyncKeyword, token => token.value === "async");
88+
const asyncRange = [asyncToken.range[0], sourceCode.getTokenAfter(asyncToken, { includeComments: true }).range[0]];
89+
90+
/*
91+
* Removing the `async` keyword can cause parsing errors if the current
92+
* statement is relying on automatic semicolon insertion. If ASI is currently
93+
* being used, then we should replace the `async` keyword with a semicolon.
94+
*/
95+
const nextToken = sourceCode.getTokenAfter(asyncToken);
96+
const addSemiColon =
97+
nextToken.type === "Punctuator" &&
98+
(nextToken.value === "[" || nextToken.value === "(") &&
99+
(nodeWithAsyncKeyword.type === "MethodDefinition" || astUtils.isStartOfExpressionStatement(nodeWithAsyncKeyword)) &&
100+
astUtils.needsPrecedingSemicolon(sourceCode, nodeWithAsyncKeyword);
101+
72102
context.report({
73103
node,
74104
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
@@ -77,7 +107,11 @@ module.exports = {
77107
name: capitalizeFirstLetter(
78108
astUtils.getFunctionNameWithKind(node)
79109
)
80-
}
110+
},
111+
suggest: [{
112+
messageId: "removeAsync",
113+
fix: fixer => fixer.replaceTextRange(asyncRange, addSemiColon ? ";" : "")
114+
}]
81115
});
82116
}
83117

lib/rules/utils/ast-utils.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,11 +1042,12 @@ function isStartOfExpressionStatement(node) {
10421042

10431043
/**
10441044
* Determines whether an opening parenthesis `(`, bracket `[` or backtick ``` ` ``` needs to be preceded by a semicolon.
1045-
* This opening parenthesis or bracket should be at the start of an `ExpressionStatement` or at the start of the body of an `ArrowFunctionExpression`.
1045+
* This opening parenthesis or bracket should be at the start of an `ExpressionStatement`, a `MethodDefinition` or at
1046+
* the start of the body of an `ArrowFunctionExpression`.
10461047
* @type {(sourceCode: SourceCode, node: ASTNode) => boolean}
10471048
* @param {SourceCode} sourceCode The source code object.
10481049
* @param {ASTNode} node A node at the position where an opening parenthesis or bracket will be inserted.
1049-
* @returns {boolean} Whether a semicolon is required before the opening parenthesis or braket.
1050+
* @returns {boolean} Whether a semicolon is required before the opening parenthesis or bracket.
10501051
*/
10511052
let needsPrecedingSemicolon;
10521053

@@ -1106,7 +1107,7 @@ let needsPrecedingSemicolon;
11061107

11071108
if (isClosingBraceToken(prevToken)) {
11081109
return (
1109-
prevNode.type === "BlockStatement" && prevNode.parent.type === "FunctionExpression" ||
1110+
prevNode.type === "BlockStatement" && prevNode.parent.type === "FunctionExpression" && prevNode.parent.parent.type !== "MethodDefinition" ||
11101111
prevNode.type === "ClassBody" && prevNode.parent.type === "ClassExpression" ||
11111112
prevNode.type === "ObjectExpression"
11121113
);

tests/lib/rules/require-await.js

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,70 +96,178 @@ ruleTester.run("require-await", rule, {
9696
code: "async function foo() { doSomething() }",
9797
errors: [{
9898
messageId: "missingAwait",
99-
data: { name: "Async function 'foo'" }
99+
data: { name: "Async function 'foo'" },
100+
suggestions: [
101+
{ output: "function foo() { doSomething() }", messageId: "removeAsync" }
102+
]
100103
}]
101104
},
102105
{
103106
code: "(async function() { doSomething() })",
104107
errors: [{
105108
messageId: "missingAwait",
106-
data: { name: "Async function" }
109+
data: { name: "Async function" },
110+
suggestions: [
111+
{ output: "(function() { doSomething() })", messageId: "removeAsync" }
112+
]
107113
}]
108114
},
109115
{
110116
code: "async () => { doSomething() }",
111117
errors: [{
112118
messageId: "missingAwait",
113-
data: { name: "Async arrow function" }
119+
data: { name: "Async arrow function" },
120+
suggestions: [
121+
{ output: "() => { doSomething() }", messageId: "removeAsync" }
122+
]
114123
}]
115124
},
116125
{
117126
code: "async () => doSomething()",
118127
errors: [{
119128
messageId: "missingAwait",
120-
data: { name: "Async arrow function" }
129+
data: { name: "Async arrow function" },
130+
suggestions: [
131+
{ output: "() => doSomething()", messageId: "removeAsync" }
132+
]
121133
}]
122134
},
123135
{
124136
code: "({ async foo() { doSomething() } })",
125137
errors: [{
126138
messageId: "missingAwait",
127-
data: { name: "Async method 'foo'" }
139+
data: { name: "Async method 'foo'" },
140+
suggestions: [
141+
{ output: "({ foo() { doSomething() } })", messageId: "removeAsync" }
142+
]
128143
}]
129144
},
130145
{
131146
code: "class A { async foo() { doSomething() } }",
132147
errors: [{
133148
messageId: "missingAwait",
134-
data: { name: "Async method 'foo'" }
149+
data: { name: "Async method 'foo'" },
150+
suggestions: [
151+
{ output: "class A { foo() { doSomething() } }", messageId: "removeAsync" }
152+
]
135153
}]
136154
},
137155
{
138156
code: "(class { async foo() { doSomething() } })",
139157
errors: [{
140158
messageId: "missingAwait",
141-
data: { name: "Async method 'foo'" }
159+
data: { name: "Async method 'foo'" },
160+
suggestions: [
161+
{ output: "(class { foo() { doSomething() } })", messageId: "removeAsync" }
162+
]
142163
}]
143164
},
144165
{
145166
code: "(class { async ''() { doSomething() } })",
146167
errors: [{
147168
messageId: "missingAwait",
148-
data: { name: "Async method ''" }
169+
data: { name: "Async method ''" },
170+
suggestions: [
171+
{ output: "(class { ''() { doSomething() } })", messageId: "removeAsync" }
172+
]
149173
}]
150174
},
151175
{
152176
code: "async function foo() { async () => { await doSomething() } }",
153177
errors: [{
154178
messageId: "missingAwait",
155-
data: { name: "Async function 'foo'" }
179+
data: { name: "Async function 'foo'" },
180+
suggestions: [
181+
{ output: "function foo() { async () => { await doSomething() } }", messageId: "removeAsync" }
182+
]
156183
}]
157184
},
158185
{
159186
code: "async function foo() { await (async () => { doSomething() }) }",
160187
errors: [{
161188
messageId: "missingAwait",
162-
data: { name: "Async arrow function" }
189+
data: { name: "Async arrow function" },
190+
suggestions: [
191+
{ output: "async function foo() { await (() => { doSomething() }) }", messageId: "removeAsync" }
192+
]
193+
}]
194+
},
195+
{
196+
code: "const obj = { async: async function foo() { bar(); } }",
197+
errors: [{
198+
messageId: "missingAwait",
199+
data: { name: "Async method 'async'" },
200+
suggestions: [
201+
{ output: "const obj = { async: function foo() { bar(); } }", messageId: "removeAsync" }
202+
]
203+
}]
204+
},
205+
{
206+
code: "async /* test */ function foo() { doSomething() }",
207+
errors: [{
208+
messageId: "missingAwait",
209+
data: { name: "Async function 'foo'" },
210+
suggestions: [
211+
{ output: "/* test */ function foo() { doSomething() }", messageId: "removeAsync" }
212+
]
213+
}]
214+
},
215+
{
216+
code: `class A {
217+
a = 0
218+
async [b](){ return 0; }
219+
}`,
220+
languageOptions: { ecmaVersion: 2022 },
221+
errors: [{
222+
messageId: "missingAwait",
223+
data: { name: "Async method" },
224+
suggestions: [
225+
{
226+
output: `class A {
227+
a = 0
228+
;[b](){ return 0; }
229+
}`,
230+
messageId: "removeAsync"
231+
}
232+
]
233+
}]
234+
},
235+
{
236+
code: `foo
237+
async () => { return 0; }
238+
`,
239+
languageOptions: { ecmaVersion: 2022 },
240+
errors: [{
241+
messageId: "missingAwait",
242+
data: { name: "Async arrow function" },
243+
suggestions: [
244+
{
245+
output: `foo
246+
;() => { return 0; }
247+
`,
248+
messageId: "removeAsync"
249+
}
250+
]
251+
}]
252+
},
253+
{
254+
code: `class A {
255+
foo() {}
256+
async [bar] () { baz; }
257+
}`,
258+
languageOptions: { ecmaVersion: 2022 },
259+
errors: [{
260+
messageId: "missingAwait",
261+
data: { name: "Async method" },
262+
suggestions: [
263+
{
264+
output: `class A {
265+
foo() {}
266+
[bar] () { baz; }
267+
}`,
268+
messageId: "removeAsync"
269+
}
270+
]
163271
}]
164272
}
165273
]

0 commit comments

Comments
 (0)