fix(codegen): drop redundant space after else when minifying (#23025)

## What

Found with the whitespace analyzer (remove each space in minified output, check it still canonicalizes to the same AST). `print_body` printed an unconditional hard space before a single-statement body, so an `else` body kept a space even before non-identifier tokens:

| Input | Before | After |
| --- | --- | --- |
| ``if(x)a();else`t` `` | ``else `t` `` | ``else`t` `` |
| `if(x)a();else++b` | `else ++b` | `else++b` |
| `if(x)a();else[b]` | `else [b]` | `else[b]` |

esbuild emits these tight too.

## Fix

The body statement's first token already inserts a hard space via `print_space_before_identifier` whenever it would merge with a preceding identifier char — this is how `for`/`while`/`do` bodies (which pass `need_space=false`) already work; they're safe because they end in `)`. `else` ends in `e`, so the unconditional space was added as a blunt guard.

Removing it and relying on the body's own guard keeps the required spaces (`else x`, `else return 5`, `else debugger`, `else var k`, `else 5`) while dropping the unnecessary ones. The `need_space` parameter is now unused and removed.

Verified: every keyword-statement else-body (`return`/`throw`/`debugger`/`var`/…) self-guards; full codegen (116) + minifier (506) suites pass, and a 150-file real-world minify round-trip is idempotent.
This commit is contained in:
Boshen committed 2026-06-06 15:59:34 +00:00
1 parent 324c8ae137
commit 62965ae72c
4 files changed
+25 -18

No files matched your search

+9 -9
View File
@@ -253,7 +253,7 @@ fn print_if(if_stmt: &IfStatement<'_>, p: &mut Codegen, ctx: Context) {
}
}
stmt => {
p.print_body(stmt, false, ctx);
p.print_body(stmt, ctx);
if if_stmt.alternate.is_some() {
p.print_indent();
}
@@ -273,7 +273,7 @@ fn print_if(if_stmt: &IfStatement<'_>, p: &mut Codegen, ctx: Context) {
p.print_hard_space();
print_if(if_stmt, p, ctx);
}
stmt => p.print_body(stmt, true, ctx),
stmt => p.print_body(stmt, ctx),
}
}
}
@@ -338,7 +338,7 @@ impl Gen for ForStatement<'_> {
}
p.print_ascii_byte(b')');
p.print_body(&self.body, false, ctx);
p.print_body(&self.body, ctx);
}
}
@@ -358,7 +358,7 @@ impl Gen for ForInStatement<'_> {
p.print_soft_space();
p.print_expression(&self.right);
p.print_ascii_byte(b')');
p.print_body(&self.body, false, ctx);
p.print_body(&self.body, ctx);
}
}
@@ -423,7 +423,7 @@ impl Gen for ForOfStatement<'_> {
p.print_soft_space();
self.right.print_expr(p, Precedence::Comma, Context::empty());
p.print_ascii_byte(b')');
p.print_body(&self.body, false, ctx);
p.print_body(&self.body, ctx);
}
}
@@ -458,7 +458,7 @@ impl Gen for WhileStatement<'_> {
p.print_ascii_byte(b'(');
p.print_expression(&self.test);
p.print_ascii_byte(b')');
p.print_body(&self.body, false, ctx);
p.print_body(&self.body, ctx);
}
}
@@ -576,7 +576,7 @@ impl Gen for SwitchCase<'_> {
let single_line = self.consequent.len() == 1
&& !p.has_legal_orphans_before(self.consequent[0].span().start);
if single_line {
p.print_body(&self.consequent[0], false, ctx);
p.print_body(&self.consequent[0], ctx);
return;
}
@@ -612,7 +612,7 @@ impl Gen for LabeledStatement<'_> {
p.add_source_mapping(self.span);
self.label.print(p, ctx);
p.print_colon();
p.print_body(&self.body, false, ctx);
p.print_body(&self.body, ctx);
}
}
@@ -688,7 +688,7 @@ impl Gen for WithStatement<'_> {
p.print_ascii_byte(b'(');
p.print_expression(&self.object);
p.print_ascii_byte(b')');
p.print_body(&self.body, false, ctx);
p.print_body(&self.body, ctx);
}
}
+5 -4
View File
@@ -630,7 +630,7 @@ impl<'a> Codegen<'a> {
self.print_ascii_byte(b'}');
}
fn print_body(&mut self, stmt: &Statement<'_>, need_space: bool, ctx: Context) {
fn print_body(&mut self, stmt: &Statement<'_>, ctx: Context) {
match stmt {
Statement::BlockStatement(stmt) => {
self.print_soft_space();
@@ -642,9 +642,10 @@ impl<'a> Codegen<'a> {
self.print_soft_newline();
}
stmt => {
if need_space && self.options.minify {
self.print_hard_space();
}
// The statement's first token inserts a hard space itself (via
// `print_space_before_identifier`) when it would merge with a preceding
// identifier char (e.g. `else`), so `else{...}`/`else++x`/`else[0]` stay
// tight while `else x` keeps its space.
self.print_next_indent_as_space = true;
stmt.print(self, ctx);
}
@@ -219,6 +219,12 @@ fn if_stmt() {
"function f() { if (foo) return foo; else if (bar) return foo; }",
"function f(){if(foo)return foo;else if(bar)return foo}",
);
// `else` only needs a space before an identifier-like body.
test_minify("if(x)a();else b()", "if(x)a();else b();");
test_minify("if(x)a();else++b", "if(x)a();else++b;");
test_minify("if(x)a();else[b]", "if(x)a();else[b];");
test_minify("if(x)a();else`b`", "if(x)a();else`b`;");
test_minify("if(x)a();else debugger", "if(x)a();else debugger;");
}
#[test]
+5 -5
View File
@@ -7,21 +7,21 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
287.63 kB | 89.26 kB | 90.07 kB | 30.91 kB | 31.95 kB | 2 | jquery.js
342.15 kB | 116.98 kB | 118.14 kB | 43.18 kB | 44.37 kB | 2 | vue.js
342.15 kB | 116.97 kB | 118.14 kB | 43.18 kB | 44.37 kB | 2 | vue.js
544.10 kB | 71.04 kB | 72.48 kB | 25.77 kB | 26.20 kB | 2 | lodash.js
555.77 kB | 267.40 kB | 270.13 kB | 88.00 kB | 90.80 kB | 2 | d3.js
555.77 kB | 267.40 kB | 270.13 kB | 88.01 kB | 90.80 kB | 2 | d3.js
1.01 MB | 439.35 kB | 458.89 kB | 122.05 kB | 126.71 kB | 2 | bundle.min.js
1.25 MB | 642.65 kB | 646.76 kB | 159.41 kB | 163.73 kB | 2 | three.js
2.14 MB | 711.10 kB | 724.14 kB | 160.43 kB | 181.07 kB | 2 | victory.js
2.14 MB | 711.10 kB | 724.14 kB | 160.44 kB | 181.07 kB | 2 | victory.js
3.20 MB | 1.00 MB | 1.01 MB | 322.61 kB | 331.56 kB | 2 | echarts.js
6.69 MB | 2.22 MB | 2.31 MB | 458.46 kB | 488.28 kB | 4 | antd.js
6.69 MB | 2.22 MB | 2.31 MB | 458.45 kB | 488.28 kB | 4 | antd.js
10.95 MB | 3.33 MB | 3.49 MB | 853.35 kB | 915.50 kB | 4 | typescript.js
10.95 MB | 3.33 MB | 3.49 MB | 853.33 kB | 915.50 kB | 4 | typescript.js