fix(codegen): preserve in restriction through yield arguments (#26413)

## Problem

A `yield` expression in a classic `for` loop initializer can lose the parentheses required around an `in` operator in its argument. This turns valid JavaScript into output that cannot be parsed, in both default and minified code generation.

For example, this input is valid:

```js
function *g(o) {
  for (yield (1 in o); false;);
}
```

Previously, codegen emitted:

```js
// Default output
function* g(o) {
  for (yield 1 in o; false;);
}

// Minified output
function*g(o){for(yield 1 in o;false;);}
```

The unparenthesized `in` is not allowed at this position in a classic `for` initializer. The same restriction applies to the argument of `yield*`, and can also be lost through nested yields or an assignment inside the yield argument.

## Change

`ForStatement` already prints its initializer with `Context::FORBID_IN`. However, `YieldExpression::gen_expr` ignored its incoming context and always printed its argument with `Context::empty()`.

Preserve `FORBID_IN` when printing the argument of an unwrapped yield expression. This lets the existing expression printers retain the required grouping:

```js
// Default output
function* g(o) {
  for (yield (1 in o); false;);
}

// Minified output
function*g(o){for(yield(1 in o);false;);}
```

Only the relevant `FORBID_IN` flag is forwarded; the argument's existing precedence remains `Precedence::Yield`. Both `yield` and `yield*` use this path.

## Parentheses around yield

When precedence requires parentheses around the entire yield expression, those parentheses already allow `in` in the argument. Clear the restriction in that case to avoid redundant inner grouping:

```js
// Input
function *g(o) {
  for ((yield (1 in o)) + 1; false;);
}

// Default output
function* g(o) {
  for ((yield 1 in o) + 1; false;);
}

// Minified output
function*g(o){for((yield 1 in o)+1;false;);}
```

Outside a restricted initializer, ordinary yield expressions continue to omit unnecessary parentheses: `yield (1 in o)` prints as `yield 1 in o`.
This commit is contained in:
camc314
2026-09-07 17:16:17 +00:00
parent 42ac916b21
commit ae6c386ed5
2 changed files with 52 additions and 3 deletions
+5 -3
View File
@@ -1910,8 +1910,10 @@ impl GenExpr for ArrowFunctionExpression<'_> {
}
impl GenExpr for YieldExpression<'_> {
fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, _ctx: Context) {
p.wrap(precedence >= Precedence::Assign, |p| {
fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) {
let wrap = precedence >= Precedence::Assign;
let argument_ctx = if wrap { Context::empty() } else { ctx & Context::FORBID_IN };
p.wrap(wrap, |p| {
p.print_space_before_identifier();
p.add_source_mapping(self.span);
p.print_str("yield");
@@ -1920,7 +1922,7 @@ impl GenExpr for YieldExpression<'_> {
}
if let Some(argument) = self.argument.as_ref() {
p.print_soft_space();
argument.print_expr(p, Precedence::Yield, Context::empty());
argument.print_expr(p, Precedence::Yield, argument_ctx);
}
});
}
@@ -686,6 +686,53 @@ fn in_expr_in_sequence_in_for_loop_init() {
);
}
#[test]
fn in_expr_in_yield_expression() {
for (keyword, prefix) in [("yield", "yield "), ("yield*", "yield*")] {
for (init, expected, minified) in [
(
format!("{keyword} (1 in o)"),
format!("{keyword} (1 in o)"),
format!("{keyword}(1 in o)"),
),
(
format!("x = {keyword} (1 in o)"),
format!("x = {keyword} (1 in o)"),
format!("x={keyword}(1 in o)"),
),
(
format!("{keyword} yield (1 in o)"),
format!("{keyword} yield (1 in o)"),
format!("{prefix}yield(1 in o)"),
),
(
format!("{keyword} (x = (1 in o))"),
format!("{keyword} x = (1 in o)"),
format!("{prefix}x=(1 in o)"),
),
// Parentheses around the yield expression allow `in` in its argument.
(
format!("({keyword} (1 in o)) + 1"),
format!("({keyword} 1 in o) + 1"),
format!("({prefix}1 in o)+1"),
),
] {
let source = format!("function *g(o) {{ for ({init}; false;); }}");
test(&source, &format!("function* g(o) {{\n\tfor ({expected}; false;);\n}}\n"));
test_minify(&source, &format!("function*g(o){{for({minified};false;);}}"));
crate::test_idempotency(&source);
crate::test_idempotency_options(
&source,
&CodegenOptions { minify: true, ..CodegenOptions::default() },
);
}
let source = format!("function *g(o) {{ {keyword} (1 in o); }}");
test(&source, &format!("function* g(o) {{\n\t{keyword} 1 in o;\n}}\n"));
test_minify(&source, &format!("function*g(o){{{prefix}1 in o}}"));
}
}
#[test]
fn in_expr_in_arrow_function_expression() {
test("() => ('foo' in bar)", "() => \"foo\" in bar;\n");