Skip to content

Commit 05722a1

Browse files
fiskersindresorhus
andauthored
Add prefer-top-level-await rule (#1325)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 03c540b commit 05722a1

File tree

8 files changed

+511
-8
lines changed

8 files changed

+511
-8
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Prefer top-level await over top-level promises and async function calls
2+
3+
[Top-level await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top-level-await) is more readable and can prevent unhandled rejections.
4+
5+
## Fail
6+
7+
```js
8+
(async () => {
9+
try {
10+
await run();
11+
} catch (error) {
12+
console.error(error);
13+
process.exit(1);
14+
}
15+
})();
16+
```
17+
18+
```js
19+
run().catch(error => {
20+
console.error(error);
21+
process.exit(1);
22+
});
23+
```
24+
25+
```js
26+
async function main() {
27+
try {
28+
await run();
29+
} catch (error) {
30+
console.error(error);
31+
process.exit(1);
32+
}
33+
}
34+
35+
main();
36+
```
37+
38+
## Pass
39+
40+
```js
41+
await run();
42+
```

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ module.exports = {
119119
'unicorn/prefer-string-trim-start-end': 'error',
120120
'unicorn/prefer-switch': 'error',
121121
'unicorn/prefer-ternary': 'error',
122+
// TODO: Enable this by default when targeting Node.js 14.
123+
'unicorn/prefer-top-level-await': 'off',
122124
'unicorn/prefer-type-error': 'error',
123125
'unicorn/prevent-abbreviations': 'error',
124126
'unicorn/require-array-join-separator': 'error',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ Configure it in `package.json`.
112112
"unicorn/prefer-string-trim-start-end": "error",
113113
"unicorn/prefer-switch": "error",
114114
"unicorn/prefer-ternary": "error",
115+
"unicorn/prefer-top-level-await": "off",
115116
"unicorn/prefer-type-error": "error",
116117
"unicorn/prevent-abbreviations": "error",
117118
"unicorn/require-array-join-separator": "error",
@@ -214,6 +215,7 @@ Each rule has emojis denoting:
214215
| [prefer-string-trim-start-end](docs/rules/prefer-string-trim-start-end.md) | Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. || 🔧 | |
215216
| [prefer-switch](docs/rules/prefer-switch.md) | Prefer `switch` over multiple `else-if`. || 🔧 | |
216217
| [prefer-ternary](docs/rules/prefer-ternary.md) | Prefer ternary expressions over simple `if-else` statements. || 🔧 | |
218+
| [prefer-top-level-await](docs/rules/prefer-top-level-await.md) | Prefer top-level await over top-level promises and async function calls. | | | 💡 |
217219
| [prefer-type-error](docs/rules/prefer-type-error.md) | Enforce throwing `TypeError` in type checking conditions. || 🔧 | |
218220
| [prevent-abbreviations](docs/rules/prevent-abbreviations.md) | Prevent abbreviations. || 🔧 | |
219221
| [require-array-join-separator](docs/rules/require-array-join-separator.md) | Enforce using the separator argument with `Array#join()`. || 🔧 | |

rules/prefer-top-level-await.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use strict';
2+
const {findVariable, getFunctionHeadLocation} = require('eslint-utils');
3+
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const {matches, memberExpressionSelector} = require('./selectors');
5+
6+
const ERROR_PROMISE = 'promise';
7+
const ERROR_IIFE = 'iife';
8+
const ERROR_IDENTIFIER = 'identifier';
9+
const SUGGESTION_ADD_AWAIT = 'add-await';
10+
const messages = {
11+
[ERROR_PROMISE]: 'Prefer top-level await over using a promise chain.',
12+
[ERROR_IIFE]: 'Prefer top-level await over an async IIFE.',
13+
[ERROR_IDENTIFIER]: 'Prefer top-level await over an async function `{{name}}` call.',
14+
[SUGGESTION_ADD_AWAIT]: 'Insert `await`.'
15+
};
16+
17+
const topLevelCallExpression = 'Program > ExpressionStatement > CallExpression[optional!=true].expression';
18+
const iife = [
19+
topLevelCallExpression,
20+
matches([
21+
'[callee.type="FunctionExpression"]',
22+
'[callee.type="ArrowFunctionExpression"]'
23+
]),
24+
'[callee.async!=false]',
25+
'[callee.generator!=true]'
26+
].join('');
27+
const promise = [
28+
topLevelCallExpression,
29+
memberExpressionSelector({
30+
path: 'callee',
31+
names: ['then', 'catch', 'finally']
32+
})
33+
].join('');
34+
const identifier = [
35+
topLevelCallExpression,
36+
'[callee.type="Identifier"]'
37+
].join('');
38+
39+
/** @param {import('eslint').Rule.RuleContext} context */
40+
function create(context) {
41+
return {
42+
[promise](node) {
43+
context.report({
44+
node: node.callee.property,
45+
messageId: ERROR_PROMISE
46+
});
47+
},
48+
[iife](node) {
49+
context.report({
50+
node,
51+
loc: getFunctionHeadLocation(node.callee, context.getSourceCode()),
52+
messageId: ERROR_IIFE
53+
});
54+
},
55+
[identifier](node) {
56+
const variable = findVariable(context.getScope(), node.callee);
57+
if (!variable || variable.defs.length !== 1) {
58+
return;
59+
}
60+
61+
const [definition] = variable.defs;
62+
const value = definition.type === 'Variable' && definition.kind === 'const' ?
63+
definition.node.init :
64+
definition.node;
65+
if (
66+
!(
67+
(
68+
value.type === 'ArrowFunctionExpression' ||
69+
value.type === 'FunctionExpression' ||
70+
value.type === 'FunctionDeclaration'
71+
) && !value.generator && value.async
72+
)
73+
) {
74+
return;
75+
}
76+
77+
context.report({
78+
node,
79+
messageId: ERROR_IDENTIFIER,
80+
data: {name: node.callee.name},
81+
suggest: [
82+
{
83+
messageId: SUGGESTION_ADD_AWAIT,
84+
fix: fixer => fixer.insertTextBefore(node, 'await ')
85+
}
86+
]
87+
});
88+
}
89+
};
90+
}
91+
92+
module.exports = {
93+
create,
94+
meta: {
95+
type: 'suggestion',
96+
docs: {
97+
description: 'Prefer top-level await over top-level promises and async function calls.',
98+
url: getDocumentationUrl(__filename),
99+
suggestion: true
100+
},
101+
schema: [],
102+
messages
103+
}
104+
};

test/prefer-top-level-await.mjs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
// Async IIFE
7+
test.snapshot({
8+
valid: [
9+
'a()',
10+
'a = async () => {}',
11+
'a = (async () => {})()',
12+
outdent`
13+
{
14+
(async () => {})();
15+
}
16+
`,
17+
'!async function() {}()',
18+
'void async function() {}()',
19+
'(async function *() {})()',
20+
'(async () => {})?.()'
21+
],
22+
invalid: [
23+
'(async () => {})()',
24+
'(async function() {})()',
25+
'(async function() {}())',
26+
'(async function run() {})()',
27+
'(async function(c, d) {})(a, b)'
28+
]
29+
});
30+
31+
// Promise
32+
test.snapshot({
33+
valid: [
34+
'foo.then',
35+
'foo.then().toString()',
36+
'!foo.then()',
37+
'foo.then?.(bar)',
38+
'foo?.then(bar)',
39+
'foo?.then(bar).finally(qux)'
40+
],
41+
invalid: [
42+
'foo.then(bar)',
43+
'foo.catch(() => process.exit(1))',
44+
'foo.finally(bar)',
45+
'foo.then(bar, baz)',
46+
'foo.then(bar, baz).finally(qux)',
47+
'(foo.then(bar, baz)).finally(qux)',
48+
'(async () => {})().catch(() => process.exit(1))',
49+
'(async function() {}()).finally(() => {})'
50+
]
51+
});
52+
53+
// Identifier
54+
test.snapshot({
55+
valid: [
56+
'foo()',
57+
'foo.bar()',
58+
outdent`
59+
function foo() {
60+
return async () => {};
61+
}
62+
foo()();
63+
`,
64+
outdent`
65+
const [foo] = [async () => {}];
66+
foo();
67+
`,
68+
outdent`
69+
function foo() {}
70+
foo();
71+
`,
72+
outdent`
73+
async function * foo() {}
74+
foo();
75+
`,
76+
outdent`
77+
var foo = async () => {};
78+
foo();
79+
`,
80+
outdent`
81+
let foo = async () => {};
82+
foo();
83+
`,
84+
outdent`
85+
const foo = 1, bar = async () => {};
86+
foo();
87+
`,
88+
outdent`
89+
async function foo() {}
90+
const bar = foo;
91+
bar();
92+
`,
93+
{
94+
code: outdent`
95+
async function foo() {}
96+
async function foo() {}
97+
foo();
98+
`,
99+
parserOptions: {sourceType: 'script'}
100+
},
101+
{
102+
code: outdent`
103+
foo();
104+
async function foo() {}
105+
async function foo() {}
106+
`,
107+
parserOptions: {sourceType: 'script'}
108+
},
109+
outdent`
110+
const foo = async () => {};
111+
foo?.();
112+
`,
113+
outdent`
114+
const program = {async run () {}};
115+
program.run()
116+
`,
117+
outdent`
118+
const program = {async run () {}};
119+
const {run} = program;
120+
run()
121+
`
122+
],
123+
invalid: [
124+
outdent`
125+
const foo = async () => {};
126+
foo();
127+
`,
128+
outdent`
129+
const foo = async function () {}, bar = 1;
130+
foo(bar);
131+
`,
132+
outdent`
133+
foo();
134+
async function foo() {}
135+
`
136+
]
137+
});
138+
139+
test.babel({
140+
valid: [
141+
'await foo',
142+
'await foo()',
143+
outdent`
144+
try {
145+
await run()
146+
} catch {
147+
process.exit(1)
148+
}
149+
`
150+
],
151+
invalid: []
152+
});
153+

test/run-rules-on-codebase/lint.mjs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@ const {recommended} = unicorn.configs;
66
const files = [process.argv[2] || '.'];
77
const fix = process.argv.includes('--fix');
88

9+
const enableAllRules = Object.fromEntries(
10+
Object.entries(recommended.rules)
11+
.filter(([id, options]) => id.startsWith('unicorn/') && options === 'off')
12+
.map(([id]) => [id, 'error'])
13+
);
14+
915
const eslint = new ESLint({
10-
baseConfig: recommended,
16+
baseConfig: {
17+
...recommended,
18+
rules: enableAllRules
19+
},
1120
useEslintrc: false,
1221
extensions: ['.js', '.mjs'],
1322
plugins: {
@@ -39,17 +48,22 @@ const eslint = new ESLint({
3948
'flatten'
4049
]
4150
}
42-
]
51+
],
52+
// Annoying
53+
'unicorn/no-keyword-prefix': 'off',
54+
'unicorn/no-unsafe-regex': 'off',
55+
// Outdated
56+
'unicorn/import-index': 'off',
57+
// Not ready yet
58+
'unicorn/prefer-string-replace-all': 'off',
59+
'unicorn/prefer-top-level-await': 'off',
60+
'unicorn/prefer-object-has-own': 'off',
61+
'unicorn/prefer-at': 'off'
4362
},
4463
overrides: [
4564
{
4665
files: [
47-
// ESLint don't support module
48-
'rules/**/*.js',
49-
'index.js',
50-
'test/integration/config.js',
51-
// `eslint-remote-tester` only support cjs config
52-
'test/smoke/eslint-remote-tester.config.js'
66+
'**/*.js'
5367
],
5468
rules: {
5569
'unicorn/prefer-module': 'off'

0 commit comments

Comments
 (0)