diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 66d9a7a4e0414..b4b150fc688cb 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2800,6 +2800,11 @@ impl RuleRunner for crate::rules::unicorn::prefer_structured_clone::PreferStruct Some(&AstTypesBitset::from_types(&[AstType::CallExpression])); } +impl RuleRunner for crate::rules::unicorn::prefer_top_level_await::PreferTopLevelAwait { + const NODE_TYPES: Option<&AstTypesBitset> = + Some(&AstTypesBitset::from_types(&[AstType::CallExpression])); +} + impl RuleRunner for crate::rules::unicorn::prefer_type_error::PreferTypeError { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::ThrowStatement])); diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index a36446817a7d2..19002d7738f65 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -487,6 +487,7 @@ pub(crate) mod unicorn { pub mod prefer_string_starts_ends_with; pub mod prefer_string_trim_start_end; pub mod prefer_structured_clone; + pub mod prefer_top_level_await; pub mod prefer_type_error; pub mod require_array_join_separator; pub mod require_number_to_fixed_digits_argument; @@ -1177,6 +1178,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::number_literal_case, unicorn::numeric_separators_style, unicorn::prefer_class_fields, + unicorn::prefer_top_level_await, unicorn::prefer_at, unicorn::prefer_global_this, unicorn::prefer_object_from_entries, diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_top_level_await.rs b/crates/oxc_linter/src/rules/unicorn/prefer_top_level_await.rs new file mode 100644 index 0000000000000..c050d409cfb19 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/prefer_top_level_await.rs @@ -0,0 +1,464 @@ +use oxc_ast::{ + AstKind, + ast::{Expression, VariableDeclarationKind}, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{AstNode, ast_util::is_method_call, context::LintContext, rule::Rule}; + +fn prefer_top_level_await_over_promise_chain_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Prefer top-level await over using a promise chain.").with_label(span) +} + +fn prefer_top_level_await_over_async_iife_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Prefer top-level await over using an async IIFE.").with_label(span) +} + +fn prefer_top_level_await_over_async_function_call_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Prefer top-level await over an async function call.") + .with_help("Add `await` before the function call.") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct PreferTopLevelAwait; + +declare_oxc_lint!( + /// ### What it does + /// + /// Prefer top-level await over top-level promises and async function calls. + /// + /// ### Why is this bad? + /// + /// Top-level await is more readable and can prevent unhandled rejections. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// (async () => { + /// await run(); + /// })(); + /// + /// run().catch(error => { + /// console.error(error); + /// }); + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// await run(); + /// + /// try { + /// await run(); + /// } catch (error) { + /// console.error(error); + /// } + /// ``` + PreferTopLevelAwait, + unicorn, + pedantic +); + +impl Rule for PreferTopLevelAwait { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + + if node.scope_id() != ctx.scoping().root_scope_id() + && ctx.nodes().ancestor_kinds(node.id()).any(|kind| { + matches!( + kind, + AstKind::FunctionBody(_) + | AstKind::ArrowFunctionExpression(_) + | AstKind::ClassBody(_) + ) + }) + { + return; + } + + let parent = ctx.nodes().parent_node(node.id()); + // TODO: remove this block once removing `AstKind::Argument` is complete + let grand_parent = { + let p = ctx.nodes().parent_node(parent.id()); + if let AstKind::Argument(_) = p.kind() { ctx.nodes().parent_node(p.id()) } else { p } + }; + + if let AstKind::StaticMemberExpression(member_expr) = parent.kind() + && member_expr.object.span() == call_expr.span + && matches!(member_expr.property.name.as_str(), "then" | "catch" | "finally") + && let AstKind::CallExpression(grand_call_expr) = grand_parent.kind() + && grand_call_expr.callee.span() == member_expr.span() + { + return; + } + + if let Some(AstKind::AwaitExpression(_)) = ctx + .nodes() + .ancestors(node.id()) + .find(|ancestor| { + !matches!( + ancestor.kind(), + AstKind::ParenthesizedExpression(_) + | AstKind::TSAsExpression(_) + | AstKind::TSSatisfiesExpression(_) + | AstKind::ChainExpression(_) + | AstKind::StaticMemberExpression(_) + ) + }) + .map(AstNode::kind) + { + return; + } + + if let AstKind::ArrayExpression(_) = parent.kind() + && let AstKind::CallExpression(grand_call_expr) = grand_parent.kind() + && is_method_call( + grand_call_expr, + Some(&["Promise"]), + Some(&["all", "allSettled", "any", "race"]), + Some(1), + Some(1), + ) + { + return; + } + + if let Expression::StaticMemberExpression(member_expr) = &call_expr.callee + && matches!(member_expr.property.name.as_str(), "then" | "catch" | "finally") + { + ctx.diagnostic(prefer_top_level_await_over_promise_chain_diagnostic(call_expr.span)); + return; + } + + if match call_expr.callee.get_inner_expression() { + Expression::FunctionExpression(func) if func.r#async && !func.generator => true, + Expression::ArrowFunctionExpression(func) if func.r#async => true, + _ => false, + } { + ctx.diagnostic(prefer_top_level_await_over_async_iife_diagnostic(call_expr.span)); + return; + } + + let Expression::Identifier(ident) = &call_expr.callee else { + return; + }; + + let Some(symbol_id) = ctx.scoping().get_reference(ident.reference_id()).symbol_id() else { + return; + }; + + if ctx.scoping().get_resolved_references(symbol_id).count() > 1 { + return; + } + + let declaration = ctx.symbol_declaration(symbol_id); + + match declaration.kind() { + AstKind::VariableDeclarator(var_decl) + if var_decl.kind == VariableDeclarationKind::Const => + { + let Some(init) = &var_decl.init else { return }; + + if !matches!(init.get_inner_expression(), Expression::ArrowFunctionExpression(func) if func.r#async) + && !matches!(init.get_inner_expression(), Expression::FunctionExpression(func) if func.r#async && !func.generator) + { + return; + } + + ctx.diagnostic(prefer_top_level_await_over_async_function_call_diagnostic( + call_expr.span, + )); + } + AstKind::Function(func) if func.r#async && !func.generator => { + ctx.diagnostic(prefer_top_level_await_over_async_function_call_diagnostic( + call_expr.span, + )); + } + _ => {} + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + use std::path::PathBuf; + + let pass = vec![ + ("a()", None, None, None), + ("a = async () => {}", None, None, None), + ("(async function *() {})()", None, None, None), + ( + "function foo() { + if (foo) { + (async () => {})() + } + }", + None, + None, + None, + ), + ("await (async () => {})()", None, None, None), + ("foo.then", None, None, None), + ("await foo.then(bar)", None, None, None), + ("await foo.then(bar).catch(bar)", None, None, None), + ("await foo.then?.(bar)", None, None, None), + ("await foo.then(bar)?.catch(bar)", None, None, None), + ("await foo.then(bar)?.catch?.(bar)", None, None, None), + ( + "class Example { + property = promise.then(bar) + }", + None, + None, + None, + ), + ( + "const Example = class Example { + property = promise.then(bar) + }", + None, + None, + None, + ), + ( + "class Example { + static { + promise.then(bar) + } + }", + None, + None, + None, + ), + ( + "const Example = class Example { + static { + promise.then(bar) + } + }", + None, + None, + None, + ), + ("foo.then(bar)", None, None, Some(PathBuf::from("'foo.cjS'"))), + ("foo()", None, None, None), + ("foo.bar()", None, None, None), + ( + "function foo() { + return async () => {}; + } + foo()();", + None, + None, + None, + ), + ( + "const [foo] = [async () => {}]; + foo();", + None, + None, + None, + ), + ( + "function foo() {} + foo();", + None, + None, + None, + ), + ( + "async function * foo() {} + foo();", + None, + None, + None, + ), + ( + "var foo = async () => {}; + foo();", + None, + None, + None, + ), + ( + "let foo = async () => {}; + foo();", + None, + None, + None, + ), + ( + "const foo = 1, bar = async () => {}; + foo();", + None, + None, + None, + ), + ( + "async function foo() {} + const bar = foo; + bar();", + None, + None, + None, + ), + ( + "const program = {async run () {}}; + program.run()", + None, + None, + None, + ), + ( + "const program = {async run () {}}; + const {run} = program; + run()", + None, + None, + None, + ), + ( + "const foo = async () => {}; + await foo();", + None, + None, + None, + ), + ("for (const statement of statements) { statement() };", None, None, None), + ( + "const foo = async () => {}; + await Promise.all([ + (async () => {})(), + /* hole */, + foo(), + foo.then(bar), + foo.catch(bar), + ]); + await Promise.allSettled([foo()]); + await Promise?.any([foo()]); + await Promise.race?.([foo()]);", + None, + None, + None, + ), + ( + "const foo = async () => {}; + const promise = Promise.all([ + (async () => {})(), + foo(), + foo.then(bar), + foo.catch(bar), + ]); + await promise;", + None, + None, + None, + ), + ("await foo", None, None, None), + ("await foo()", None, None, None), + ( + "try { + await run() + } catch { + process.exit(1) + }", + None, + None, + None, + ), + ]; + + let fail = vec![ + ("(async () => {})()", None, None, None), + ("(async () => {})?.()", None, None, None), + ("(async function() {})()", None, None, None), + ("(async function() {}())", None, None, None), + ("(async function run() {})()", None, None, None), + ("(async function(c, d) {})(a, b)", None, None, None), + ("if (foo) (async () => {})()", None, None, None), + ( + "{ + (async () => {})(); + }", + None, + None, + None, + ), + ("a = (async () => {})()", None, None, None), + ("!async function() {}()", None, None, None), + ("void async function() {}()", None, None, None), + ("(async () => {})().catch(foo)", None, None, None), + ("foo.then(bar)", None, None, None), + ("foo.then?.(bar)", None, None, None), + ("foo?.then(bar)", None, None, None), + ("foo.catch(() => process.exit(1))", None, None, None), + ("foo.finally(bar)", None, None, None), + ("foo.then(bar, baz)", None, None, None), + ("foo.then(bar, baz).finally(qux)", None, None, None), + ("(foo.then(bar, baz)).finally(qux)", None, None, None), + ("(async () => {})().catch(() => process.exit(1))", None, None, None), + ("(async function() {}()).finally(() => {})", None, None, None), + ("for (const foo of bar) foo.then(bar)", None, None, None), + ("foo?.then(bar).finally(qux)", None, None, None), + ("foo.then().toString()", None, None, None), + ("!foo.then()", None, None, None), + ("foo.then(bar).then(baz)?.then(qux)", None, None, None), + ("foo.then(bar).then(baz).then?.(qux)", None, None, None), + ("foo.then(bar).catch(bar).finally(bar)", None, None, None), + ( + "const foo = async () => {}; + foo();", + None, + None, + None, + ), + ( + "const foo = async () => {}; + foo?.();", + None, + None, + None, + ), + ( + "const foo = async () => {}; + foo().then(foo);", + None, + None, + None, + ), + ( + "const foo = async function () {}, bar = 1; + foo(bar);", + None, + None, + None, + ), + ( + "foo(); + async function foo() {}", + None, + None, + None, + ), + ( + "const foo = async () => {}; + if (true) { + alert(); + } else { + foo(); + }", + None, + None, + None, + ), + ]; + + Tester::new(PreferTopLevelAwait::NAME, PreferTopLevelAwait::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/unicorn_prefer_top_level_await.snap b/crates/oxc_linter/src/snapshots/unicorn_prefer_top_level_await.snap new file mode 100644 index 0000000000000..d092dd005b106 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/unicorn_prefer_top_level_await.snap @@ -0,0 +1,238 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async () => {})() + · ────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async () => {})?.() + · ──────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async function() {})() + · ─────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:2] + 1 │ (async function() {}()) + · ───────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async function run() {})() + · ─────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async function(c, d) {})(a, b) + · ─────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:10] + 1 │ if (foo) (async () => {})() + · ────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:2:5] + 1 │ { + 2 │ (async () => {})(); + · ────────────────── + 3 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:5] + 1 │ a = (async () => {})() + · ────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:2] + 1 │ !async function() {}() + · ───────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:6] + 1 │ void async function() {}() + · ───────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async () => {})().catch(foo) + · ───────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then(bar) + · ───────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then?.(bar) + · ─────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo?.then(bar) + · ────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.catch(() => process.exit(1)) + · ──────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.finally(bar) + · ──────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then(bar, baz) + · ────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then(bar, baz).finally(qux) + · ─────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (foo.then(bar, baz)).finally(qux) + · ───────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:2] + 1 │ (foo.then(bar, baz)).finally(qux) + · ────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async () => {})().catch(() => process.exit(1)) + · ─────────────────────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ (async function() {}()).finally(() => {}) + · ───────────────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using an async IIFE. + ╭─[prefer_top_level_await.tsx:1:2] + 1 │ (async function() {}()).finally(() => {}) + · ───────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:24] + 1 │ for (const foo of bar) foo.then(bar) + · ───────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo?.then(bar).finally(qux) + · ─────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then().toString() + · ────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:2] + 1 │ !foo.then() + · ────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then(bar).then(baz)?.then(qux) + · ────────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then(bar).then(baz).then?.(qux) + · ─────────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo.then(bar).catch(bar).finally(bar) + · ───────────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over an async function call. + ╭─[prefer_top_level_await.tsx:2:4] + 1 │ const foo = async () => {}; + 2 │ foo(); + · ───── + ╰──── + help: Add `await` before the function call. + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over an async function call. + ╭─[prefer_top_level_await.tsx:2:4] + 1 │ const foo = async () => {}; + 2 │ foo?.(); + · ─────── + ╰──── + help: Add `await` before the function call. + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over using a promise chain. + ╭─[prefer_top_level_await.tsx:2:4] + 1 │ const foo = async () => {}; + 2 │ foo().then(foo); + · ─────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over an async function call. + ╭─[prefer_top_level_await.tsx:2:4] + 1 │ const foo = async function () {}, bar = 1; + 2 │ foo(bar); + · ──────── + ╰──── + help: Add `await` before the function call. + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over an async function call. + ╭─[prefer_top_level_await.tsx:1:1] + 1 │ foo(); + · ───── + 2 │ async function foo() {} + ╰──── + help: Add `await` before the function call. + + ⚠ eslint-plugin-unicorn(prefer-top-level-await): Prefer top-level await over an async function call. + ╭─[prefer_top_level_await.tsx:5:5] + 4 │ } else { + 5 │ foo(); + · ───── + 6 │ } + ╰──── + help: Add `await` before the function call.