From f54eb1a026fe2bbe618efe370c1bd6dae23ecf0c Mon Sep 17 00:00:00 2001
From: roikoren755 <26850796+roikoren755@users.noreply.github.com>
Date: Thu, 2 Dec 2021 12:20:18 +0200
Subject: [PATCH] feat: add `es-roikoren/no-top-level-await` rule (#12)

* feat: add `es-roikoren/no-top-level-await` rule

* docs: tiny update

* fix: import order

* test: fix

* chore: update watch script

* test: small fix
---
 .changeset/real-rabbits-listen.md     |  5 ++
 docs/rules/README.md                  |  1 +
 docs/rules/no-top-level-await.md      | 20 +++++++
 package.json                          |  2 +-
 src/configs/no-new-in-esnext.ts       |  1 +
 src/index.ts                          |  2 +
 src/rules/no-top-level-await.ts       | 37 ++++++++++++
 tests/src/rules/no-top-level-await.ts | 84 +++++++++++++++++++++++++++
 8 files changed, 151 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/real-rabbits-listen.md
 create mode 100644 docs/rules/no-top-level-await.md
 create mode 100644 src/rules/no-top-level-await.ts
 create mode 100644 tests/src/rules/no-top-level-await.ts

diff --git a/.changeset/real-rabbits-listen.md b/.changeset/real-rabbits-listen.md
new file mode 100644
index 00000000..0f3f65c6
--- /dev/null
+++ b/.changeset/real-rabbits-listen.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-es-roikoren': patch
+---
+
+feat: add `es-roikoren/no-top-level-await` rule
diff --git a/docs/rules/README.md b/docs/rules/README.md
index e8fb7926..7a30f931 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -16,6 +16,7 @@ There is a config that enables the rules in this category: `plugin:es-roikoren/n
 | [es-roikoren/no-object-hasown](./no-object-hasown.md) | disallow the `Object.hasOwn` method. |  |
 | [es-roikoren/no-private-in](./no-private-in.md) | disallow `#x in obj`. |  |
 | [es-roikoren/no-regexp-d-flag](./no-regexp-d-flag.md) | disallow RegExp `d` flag. |  |
+| [es-roikoren/no-top-level-await](./no-top-level-await.md) | disallow top-level `await`. |  |
 
 ## ES2021
 
diff --git a/docs/rules/no-top-level-await.md b/docs/rules/no-top-level-await.md
new file mode 100644
index 00000000..0246f38d
--- /dev/null
+++ b/docs/rules/no-top-level-await.md
@@ -0,0 +1,20 @@
+# es-roikoren/no-top-level-await
+> disallow top-level `await`.
+
+- ✅ The following configurations enable this rule: `plugin:es-roikoren/no-new-in-esnext`
+
+This rule reports ES2022 [Top-level `await`](https://github.com/tc39/proposal-top-level-await) as errors.
+
+## Examples
+
+⛔ Examples of **incorrect** code for this rule:
+
+```js
+/*eslint es-roikoren/no-top-level-await: error */
+await expr;
+```
+
+## 📚 References
+
+- [Rule source](https://github.com/roikoren755/eslint-plugin-es/blob/v0.0.6/src/rules/no-top-level-await.ts)
+- [Test source](https://github.com/roikoren755/eslint-plugin-es/blob/v0.0.6/tests/src/rules/no-top-level-await.ts)
diff --git a/package.json b/package.json
index 7bed5a32..e5ea1025 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
     "update:doc": "ts-node scripts/update-docs-readme",
     "update:index": "ts-node scripts/update-src-index",
     "update:ruledocs": "ts-node scripts/update-docs-rules",
-    "watch": "mocha tests/**/*.js --reporter progress --watch --growl"
+    "watch": "mocha tests/**/*.ts --reporter progress --watch"
   },
   "resolutions": {
     "prettier": "2.5.0"
diff --git a/src/configs/no-new-in-esnext.ts b/src/configs/no-new-in-esnext.ts
index b8a89e1e..e86aa702 100644
--- a/src/configs/no-new-in-esnext.ts
+++ b/src/configs/no-new-in-esnext.ts
@@ -13,6 +13,7 @@ const config: TSESLint.Linter.Config = {
     'es-roikoren/no-object-hasown': 'error',
     'es-roikoren/no-private-in': 'error',
     'es-roikoren/no-regexp-d-flag': 'error',
+    'es-roikoren/no-top-level-await': 'error',
   },
 };
 
diff --git a/src/index.ts b/src/index.ts
index 440fab28..d3346941 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -167,6 +167,7 @@ import noSubclassingBuiltins from './rules/no-subclassing-builtins';
 import noSymbol from './rules/no-symbol';
 import noSymbolPrototypeDescription from './rules/no-symbol-prototype-description';
 import noTemplateLiterals from './rules/no-template-literals';
+import noTopLevelAwait from './rules/no-top-level-await';
 import noTrailingCommas from './rules/no-trailing-commas';
 import noTrailingFunctionCommas from './rules/no-trailing-function-commas';
 import noTypedArrays from './rules/no-typed-arrays';
@@ -344,6 +345,7 @@ export default {
     'no-symbol': noSymbol,
     'no-symbol-prototype-description': noSymbolPrototypeDescription,
     'no-template-literals': noTemplateLiterals,
+    'no-top-level-await': noTopLevelAwait,
     'no-trailing-commas': noTrailingCommas,
     'no-trailing-function-commas': noTrailingFunctionCommas,
     'no-typed-arrays': noTypedArrays,
diff --git a/src/rules/no-top-level-await.ts b/src/rules/no-top-level-await.ts
new file mode 100644
index 00000000..499b4c12
--- /dev/null
+++ b/src/rules/no-top-level-await.ts
@@ -0,0 +1,37 @@
+import type { TSESTree } from '@typescript-eslint/typescript-estree';
+
+import { createRule } from '../util/create-rule';
+
+export const category = 'ES2022';
+export default createRule<[], 'forbidden'>({
+  name: 'no-top-level-await',
+  meta: {
+    type: 'problem',
+    docs: { description: 'disallow top-level `await`.', recommended: false },
+    schema: [],
+    messages: { forbidden: "ES2022 top-level 'await' is forbidden." },
+  },
+  defaultOptions: [],
+  create(context) {
+    let inFunction: TSESTree.FunctionExpression | null = null;
+
+    return {
+      ':function'(node: TSESTree.FunctionExpression) {
+        inFunction = node;
+      },
+      ':function:exit'(node: TSESTree.FunctionExpression) {
+        if (inFunction === node) {
+          inFunction = null;
+        }
+      },
+      'AwaitExpression, ForOfStatement[await=true]'(node: TSESTree.AwaitExpression | TSESTree.ForOfStatement) {
+        if (inFunction) {
+          // not top-level
+          return;
+        }
+
+        context.report({ node, messageId: 'forbidden' });
+      },
+    };
+  },
+});
diff --git a/tests/src/rules/no-top-level-await.ts b/tests/src/rules/no-top-level-await.ts
new file mode 100644
index 00000000..6aeddd8c
--- /dev/null
+++ b/tests/src/rules/no-top-level-await.ts
@@ -0,0 +1,84 @@
+import type { TSESLint } from '@typescript-eslint/experimental-utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/types';
+
+import { RuleTester } from '../../tester';
+import rule from '../../../src/rules/no-top-level-await';
+
+const error = { messageId: 'forbidden' as const, line: 1, column: 1, type: AST_NODE_TYPES.AwaitExpression, data: {} };
+
+if (!RuleTester.isSupported(2022)) {
+  console.log('Skip the tests of no-top-level-await.');
+} else {
+  new RuleTester({ parserOptions: { sourceType: 'module' } } as TSESLint.RuleTesterConfig).run(
+    'no-top-level-await',
+    rule,
+    {
+      valid: [
+        'async function f() { await expr }',
+        'expr;',
+        'const f = async function() { await expr }',
+        'const f = async () => { await expr }',
+        '({ async method() { await expr } })',
+        'class A { async method() { await expr } }',
+        '(class { async method() { await expr } })',
+        'async function f() { for await (a of b); }',
+        'async function f() { for await (var a of b); }',
+        'async function f() { for await (let a of b); }',
+        'async function f() { for await (const a of b); }',
+        'function f() { async function f() { await expr } }',
+      ],
+      invalid: [
+        { code: 'await expr', errors: [{ ...error }] },
+        { code: 'for await (a of b);', errors: [{ ...error, type: AST_NODE_TYPES.ForOfStatement }] },
+        { code: 'for await (var a of b);', errors: [{ ...error, type: AST_NODE_TYPES.ForOfStatement }] },
+        { code: 'for await (let a of b);', errors: [{ ...error, type: AST_NODE_TYPES.ForOfStatement }] },
+        { code: 'for await (const a of b);', errors: [{ ...error, type: AST_NODE_TYPES.ForOfStatement }] },
+        {
+          code: `
+await expr
+async function f() {
+  await expr
+}
+await expr`,
+          errors: [
+            { ...error, line: 2 },
+            { ...error, line: 6 },
+          ],
+        },
+        {
+          code: `
+await expr
+async function f() {
+  await expr
+  async function f() {
+    await expr
+  }
+}
+await expr`,
+          errors: [
+            { ...error, line: 2 },
+            { ...error, line: 9 },
+          ],
+        },
+        {
+          code: `
+let jQuery;
+try {
+  jQuery = await import('https://cdn-a.com/jQuery');
+} catch {
+  jQuery = await import('https://cdn-b.com/jQuery');
+}`,
+          errors: [
+            { ...error, line: 4, column: 12 },
+            { ...error, line: 6, column: 12 },
+          ],
+        },
+        { code: '{ await expr }', errors: [{ ...error, column: 3 }] },
+        { code: '( await expr )', errors: [{ ...error, column: 3 }] },
+        { code: 'fn( await expr )', errors: [{ ...error, column: 5 }] },
+        { code: 'if (foo) { await expr }', errors: [{ ...error, column: 12 }] },
+        { code: 'for (const foo of bar) { await expr }', errors: [{ ...error, column: 26 }] },
+      ],
+    },
+  );
+}