diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 8963aa480fdb9..0a44029477bd5 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -439,6 +439,7 @@ mod promise { pub mod no_return_in_finally; pub mod no_return_wrap; pub mod param_names; + pub mod prefer_await_to_then; pub mod valid_params; } @@ -839,4 +840,5 @@ oxc_macros::declare_all_lint_rules! { promise::param_names, promise::no_return_wrap, promise::no_promise_in_callback, + promise::prefer_await_to_then, } diff --git a/crates/oxc_linter/src/rules/promise/prefer_await_to_then.rs b/crates/oxc_linter/src/rules/promise/prefer_await_to_then.rs new file mode 100644 index 0000000000000..bcdcf1dc8d390 --- /dev/null +++ b/crates/oxc_linter/src/rules/promise/prefer_await_to_then.rs @@ -0,0 +1,107 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +fn prefer_wait_to_then_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn( + "eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()", + ) + .with_label(span0) +} + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Default, Clone)] +pub struct PreferAwaitToThen; + +declare_oxc_lint!( + /// ### What it does + /// + /// Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values + /// + /// ### Why is this bad? + /// + /// Async/await sytnax can be seen as more readable. + /// + /// ### Example + /// ```javascript + /// myPromise.then(doSomething) + /// ``` + PreferAwaitToThen, + style, +); + +fn is_inside_yield_or_await(node: &AstNode) -> bool { + matches!(node.kind(), AstKind::YieldExpression(_) | AstKind::AwaitExpression(_)) +} + +impl Rule for PreferAwaitToThen { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + + let Some(member_expr) = call_expr.callee.get_member_expr() else { + return; + }; + + let Some(prop_name) = member_expr.static_property_name() else { + return; + }; + + if !matches!(prop_name, "then" | "catch" | "finally") { + return; + } + + // Await statements cannot be added to the top level scope + if ctx.scopes().get_flags(node.scope_id()).is_top() { + return; + } + + // Already inside a yield or await + if ctx + .nodes() + .ancestors(node.id()) + .any(|node_id| is_inside_yield_or_await(ctx.nodes().get_node(node_id))) + { + return; + } + + ctx.diagnostic(prefer_wait_to_then_diagnostic(call_expr.span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "async function hi() { await thing() }", + "async function hi() { await thing().then() }", + "async function hi() { await thing().catch() }", + "a = async () => (await something())", + "a = async () => { + try { await something() } catch (error) { somethingElse() } + }", + "something().then(async () => await somethingElse())", + "function foo() { hey.somethingElse(x => {}) }", + "const isThenable = (obj) => { + return obj && typeof obj.then === 'function'; + };", + "function isThenable(obj) { + return obj && typeof obj.then === 'function'; + }", + ]; + + let fail = vec![ + "function foo() { hey.then(x => {}) }", + "function foo() { hey.then(function() { }).then() }", + "function foo() { hey.then(function() { }).then(x).catch() }", + "async function a() { hey.then(function() { }).then(function() { }) }", + "function foo() { hey.catch(x => {}) }", + "function foo() { hey.finally(x => {}) }", + ]; + + Tester::new(PreferAwaitToThen::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_await_to_then.snap b/crates/oxc_linter/src/snapshots/prefer_await_to_then.snap new file mode 100644 index 0000000000000..990f057288bac --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_await_to_then.snap @@ -0,0 +1,62 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.then(x => {}) } + · ───────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.then(function() { }).then() } + · ─────────────────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.then(function() { }).then() } + · ──────────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.then(function() { }).then(x).catch() } + · ──────────────────────────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.then(function() { }).then(x).catch() } + · ──────────────────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.then(function() { }).then(x).catch() } + · ──────────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:22] + 1 │ async function a() { hey.then(function() { }).then(function() { }) } + · ───────────────────────────────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:22] + 1 │ async function a() { hey.then(function() { }).then(function() { }) } + · ──────────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.catch(x => {}) } + · ────────────────── + ╰──── + + ⚠ eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally() + ╭─[prefer_await_to_then.tsx:1:18] + 1 │ function foo() { hey.finally(x => {}) } + · ──────────────────── + ╰────