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 }] }, + ], + }, + ); +}