mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(codegen): parenthesize let/async for-of head target (#23008)
## What
A `for...of` head may not start with the `let` token, nor be the bare identifier `async` (ECMA-262: `for ( [lookahead ∉ { let, async of }] LeftHandSideExpression of ...`). Such a head must be parenthesized.
## Approach
The wrap is decided **inside the for-of head printing** (`ForOfStatement::gen`), so the hot `IdentifierReference` path stays untouched. A small structural check determines whether the emitted head would start with `let`, mirroring codegen's output decisions:
- member / TS-wrapper / parenthesized expressions are walked to their leftmost object (`let.x`, `let.x[0]`, `(let).x`)
- `let[...]` already self-wraps to `(let)[...]`, so it is not double-wrapped
A bare non-`await` `async` head is handled with a direct check. `for...in` heads are never wrapped.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `for ((let) of x)` | `for (let of x)` ❌ | `for ((let) of x)` ✅ |
| `for ((let.x) of x)` | `for (let.x of x)` ❌ | `for ((let.x) of x)` ✅ |
| `for (((let).x) of x)` | `for (let.x of x)` ❌ | `for ((let.x) of x)` ✅ |
| `for ((let[0]) of x)` | `for (let[0] of x)` ❌ | `for ((let)[0] of x)` ✅ |
| `for ((async) of x)` | `for ((async) of x)` | `for ((async) of x)` |
| `for (async in x)` | `for ((async) in x)` | `for (async in x)` ✅ |
All outputs verified with `node --check`.
This commit is contained in:
2 files changed
+64
-6
No files matched your search
@@ -362,6 +362,43 @@ impl Gen for ForInStatement<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the leading token a `for...of` head would emit is `let`, which is
|
||||
/// forbidden unparenthesized (`for ((let.x) of y)`).
|
||||
fn for_of_head_starts_with_let(left: &ForStatementLeft) -> bool {
|
||||
match left {
|
||||
ForStatementLeft::AssignmentTargetIdentifier(id) => id.name == "let",
|
||||
ForStatementLeft::ComputedMemberExpression(m) => computed_object_starts_with_let(m),
|
||||
ForStatementLeft::StaticMemberExpression(m) => expr_starts_with_let(&m.object),
|
||||
ForStatementLeft::PrivateFieldExpression(m) => expr_starts_with_let(&m.object),
|
||||
ForStatementLeft::TSAsExpression(e) => expr_starts_with_let(&e.expression),
|
||||
ForStatementLeft::TSSatisfiesExpression(e) => expr_starts_with_let(&e.expression),
|
||||
ForStatementLeft::TSNonNullExpression(e) => expr_starts_with_let(&e.expression),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn expr_starts_with_let(expr: &Expression) -> bool {
|
||||
match expr {
|
||||
Expression::Identifier(id) => id.name == "let",
|
||||
Expression::ComputedMemberExpression(m) => computed_object_starts_with_let(m),
|
||||
Expression::StaticMemberExpression(m) => expr_starts_with_let(&m.object),
|
||||
Expression::PrivateFieldExpression(m) => expr_starts_with_let(&m.object),
|
||||
Expression::TSAsExpression(e) => expr_starts_with_let(&e.expression),
|
||||
Expression::TSSatisfiesExpression(e) => expr_starts_with_let(&e.expression),
|
||||
Expression::TSNonNullExpression(e) => expr_starts_with_let(&e.expression),
|
||||
// Codegen drops redundant parens around a `let` object (`(let).x` -> `let.x`).
|
||||
Expression::ParenthesizedExpression(e) => expr_starts_with_let(&e.expression),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `let[...]` is emitted as `(let)[...]` (the computed member wraps a bare `let`
|
||||
/// object), so it no longer starts with `let`.
|
||||
fn computed_object_starts_with_let(m: &ComputedMemberExpression) -> bool {
|
||||
!m.object.get_identifier_reference().is_some_and(|r| r.name == "let")
|
||||
&& expr_starts_with_let(&m.object)
|
||||
}
|
||||
|
||||
impl Gen for ForOfStatement<'_> {
|
||||
fn r#gen(&self, p: &mut Codegen, ctx: Context) {
|
||||
p.print_comments_at(self.span.start);
|
||||
@@ -374,7 +411,12 @@ impl Gen for ForOfStatement<'_> {
|
||||
}
|
||||
p.print_soft_space();
|
||||
p.print_ascii_byte(b'(');
|
||||
self.left.print(p, ctx);
|
||||
// A `for...of` head may not start with the `let` token (`for ((let.x) of y)`),
|
||||
// nor be the bare identifier `async` (`for ((async) of y)`, unless `await`).
|
||||
let wrap = for_of_head_starts_with_let(&self.left)
|
||||
|| (!self.r#await
|
||||
&& matches!(&self.left, ForStatementLeft::AssignmentTargetIdentifier(id) if id.name == "async"));
|
||||
p.wrap(wrap, |p| self.left.print(p, ctx));
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("of");
|
||||
@@ -398,12 +440,8 @@ impl Gen for ForStatementLeft<'_> {
|
||||
fn r#gen(&self, p: &mut Codegen, ctx: Context) {
|
||||
match self {
|
||||
ForStatementLeft::VariableDeclaration(var) => var.print(p, ctx),
|
||||
ForStatementLeft::AssignmentTargetIdentifier(identifier) => {
|
||||
let wrap = identifier.name == "async";
|
||||
p.wrap(wrap, |p| self.to_assignment_target().print(p, ctx));
|
||||
}
|
||||
match_assignment_target!(ForStatementLeft) => {
|
||||
p.wrap(false, |p| self.to_assignment_target().print(p, ctx));
|
||||
self.to_assignment_target().print(p, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,26 @@ fn for_stmt() {
|
||||
"for (var a = 1 || (2 in {}) in { x: 1 }) count++;",
|
||||
"for (var a = 1 || (2 in {}) in { x: 1 }) count++;\n",
|
||||
);
|
||||
|
||||
// A `for...of` head may not start with `let` or `async`; `for...in` may.
|
||||
test("for ((let) of x);", "for ((let) of x);\n");
|
||||
test("for ((async) of x);", "for ((async) of x);\n");
|
||||
test("for (let in x);", "for (let in x);\n");
|
||||
test("for (async in x);", "for (async in x);\n");
|
||||
// A for-of head whose leading token is `let` is wrapped wherever it appears,
|
||||
// not only as a bare identifier; a bare `async` is wrapped but `async.x` is fine.
|
||||
test("for ((let.x) of x);", "for ((let.x) of x);\n");
|
||||
test("for ((let.x[0]) of x);", "for ((let.x[0]) of x);\n");
|
||||
test("for ((let[0]) of x);", "for ((let)[0] of x);\n");
|
||||
test("for ((async.x) of x);", "for (async.x of x);\n");
|
||||
test("for (((let).x) of x);", "for ((let.x) of x);\n");
|
||||
// A leading `let` outside a for-of head is untouched.
|
||||
test("let.x;", "let.x;\n");
|
||||
// A nested for-of inside a `for` init is still wrapped correctly.
|
||||
test(
|
||||
"for (function f(){ for ((async) of xs); };;);",
|
||||
"for (function f() {\n\tfor ((async) of xs);\n};;);\n",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user