From 40e47d6111fcd85fb13be05de56c3d788192228e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 24 May 2026 09:02:48 +0000 Subject: [PATCH 1/2] Parenthesize `async` when it starts a for-of loop initializer The for-of grammar forbids the initializer from starting with the token sequence `async of`, so an identifier spelled `\u0061sync` (or an already-parenthesized `(async)`) must not be printed as a bare `async` there. Wrap it in parentheses, mirroring the existing `let` handling. --- src/js_printer/lib.rs | 22 +++++++++++++++------ test/bundler/transpiler/transpiler.test.js | 23 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index e5868686642..74dde0b5c51 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1524,6 +1524,7 @@ pub enum ExprFlag { ForbidIn, HasNonOptionalChainParent, ExprResultIsUnused, + IsFollowedByOf, } pub type ExprFlagSet = enumset::EnumSet; @@ -1546,6 +1547,10 @@ impl ExprFlag { pub fn expr_result_is_unused() -> ExprFlagSet { ExprFlag::ExprResultIsUnused.into() } + #[inline] + pub fn is_followed_by_of() -> ExprFlagSet { + ExprFlag::IsFollowedByOf.into() + } } #[derive(Clone, Copy, PartialEq, Eq)] @@ -4255,7 +4260,12 @@ pub mod __gated_printer { } ExprData::EIdentifier(e) => { let name = self.name_for_symbol(e.ref_); - let wrap = self.writer.written() == self.for_of_init_start && name == b"let"; + // A for-of loop initializer must not start with the token "let" and + // must not be the exact token sequence "async of" (e.g. the escaped + // identifier in "for (\u0061sync of []) ;"), so wrap in parentheses. + let wrap = self.writer.written() == self.for_of_init_start + && (name == b"let" + || (name == b"async" && flags.contains(ExprFlag::IsFollowedByOf))); if wrap { self.print(b"("); @@ -5808,7 +5818,7 @@ pub mod __gated_printer { self.print(b"for"); self.print_space(); self.print(b"("); - self.print_for_loop_init(s.init); + self.print_for_loop_init(s.init, ExprFlag::none()); self.print_space(); self.print_space_before_identifier(); self.print(b"in"); @@ -5828,7 +5838,7 @@ pub mod __gated_printer { self.print_space(); self.print(b"("); self.for_of_init_start = self.writer.written(); - self.print_for_loop_init(s.init); + self.print_for_loop_init(s.init, ExprFlag::is_followed_by_of()); self.print_space(); self.print_space_before_identifier(); self.print(b"of"); @@ -5914,7 +5924,7 @@ pub mod __gated_printer { self.print(b"("); if let Some(init_) = &s.init { - self.print_for_loop_init(*init_); + self.print_for_loop_init(*init_, ExprFlag::none()); } self.print(b";"); @@ -6601,13 +6611,13 @@ pub mod __gated_printer { self.print(b", enumerable: true, configurable: true})"); } - pub fn print_for_loop_init(&mut self, init_st: Stmt) { + pub fn print_for_loop_init(&mut self, init_st: Stmt, extra_flags: ExprFlagSet) { match &init_st.data { StmtData::SExpr(s) => { self.print_expr( s.value, Level::Lowest, - ExprFlag::ForbidIn | ExprFlag::ExprResultIsUnused, + ExprFlag::ForbidIn | ExprFlag::ExprResultIsUnused | extra_flags, ); } StmtData::SLocal(s) => { diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 1faa1b04f2b..d9a52bc8ab8 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2052,6 +2052,29 @@ console.log(
);`), expectParseError("await -x ** 0", "Unexpected **"); }); + it("for-of loop variable named async", () => { + // "\u0061sync" is the identifier `async`, which is legal as a for-of loop + // variable, but printing it as the raw token sequence `async of` is a + // syntax error, so the printer must parenthesize it. + expectPrinted_("for (\\u0061sync of [7]);", "for ((async) of [7])\n ;\n"); + expectPrinted_("for ((async) of [7]);", "for ((async) of [7])\n ;\n"); + expectPrinted_( + "async function f() { for await (\\u0061sync of [7]); }", + "async function f() {\n for await ((async) of [7])\n ;\n}", + ); + + // The same identifier needs no parentheses when it is not directly followed by `of` + expectPrinted_("for (async.x of [7]);", "for (async.x of [7])\n ;\n"); + expectPrinted_("for (x[\\u0061sync] of [7]);", "for (x[async] of [7])\n ;\n"); + expectPrinted_("for (\\u0061sync in x);", "for (async in x)\n ;\n"); + + // `let` as a for-of loop variable keeps its parentheses too + expectPrinted_("for ((let) of [7]);", "for ((let) of [7])\n ;\n"); + + // The keyword spelling is a syntax error, which is why the parentheses matter + expect(() => parsed("for (async of [7]);", false, false)).toThrow(); + }); + it("await", () => { expectPrinted("await x", "await x"); expectPrinted("await +x", "await +x"); From b0a9218b927fba2fd3f9e6849beb440eeb0584d0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 24 May 2026 11:15:46 +0000 Subject: [PATCH 2/2] Don't forward IsFollowedByOf to index expression targets `for (\u0061sync[0] of x)` doesn't need parentheses around `async` because the grammar restriction only applies to the exact token sequence `async of`, matching the existing dot-expression behavior. --- src/js_printer/lib.rs | 2 ++ test/bundler/transpiler/transpiler.test.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 74dde0b5c51..f4da4050d92 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -3823,6 +3823,8 @@ pub mod __gated_printer { flags.remove(ExprFlag::HasNonOptionalChainParent); } + // The index target is not directly followed by `of`. + flags.remove(ExprFlag::IsFollowedByOf); self.print_expr(e.target, Level::Postfix, flags); let is_optional_chain_start = diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index d9a52bc8ab8..7666192ed86 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2065,6 +2065,7 @@ console.log(
);`), // The same identifier needs no parentheses when it is not directly followed by `of` expectPrinted_("for (async.x of [7]);", "for (async.x of [7])\n ;\n"); + expectPrinted_("for (\\u0061sync[0] of [7]);", "for (async[0] of [7])\n ;\n"); expectPrinted_("for (x[\\u0061sync] of [7]);", "for (x[async] of [7])\n ;\n"); expectPrinted_("for (\\u0061sync in x);", "for (async in x)\n ;\n");