Skip to content

Commit b054d65

Browse files
authored
prefer-top-level-await: Improve top-level expression detection (#1526)
1 parent 7fb6f7b commit b054d65

File tree

4 files changed

+316
-28
lines changed

4 files changed

+316
-28
lines changed

rules/prefer-top-level-await.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ const messages = {
1313
[SUGGESTION_ADD_AWAIT]: 'Insert `await`.',
1414
};
1515

16-
const topLevelCallExpression = 'Program > ExpressionStatement > CallExpression[optional!=true].expression';
16+
const promiseMethods = ['then', 'catch', 'finally'];
17+
18+
const topLevelCallExpression = 'CallExpression:not(:function *)';
1719
const iife = [
1820
topLevelCallExpression,
1921
matches([
@@ -27,31 +29,60 @@ const promise = [
2729
topLevelCallExpression,
2830
memberExpressionSelector({
2931
path: 'callee',
30-
properties: ['then', 'catch', 'finally'],
32+
properties: promiseMethods,
33+
includeOptional: true,
3134
}),
3235
].join('');
3336
const identifier = [
3437
topLevelCallExpression,
3538
'[callee.type="Identifier"]',
3639
].join('');
3740

41+
const isPromiseMethodCalleeObject = node =>
42+
node.parent.type === 'MemberExpression'
43+
&& node.parent.object === node
44+
&& !node.parent.computed
45+
&& node.parent.property.type === 'Identifier'
46+
&& promiseMethods.includes(node.parent.property.name)
47+
&& node.parent.parent.type === 'CallExpression'
48+
&& node.parent.parent.callee === node.parent;
49+
const isAwaitArgument = node => {
50+
if (node.parent.type === 'ChainExpression') {
51+
node = node.parent;
52+
}
53+
54+
return node.parent.type === 'AwaitExpression' && node.parent.argument === node;
55+
};
56+
3857
/** @param {import('eslint').Rule.RuleContext} context */
3958
function create(context) {
4059
return {
4160
[promise](node) {
61+
if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) {
62+
return;
63+
}
64+
4265
return {
4366
node: node.callee.property,
4467
messageId: ERROR_PROMISE,
4568
};
4669
},
4770
[iife](node) {
71+
if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) {
72+
return;
73+
}
74+
4875
return {
4976
node,
5077
loc: getFunctionHeadLocation(node.callee, context.getSourceCode()),
5178
messageId: ERROR_IIFE,
5279
};
5380
},
5481
[identifier](node) {
82+
if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) {
83+
return;
84+
}
85+
5586
const variable = findVariable(context.getScope(), node.callee);
5687
if (!variable || variable.defs.length !== 1) {
5788
return;

test/prefer-top-level-await.mjs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,64 @@ test.snapshot({
88
valid: [
99
'a()',
1010
'a = async () => {}',
11-
'a = (async () => {})()',
11+
'(async function *() {})()',
1212
outdent`
13-
{
14-
(async () => {})();
13+
function foo() {
14+
if (foo) {
15+
(async () => {})()
16+
}
1517
}
1618
`,
17-
'!async function() {}()',
18-
'void async function() {}()',
19-
'(async function *() {})()',
20-
'(async () => {})?.()',
19+
'await (async () => {})()',
2120
],
2221
invalid: [
2322
'(async () => {})()',
23+
'(async () => {})?.()',
2424
'(async function() {})()',
2525
'(async function() {}())',
2626
'(async function run() {})()',
2727
'(async function(c, d) {})(a, b)',
28+
'if (foo) (async () => {})()',
29+
outdent`
30+
{
31+
(async () => {})();
32+
}
33+
`,
34+
'a = (async () => {})()',
35+
'!async function() {}()',
36+
'void async function() {}()',
37+
'(async () => {})().catch(foo)',
2838
],
2939
});
3040

3141
// Promise
3242
test.snapshot({
3343
valid: [
3444
'foo.then',
35-
'foo.then().toString()',
36-
'!foo.then()',
37-
'foo.then?.(bar)',
38-
'foo?.then(bar)',
39-
'foo?.then(bar).finally(qux)',
45+
'await foo.then(bar)',
46+
'await foo.then(bar).catch(bar)',
47+
'await foo.then?.(bar)',
48+
'await foo.then(bar)?.catch(bar)',
49+
'await foo.then(bar)?.catch?.(bar)',
4050
],
4151
invalid: [
4252
'foo.then(bar)',
53+
'foo.then?.(bar)',
54+
'foo?.then(bar)',
4355
'foo.catch(() => process.exit(1))',
4456
'foo.finally(bar)',
4557
'foo.then(bar, baz)',
4658
'foo.then(bar, baz).finally(qux)',
4759
'(foo.then(bar, baz)).finally(qux)',
4860
'(async () => {})().catch(() => process.exit(1))',
4961
'(async function() {}()).finally(() => {})',
62+
'for (const foo of bar) foo.then(bar)',
63+
'foo?.then(bar).finally(qux)',
64+
'foo.then().toString()',
65+
'!foo.then()',
66+
'foo.then(bar).then(baz)?.then(qux)',
67+
'foo.then(bar).then(baz).then?.(qux)',
68+
'foo.then(bar).catch(bar).finally(bar)',
5069
],
5170
});
5271

@@ -106,10 +125,6 @@ test.snapshot({
106125
`,
107126
parserOptions: {sourceType: 'script'},
108127
},
109-
outdent`
110-
const foo = async () => {};
111-
foo?.();
112-
`,
113128
outdent`
114129
const program = {async run () {}};
115130
program.run()
@@ -119,12 +134,24 @@ test.snapshot({
119134
const {run} = program;
120135
run()
121136
`,
137+
outdent`
138+
const foo = async () => {};
139+
await foo();
140+
`,
122141
],
123142
invalid: [
124143
outdent`
125144
const foo = async () => {};
126145
foo();
127146
`,
147+
outdent`
148+
const foo = async () => {};
149+
foo?.();
150+
`,
151+
outdent`
152+
const foo = async () => {};
153+
foo().then(foo);
154+
`,
128155
outdent`
129156
const foo = async function () {}, bar = 1;
130157
foo(bar);
@@ -133,6 +160,14 @@ test.snapshot({
133160
foo();
134161
async function foo() {}
135162
`,
163+
outdent`
164+
const foo = async () => {};
165+
if (true) {
166+
alert();
167+
} else {
168+
foo();
169+
}
170+
`,
136171
],
137172
});
138173

0 commit comments

Comments
 (0)