From be9cdfcd830efc4335c10a55fdd8d12d186d2470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E5=85=89=E5=BF=97?= <411020382@qq.com> Date: Mon, 22 Apr 2024 21:43:32 +0800 Subject: [PATCH] feat(linter): eslint/no-await-in-loop (#3070) [no-await-in-loop](https://eslint.org/docs/latest/rules/no-await-in-loop) --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/eslint/no_await_in_loop.rs | 235 ++++++++++++++++++ .../src/snapshots/no_await_in_loop.snap | 87 +++++++ 3 files changed, 324 insertions(+) create mode 100644 crates/oxc_linter/src/rules/eslint/no_await_in_loop.rs create mode 100644 crates/oxc_linter/src/snapshots/no_await_in_loop.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index bec4bf2b2..6349be573 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -48,6 +48,7 @@ mod eslint { pub mod max_params; pub mod no_array_constructor; pub mod no_async_promise_executor; + pub mod no_await_in_loop; pub mod no_bitwise; pub mod no_caller; pub mod no_case_declarations; @@ -462,6 +463,7 @@ oxc_macros::declare_all_lint_rules! { eslint::require_yield, eslint::use_isnan, eslint::valid_typeof, + eslint::no_await_in_loop, typescript::adjacent_overload_signatures, typescript::array_type, typescript::ban_ts_comment, diff --git a/crates/oxc_linter/src/rules/eslint/no_await_in_loop.rs b/crates/oxc_linter/src/rules/eslint/no_await_in_loop.rs new file mode 100644 index 000000000..a573a29ac --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_await_in_loop.rs @@ -0,0 +1,235 @@ +use oxc_ast::{ + ast::{Expression, Statement}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(no-await-in-loop): Unexpected `await` inside a loop.")] +#[diagnostic(severity(warning))] +struct NoAwaitInLoopDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoAwaitInLoop; + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule disallows the use of await within loop bodies. (for, for-in, for-of, while, do-while). + /// + /// ### Why is this bad? + /// + /// It potentially indicates that the async operations are not being effectively parallelized. + /// Instead, they are being run in series, which can lead to poorer performance. + /// + /// ### Example + /// Bad: + /// ```javascript + /// for (const user of users) { + /// const userRecord = await getUserRecord(user); + /// } + /// ``` + /// + /// Good: + /// ```javascript + /// await Promise.all(users.map(user => getUserRecord(user))); + /// ``` + NoAwaitInLoop, + perf +); + +impl Rule for NoAwaitInLoop { + fn run(&self, node: &AstNode, ctx: &LintContext) { + // if node is AwaitExpression or AwaitForOfStatement + let span = match node.kind() { + // if the await attr of ForOfStatement is false, return + AstKind::ForOfStatement(for_of_stmt) => { + if !for_of_stmt.r#await { + return; + } + + // only highlight the 'await' keyword + Span::new(for_of_stmt.span.start + 4, for_of_stmt.span.start + 9) + } + // only highlight the 'await' keyword + AstKind::AwaitExpression(expr) => Span::new(expr.span.start, expr.span.start + 5), + // other node type, return + _ => return, + }; + + let nodes = ctx.semantic().nodes(); + // Perform validation for AwaitExpression and ForOfStatement that contains await + let mut parent_node = nodes.parent_node(node.id()); + let mut is_in_loop = false; + while let Some(parent) = parent_node { + // Check if the current node is the boundary of the loop + if Self::is_boundary(parent) { + break; + } + + // if AwaitExpression or AwaitForOfStatement are in loop, break and report error + if Self::is_looped(span, parent) { + is_in_loop = true; + break; + } + + parent_node = nodes.parent_node(parent.id()); + } + + if is_in_loop { + ctx.diagnostic(NoAwaitInLoopDiagnostic(span)); + } + } +} + +impl NoAwaitInLoop { + fn node_matches_stmt_span(span: Span, stmt: &Statement) -> bool { + match stmt { + Statement::BlockStatement(block) => Self::include_span(block.span, span), + Statement::ExpressionStatement(expr_statement) => { + Self::include_span(expr_statement.span, span) + } + _ => false, + } + } + + fn node_matches_expr_span(span: Span, expr: &Expression) -> bool { + match expr { + Expression::TemplateLiteral(expr) => Self::include_span(expr.span, span), + Expression::ArrayExpression(expr) => Self::include_span(expr.span, span), + Expression::ArrowFunctionExpression(expr) => Self::include_span(expr.span, span), + Expression::AssignmentExpression(expr) => Self::include_span(expr.span, span), + Expression::AwaitExpression(expr) => Self::include_span(expr.span, span), + Expression::BinaryExpression(expr) => Self::include_span(expr.span, span), + Expression::CallExpression(expr) => Self::include_span(expr.span, span), + Expression::ChainExpression(expr) => Self::include_span(expr.span, span), + Expression::ClassExpression(expr) => Self::include_span(expr.span, span), + Expression::ConditionalExpression(expr) => Self::include_span(expr.span, span), + Expression::FunctionExpression(expr) => Self::include_span(expr.span, span), + Expression::ImportExpression(expr) => Self::include_span(expr.span, span), + Expression::LogicalExpression(expr) => Self::include_span(expr.span, span), + Expression::NewExpression(expr) => Self::include_span(expr.span, span), + Expression::ObjectExpression(expr) => Self::include_span(expr.span, span), + Expression::ParenthesizedExpression(expr) => Self::include_span(expr.span, span), + Expression::SequenceExpression(expr) => Self::include_span(expr.span, span), + Expression::TaggedTemplateExpression(expr) => Self::include_span(expr.span, span), + Expression::ThisExpression(expr) => Self::include_span(expr.span, span), + Expression::UnaryExpression(expr) => Self::include_span(expr.span, span), + Expression::UpdateExpression(expr) => Self::include_span(expr.span, span), + Expression::YieldExpression(expr) => Self::include_span(expr.span, span), + Expression::PrivateInExpression(expr) => Self::include_span(expr.span, span), + _ => false, + } + } + + fn is_looped(span: Span, parent: &AstNode) -> bool { + match parent.kind() { + AstKind::ForStatement(stmt) => { + let mut result = Self::node_matches_stmt_span(span, &stmt.body); + if result { + return result; + } + + if let Some(test) = &stmt.test { + result = Self::node_matches_expr_span(span, test); + if result { + return result; + } + } + + if let Some(update) = &stmt.update { + result = Self::node_matches_expr_span(span, update); + } + + result + } + AstKind::ForInStatement(stmt) => Self::node_matches_stmt_span(span, &stmt.body), + AstKind::ForOfStatement(stmt) => Self::node_matches_stmt_span(span, &stmt.body), + AstKind::WhileStatement(stmt) => { + Self::node_matches_stmt_span(span, &stmt.body) + || Self::node_matches_expr_span(span, &stmt.test) + } + AstKind::DoWhileStatement(stmt) => { + Self::node_matches_stmt_span(span, &stmt.body) + || Self::node_matches_expr_span(span, &stmt.test) + } + _ => false, + } + } + + // is span1 include span2 + fn include_span(span1: Span, span2: Span) -> bool { + span1.start <= span2.start && span1.end >= span2.end + } + + fn is_boundary(node: &AstNode) -> bool { + match node.kind() { + AstKind::Function(func) => func.is_declaration() || func.is_expression(), + AstKind::ArrowFunctionExpression(_) => true, + AstKind::ForOfStatement(for_of_stmt) => for_of_stmt.r#await, + _ => false, + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "async function foo() { await bar; }", + "async function foo() { for (var bar in await baz) { } }", + "async function foo() { for (var bar of await baz) { } }", + "async function foo() { for await (var bar of await baz) { } }", + "async function foo() { for (var bar = await baz in qux) {} }", + // While loops + "async function foo() { while (true) { async function foo() { await bar; } } }", // Blocked by a function declaration + // For loops + "async function foo() { for (var i = await bar; i < n; i++) { } }", + // Do while loops + "async function foo() { do { } while (bar); }", + // Blocked by a function expression + "async function foo() { while (true) { var y = async function() { await bar; } } }", + // Blocked by an arrow function + "async function foo() { while (true) { var y = async () => await foo; } }", + "async function foo() { while (true) { var y = async () => { await foo; } } }", + // Blocked by a class method + "async function foo() { while (true) { class Foo { async foo() { await bar; } } } }", + // Asynchronous iteration intentionally + "async function foo() { for await (var x of xs) { await f(x) } }", + ]; + + let fail = vec![ + // While loops + "async function foo() { while (baz) { await bar; } }", + "async function foo() { while (await foo()) { } }", + "async function foo() { while (baz) { for await (x of xs); } }", + // For of loops + "async function foo() { for (var bar of baz) { await bar; } }", + "async function foo() { for (var bar of baz) await bar; }", + // For in loops + "async function foo() { for (var bar in baz) { await bar; } }", + // For loops + "async function foo() { for (var i; i < n; i++) { await bar; } }", + "async function foo() { for (var i; await foo(i); i++) { } }", + "async function foo() { for (var i; i < n; i = await bar) { } }", + // Do while loops + "async function foo() { do { await bar; } while (baz); }", + "async function foo() { do { } while (await bar); }", + // Deep in a loop body + "async function foo() { while (true) { if (bar) { foo(await bar); } } }", + // Deep in a loop condition + "async function foo() { while (xyz || 5 > await x) { } }", + // In a nested loop of for-await-of + "async function foo() { for await (var x of xs) { while (1) await f(x) } }", + ]; + + Tester::new(NoAwaitInLoop::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_await_in_loop.snap b/crates/oxc_linter/src/snapshots/no_await_in_loop.snap new file mode 100644 index 000000000..ef8f7e02c --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_await_in_loop.snap @@ -0,0 +1,87 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_await_in_loop +--- + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:38] + 1 │ async function foo() { while (baz) { await bar; } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:31] + 1 │ async function foo() { while (await foo()) { } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:42] + 1 │ async function foo() { while (baz) { for await (x of xs); } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:47] + 1 │ async function foo() { for (var bar of baz) { await bar; } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:45] + 1 │ async function foo() { for (var bar of baz) await bar; } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:47] + 1 │ async function foo() { for (var bar in baz) { await bar; } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:50] + 1 │ async function foo() { for (var i; i < n; i++) { await bar; } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:36] + 1 │ async function foo() { for (var i; await foo(i); i++) { } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:47] + 1 │ async function foo() { for (var i; i < n; i = await bar) { } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:29] + 1 │ async function foo() { do { await bar; } while (baz); } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:38] + 1 │ async function foo() { do { } while (await bar); } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:54] + 1 │ async function foo() { while (true) { if (bar) { foo(await bar); } } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:42] + 1 │ async function foo() { while (xyz || 5 > await x) { } } + · ───── + ╰──── + + ⚠ eslint(no-await-in-loop): Unexpected `await` inside a loop. + ╭─[no_await_in_loop.tsx:1:60] + 1 │ async function foo() { for await (var x of xs) { while (1) await f(x) } } + · ───── + ╰────