Use `Ident` in the property mangler. This reduces the `Set::contains` operation faster as the hashing doesn't happen.
The property names are not always an "ident" so it may not make sense to use the `Ident` type. But currently the `Ident` type allows characters that are not allowed in identifiers so the code works.
I used AI to generate the code and then reviewed the code and iterated a bit.
adds new helper `merge_last_expression_into_sequence` and uses it
everywhere when we want to merge last expression to target expression
`a; b;` -> `a, b;`
this change avoids creation of 2 dummy nodes and unnecessary
`drop_statement` on dummy stmt per merge
`ParserReturn` contains a property `panicked`.
The name `panicked` is misleading. The parser did _not_ panic. When `true`, it means that the parser encountered a fatal error and is unable to parse the source, because it contains an unrecoverable syntax error.
Rename this property to `fatal_error`, to better describe what it means.
This is a breaking change, but one that's trivial to fix in downstream code - the property doesn't change its meaning or behavior, just its name.
All the changes in this PR in other crates are just reacting to this change and making everywhere use the term "fatal error" instead of "panicked", so the codebase is consistent.
`SequenceExpression` inlining from `minimize_conditional_after_if` is a
more optimal version of inlining when we are constructing conditional
expr, with this change we will do this always instead of falling to
`minimize_conditional_expression`
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
add test cases for uncovered code and branches that should be tested:
verified with llvm-cov
```sh
cargo +nightly llvm-cov clean --workspace && cargo +nightly llvm-cov --branch --html -p oxc_minifier --test mod
```
i found out that our coverage is not really ideal while working on
#26264
Use `Ident` instead of `Str` in `KeepVar`. This reduces the `Ident` -> `Str` -> `Ident` conversion.
I used AI to generate the code and then reviewed the code.
Use `IdentHashSet` in `PrivateMemberUsageStack`. This reduces the `Ident` -> `Str` conversion and also makes the `Set::contains` operation faster as the hashing doesn't happen.
I used AI to generate the code and then reviewed the code.
Technically a breaking change, but one that's extremely unlikely to affect any real-world applications.
On 64-bit systems, the maximum source text length was exactly `u32::MAX`.
Lower it by 256 bytes, for 2 reasons:
1. `oxc_lexer` will require source text to be followed by 64 bytes of padding. Reducing max source length ensures _padded_ length can always fit in a `u32`.
2. Counts of tokens, comments and errors are all bounded by source length plus a small constant. By reducing max source length, these can all be stored as `u32`s without any possibility of overflow.
`oxc_lexer` only needs 64 bytes of padding. Have gone with 256 to leave headroom in case later redesigns require more than 64. This avoids another breaking change later. We can always switch to 64 later on if we want, and this later relaxation would not be a breaking change.
All the changes to other crates in this PR are just updating comments which talk about max source length.
Updates the minifier documentation to use supported validation commands and fixes broken relative links. Also refreshes the coverage task README so it no longer advertises removed suite selectors.
## Summary
- Return `OutOfRange` when a `StringCharAt` position cannot fit in `usize`, avoiding a panic on 32-bit targets.
- Cover the `u32` boundary in direct evaluation and minifier constant folding, alongside existing conversion and UTF-16 edge cases.
improve negation of 1 and 0 to 0 and 1 in boolean context
previously only `!!0` and `!!1` could be inlined to `1` and `0` and now
`!0` and `!1` will also be inlined,
this change allows us to flip numeric values in-place instead of having
to first to create unary and remove it afterwards
```
!0 was converted before to 1 in some cases:
1. negate 1
2. !1 - add unary
3. 0 - remove unary
now:
1. negate 1
2. 0
```
expands leading assignment inlining into variable declarations for
`return`, `switch`, `if`, `throw`, `for`, `for in`, `for of`
```js
// return
var a; return a = b(), c;
var a = b(); return c;
// throw
var a; throw a = b(), c;
var a = b(); throw c;
// switch
var a; switch (a = b(), c) {}
var a = b(); switch (c) {}
// for
var a; for (a = b(), c; ;) d;
var a = b(); for (c; ;) d;
// if
var a; if (a = b(), c) d;
var a = b(); if (c) d;
```
fixes#25949
Expand negation to include move of `!` operator to inner children
```js
!(a == b && c == 3 && a)
a != b || c != 3 | !a
```
delta calculation is dependent on boolean context
- if we expect truthly/falsly value negation of `!a` will be converted
to `a` (-1 delta)
- in ambiguous context `!a` will be converted to `!!a` (+1 delta)
```js
a = !!(!b || !c);
// ^ ^ ^ -3
a = !(b || c)
// ^ ^ ^ +1 - if we could detect if parens can be removed it would be -1
// ^^^ ^^ -1 - if we would know that we can remove parens
```
## Summary
- Apply `ToString(undefined)` when `String.prototype.indexOf` or `lastIndexOf` is called without a search argument.
- Keep folding strings without the resulting `"undefined"` substring to `-1`.
- Add focused helper and minifier regression coverage.
```js
"undefined".indexOf(); // 0
"undefined".lastIndexOf(); // 0
"abcdef".indexOf(); // -1
"abcdef".lastIndexOf(); // -1
```
## Summary
`StringToNumber` previously removed ECMAScript whitespace only from the beginning of a string. Legal trailing whitespace therefore remained in the value passed to Rust’s numeric parsers, causing valid JavaScript numeric strings to produce `NaN`.
For example:
```js
"1 " * 2 // incorrectly folded to NaN
"0x10 " - 0 // incorrectly folded to NaN
```
This changes `trim_start_matches` to `trim_matches`, applying the existing ECMAScript whitespace predicate to both ends of the string.
After the change:
```js
"1 " * 2 // folds to 2
"0x10 " - 0 // folds to 16
```
The custom predicate remains important because ECMAScript and Rust define whitespace differently. Tests cover every ECMAScript whitespace character and verify that non-ECMAScript whitespace such as U+0085 is not accepted.
Move removal of dead code from switch stmts to remove_dead_code, this
change enables processing of switch stmts whose parent is not list of
stmts
current minification processes stmts whose parents are block stmts and
they are last stmt of set block, this change also allows us to remove
empty blocks and last break from switch cases whose parent is not block
stmt
```js
if (a) { b(); if (c) switch (d) { case 1: case 2: break; } c(); }
// ^~~~~~ due to body not being a block we where not able to process this
// Current:
if (a) { if (b(), c) switch (d) { case 1: case 2: break } c() }
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ this switch case is not being processed
// This branch:
a && (b(), c && d, c());
// ^ switch cases can be correctly processed
```
note: this change applies only dce, and dosn't substitute switch stmts
with if'stmts
#### changes in stack:
- https://github.com/oxc-project/oxc/pull/25405
- https://github.com/oxc-project/oxc/pull/25403👈
- `main`
```js
for (var i = 0; i++ < 1;) {
if (x) break;
else function f() {}
f = 0;
}
console.log(typeof f, f);
```
was compressed to
```js
for (var i = 0; i++ < 1 && !x;) {
function f() {}
f = 0;
}
console.log(typeof f, f);
```
But this is wrong in sloppy mode. The output changes `number 0` / `undefined undefined` to `function f() {}`.
This PR fixes that.
I used AI to generate the code and then reviewed the code and added tests.
I started reviewing `minimize_conditional_expression` and looks like
there is alot of wasted effort there, esp, when we are re-creating
`ConditionalExpression` recursively by calling `minimize_conditional`,
for now i decided to only optimize `!==` and `!`, but folding of
`SequenceExpression` and creation of all new
`Expression::new_conditional_expression` could be removed as we should
just mutate current expression
`substitute_unary_plus` removes a `+` when the enclosing binary operator
will do the `ToNumber` conversion anyway. The existing doc comment
reasons carefully about `ToNumber` vs `ToNumeric`, but not about *when*
each runs.
`+a` converts `a` while evaluating `+a`, before the right operand is
evaluated at all. `a - n` converts it in
`ApplyStringOrNumericBinaryOperator`, after `n` has been evaluated.
Dropping the `+` therefore moves the conversion across `n`, and a right
operand with a side effect can change what the conversion sees:
```js
var xs = [];
console.log((+xs) - (xs.push(1), 0)); // 0, converted while `xs` was still empty
// oxc emitted:
console.log(xs - (xs.push(1), 0)); // 1, converted after `xs` grew
```
This PR requires the right operand to be free of side effects before
dropping a `+` in the left position. It cannot observe the conversion
either: under the "Coercion Methods Are Pure" assumption the conversion
runs no user code, so there is nothing to observe.
The right position (`n - +a`) is left as is: `n` is evaluated first
either way, and its `ToNumeric` is a no-op because the existing guard
already established that `n` is a number, so nothing runs between
evaluating `a` and converting it.
`cargo minsize` moves the typescript.js gzip figure down by 10 bytes;
every minified size is identical.
The reproduction was found by the minifier fuzzer (#25594) at seed
12397.
### Differential testing
Every case below was run through oxc and then executed in Node against
the unminified source, comparing the result, the mutated state, and the
error constructor *and message*.
| shape | cases | value worlds | runs |
| --- | ---: | ---: | ---: |
| operator × 37 hand-written right operands | 814 | 41 | 33,374 |
| right operand reading a closure variable the conversion may write |
374 | 41 | 15,334 |
| argument shape (`+a.v`, `+f()`, `+(seq(), a)`, `+(+a)`, getter) ×
nesting | 3,168 | 41 | 129,888 |
| random expressions, 3 seeds, depth 2-3 | 7,500 | 41 | 307,500 |
The worlds cover `[]`, `{ valueOf: () => 10n }`, `Symbol()`,
`Object.create(null)`, objects with getters, `Proxy`, and the numeric
edge cases.
No difference survives, other than one that predates this pass and is
discussed in the doc comment: for a `BigInt` argument both forms throw a
`TypeError`, but `+a` says "Cannot convert a BigInt value to a number"
while `a - 1` says "Cannot mix BigInt and other types". The error kind
never changes.
---
Written with AI assistance (Claude Code). Reviewed by me, and the
before/after behaviour was checked against Node.js.
---------
Signed-off-by: Kotaro Chikuba <miz404@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
add `boolean_context` to `minimize_not` so negation folding can happen
in-place when possible, reducing extra AST rebuilds and allocations and
`!0` / `!1` can now collapse directly to `1` / `0` in boolean context
An unused addition can be side-effect-free even when constant folding keeps it. This happens when replacing the expression with its exact constant value would make the output longer.
For example, constant folding correctly keeps `0.1 + 0.2` instead of expanding it to `0.30000000000000004`. But when the result is unused, the whole expression can be removed.
This PR checks the existing side-effect analysis before running the string-addition cleanup. It removes the whole addition when it is safe. Additions that can run code or throw still use the existing cleanup and are preserved when needed.
Tests cover both the removable numeric addition and an addition containing `foo()` that must stay.
Verified with the full `oxc_minifier` test suite (652 passed, 34 ignored), Clippy, formatting, minsize, and allocation checks. Minsize and allocation snapshots are unchanged.
AI-assisted by OpenAI Codex.
Statement transforms still use `take_in` after popping or consuming their parent nodes. Removing those dummies is less mechanical because references from discarded branches and pass mutations must remain accounted for.
Move the owned statement fields directly, centralize the shared return/throw conditional rewrite, and use explicit dropped-subtree or mutation reporting at each ownership boundary. The adjacent-`if` rewrite also uses the closure-based replacement API introduced at the bottom of this stack.
Relative to its parent, the tracked allocation fixtures record 2,039 fewer arena allocations and 31,552 fewer arena bytes. Across the complete stack, the improvement is 25,252 arena allocations and 404,352 arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
Several peephole transforms call `take_in` after removing or consuming the parent AST node. At that point there is no borrowed slot to preserve, so allocating a dummy replacement is unnecessary.
Move those already-owned child nodes directly in the straightforward expression and declaration cases. This second branch stays intentionally mechanical and leaves the statement/reference-bookkeeping cases to the final PR in the stack.
Relative to its parent, the tracked allocation fixtures record 22,957 fewer arena allocations and 370,544 fewer arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
`replace_expression` currently makes callers extract the old value before constructing its replacement, leaving an arena-backed dummy node behind.
Add a closure-based replacement path that owns the old expression and records the AST mutation. Its dropped-subtree contract remains explicit: callers report discarded subtrees, while values moved into the replacement stay live. Convert the logical-not unwrapping sites whose discarded wrappers contain no references.
This is the foundation of a three-PR Graphite stack. The tracked allocation fixtures record 256 fewer arena allocations and 2,256 fewer arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
Refactors `try_minimize_if` to improve clarity:
- restructures the function around `alternate` presence rather than
`consequent` type
- removes a recursive `Self::try_minimize_if` call — the fallthrough
after branch-flip achieves the same result
- this also removes need for code duplication for inlining expressions
to `||` or `&&`
- `if (!a) {} else b();` → `a && b();`
- `if (!a) b();` → `a || b();`
- `if (a) {} else b();` → `a || b();`
- `if (a) b();` → `a && b();`
- we also do not have to always call for `wrap_to_avoid_ambiguous_else`
and we can do that after we update code (eg, after rotation, and never
when we convert it to expression)
----------------------
#### changes in stack:
- https://github.com/oxc-project/oxc/pull/25267
- https://github.com/oxc-project/oxc/pull/25178👈
- `main`
`@__KEY__` and `#__KEY__` are currently treated as normal comments. Codegen can drop them before a later property-mangling pass. The property may then be renamed while the annotated string keeps its old name.
This classifies the marker as a property-key annotation. Codegen keeps it next to string and no-substitution template literals when annotation comments are enabled, and the property mangler reads that classification instead of scanning source text. Final output still follows the normal comment options, so whitespace-minified or annotation-disabled output removes the marker.
## Example
Input:
```js
const object = { _field: 1 };
const key = /* #__KEY__ */ "_field";
object[key];
```
Before, an intermediate codegen pass could remove the marker:
```js
const object = { _field: 1 };
const key = "_field";
object[key];
```
After, the marker survives for the later property-mangling pass.
Verified with the affected crate suites, `just minsize`, workspace lint, docs, AST generation, and independent review. `just ready` is only blocked locally by existing Node-version and terminal-color snapshot differences.
Implemented with AI assistance (OpenAI Codex). I reviewed and understand the changes and remain responsible for this contribution under the repository AI usage policy.
Oxc can shorten variable names and private class names, but it cannot shorten property names such as `obj._field`. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files.
### Feature
- `include` selects names to mangle. `exclude` and `reserved` keep names unchanged.
- The cache can give a property a fixed output name or keep it unchanged.
- The mangler supports member access, object and class keys, destructuring, optional chains, the left side of `in`, and JSX property names.
- `mangle_quoted` also handles strings and template literals used directly as property keys. `/* @__KEY__ */` and `/* #__KEY__ */` can mark one other string or template literal as a property key.
- Short names are assigned in a stable order. More common properties get shorter names. Existing and reserved names are avoided.
- Bundlers can collect names from several programs, create one shared map, and apply it to every program.
- Numeric keys, `__proto__`, `constructor`, `prototype`, TypeScript-only keys, and private names are never changed by this pass.
### Design
Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in `oxc_minifier`. It only reuses the short-name generator from `oxc_mangler`.
For one program, the order is:
```text
collect names → assign names → rewrite once → compress → mangle variables and private names
```
Property names are rewritten before compression because compression can remove information about quotes and comments.
For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options.
The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls.
### Example
With `include: "^_"` and `mangle_quoted: true`:
```js
obj[`_field`] = 1;
obj._field = 2;
```
becomes:
```js
obj[`e`] = 1;
obj.e = 2;
```
### Limitation
- Property mangling is unsafe when other code still uses the old property name. Public APIs and names shared with external code, host APIs, imported namespace objects, or separate builds must be excluded or reserved.
- Quote handling is per use. Without `mangle_quoted`, `obj._field` may change while `obj["_field"]` does not.
- The mangler cannot find names built at runtime or stored in arbitrary strings. Code that uses `eval`, `Function`, or `with` must reserve any affected names.
- Compression may create a matching property after this pass has finished. For example, `'f' + 'oo_'` may later become `foo_` and will not be mangled.
- TypeScript enum reverse mappings are not supported because the transform creates quoted strings.
- Multi-program support is a caller API. The caller must collect every program that shares mangled properties before assigning names.
The public `oxc-minify` API is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374.
Supersedes #23772.
AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.
An uninitialized module-level `var` is `undefined` until it is assigned. After Rolldown removed an unreachable setter, Oxc did not keep this fact, so a dead optional call could remain in the output.
This PR records safe uninitialized module `var` bindings as known `undefined`. Existing minifier passes can then remove optional chains and simplify other reads.
The optimization is limited to module scope. It is disabled for resolved writes, redeclarations, and direct `eval`. Script globals and function-local variables are not changed.
Direct aliases keep the same known value without replacing every read with `void 0`. This avoids larger output and lets the existing alias cleanup work normally.
Generic optimizer tests now use function parameters when they need an unknown value. Focused tests cover the new behavior for uninitialized module variables.
## Example
Input:
```js
var slot
function make(value) {
slot?.(value)
return value
}
console.log(make(1))
```
Before:
```js
var slot;
function make(value) {
slot?.(value);
return value;
}
console.log(make(1));
```
After:
```js
function make(value) {
return value;
}
console.log(make(1));
```
Verified with the full `oxc_minifier` test suite (649 passed, 34 ignored), Clippy, formatting, minsize, and allocation checks. Minsize and allocation snapshots are unchanged.
Fixesrolldown/rolldown#10659
AI-assisted by OpenAI Codex.
`f64::powf` implements IEEE 754 `pow`, which returns `1` for a base of
`1` whatever the exponent.
[`Number::exponentiate`](https://tc39.es/ecma262/#sec-numeric-types-number-exponentiate)
instead returns `NaN` when the exponent is `NaN` (step 1), and when the
base has magnitude `1` and the exponent is infinite (steps 9-10).
Constant folding called `powf` directly, so:
```js
1 ** Infinity // spec: NaN oxc folded to: 1
(-1) ** Infinity // spec: NaN oxc folded to: 1
1 ** -Infinity // spec: NaN oxc folded to: 1
1 ** NaN // spec: NaN oxc folded to: 1
```
`1` is truthy and `NaN` is not, so an `if` guarded by such an expression
takes the wrong branch.
The one case the two agree on — an exponent of `0` returning `1`
whatever the base, including `NaN ** 0` and `Infinity ** 0` — is
unchanged, since `powf` already gets it right and the spec checks the
exponent before the base.
`cargo minsize` shows no snapshot change.
Found by the minifier fuzzer (#25594).
---
Written with AI assistance (Claude Code). Reviewed by me, and every
expected value in the added test was checked against Node.js.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keep the variable declaration when that is initialized with class and keepNames is enabled:
```js
var MyStore = class { static { console.log(this.name) } };
```
fixes https://github.com/oxc-project/oxc/issues/25060
I used AI to generate the code and then reviewed the code and added the tests for `keepNames` disabled cases.
Found by the semantic fuzzer added in #25594 (seed `29024`).
## What
`try_fold_left_child_op` rewrites `(a op b) op right` (`&`, `|`, `^`)
into `x op constant` by folding `right` together with whichever of
`a`/`b` evaluates, discarding both. It never checked the discarded
operands for side effects.
Being constant-evaluable does not imply being side-effect free: `[(y =
9), 1].length` evaluates to `2` while still assigning `y`.
```js
var y = 2, c = 0;
(3 ^ c) ^ [(y = 9), 1].length;
console.log(y); // 9, minified to 2 — the assignment is dropped
```
The minified output is `c ^ 1`: the array literal, and the assignment
inside it, are gone.
## How
Bail out when the discarded right operand — or the left-child operand
folded with it — may have side effects.
The shape check (`left.operator != op`) now runs before
`may_have_side_effects`, which walks the subtree, so the common
non-matching case stays as cheap as before.
Note `fold_static_member_expr` already guards its `.length` fold with
the same check; this restores the symmetry.
## Testing
- New regression test `test_fold_left_child_op_keeps_side_effects`,
covering `^`, `|`, `&` and both operand positions, plus a positive case
asserting the fold still happens when nothing has side effects.
- `cargo test -p oxc_minifier -p oxc_ecmascript`, `cargo clippy
--all-targets`, `cargo fmt --check` all pass.
- `just minsize`: no snapshot change, so no size regression.
## AI disclosure
This change was produced with AI assistance (Claude Code). I have
reviewed and understand every line; the root cause and the minimal
reproduction were verified by hand against Node.js.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Fix private-name mangling for `get #x()` / `set #x(value)` accessor pairs.
JavaScript treats an accessor pair as one private name, but the mangler previously assigned a candidate per element before collecting into a name-keyed map. The setter overwrote the getter’s mapping, while still consuming an extra candidate.
### Before
```js
class Outer {
get #x() {}
set #x(value) {}
#longName;
nested() {
return class {
#nestedName;
};
}
}
```
```js
class Outer {
get #t() {}
set #t(e) {}
#n;
nested() {
return class {
#r;
};
}
}
```
### After
```js
class Outer {
get #e() {}
set #e(e) {}
#t;
nested() {
return class {
#n;
};
}
}
```
## Implementation
- Deduplicate private names during map construction with `HashMap::entry`.
- Derive nested-class offsets from already-built lexical parent mappings, preserving collision avoidance.
- Pre-size mappings from the number of private elements.
Found by the semantic fuzzer added in #25594 (seed `15979`).
## What
`ToNumber(!x)` was computed from `ToNumber(x)` instead of
`ToBoolean(x)`:
```rust
Expression::UnaryExpression(unary) if unary.operator.is_not() => {
let number = unary.argument.to_number(ctx)?;
Some(if number == 0.0 { 1.0 } else { 0.0 })
}
```
`!x` is `ToBoolean(x)` negated, so the two predicates must agree — and
they don't. `NaN` is falsy but does not number to `0`, while `"0"`, `"
"`, `"0.0"` and `[]` all number to `0` yet are truthy.
## Why it was not caught
`minimize_conditional_expression` rewrites `a ? 1 : 0` into `+!!a`. That
rewrite is correct, but when a parent expression evaluated the freshly
built `+!!a` in the same pass, the broken branch flipped the result:
```js
(NaN ? 1 : 0) || 2 // 2, minified to 0
('0' ? 1 : 0) || 2 // 1, minified to 2
([] ? 1 : 0) || 2 // 1, minified to 2
```
A `!!x` written in the source is folded correctly by `fold_unary_expr`
before any parent observes it, so only pass-generated nodes were
affected. That is why existing tests and the conformance suites pass.
refs https://github.com/oxc-project/oxc/pull/8267
## How
Derive the value from `to_boolean`. This is strictly more conservative:
when `to_boolean` returns `None` the fold is skipped rather than
answered wrongly.
## Testing
- New regression test `test_conditional_to_unary_plus_uses_to_boolean`
covering `NaN`, `"0"`, `" "`, `"0.0"` and `[]`.
- `cargo test -p oxc_minifier -p oxc_ecmascript`, `cargo clippy
--all-targets`, `cargo fmt --check` all pass.
- `just minsize`: no snapshot change, so no size regression.
## AI disclosure
This change was produced with AI assistance (Claude Code). I have
reviewed and understand every line; the root cause and each minimal
reproduction were verified by hand against Node.js.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
### Summary
Fix private-member mangling for class heritage expressions.
The code generator previously activated a class's private-member mapping before emitting that class's decorators and `extends` expression. A private identifier in those expressions is evaluated in the enclosing lexical class scope, not the class currently being declared.
This change:
- reserves each class's mapping ID in AST traversal order;
- activates that mapping only immediately before emitting its class body;
- adds a mangler/codegen regression test covering a nested class with identically named private members.
### Reproduction
```js
class HasOuterBrand {}
class MissingOuterBrand {}
class Outer {
#x;
create(o) {
return class Inner extends (
#x in o ? HasOuterBrand : MissingOuterBrand
) {
#x;
};
}
}
```
`#x` in `Inner`'s heritage expression refers to `Outer.#x`. `Inner.#x` is not in scope until the `Inner` class body.
### Scope model
```text
Outer class body: active private mapping
Outer.#x -> #e
create(o) {
class Inner extends (#x in o ? ... : ...)
│
└─ resolves in Outer scope: #e
Inner class body: active private mapping
Inner.#x -> #t
}
```
### Before
Codegen entered `Inner` before emitting the entire class header:
```rust
p.enter_class();
p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);
...
self.body.print(p, ctx);
```
That made private-identifier lookup prefer `Inner`'s mapping while printing `extends`, producing the wrong brand:
```js
return class t extends (#t in e ? HasOuterBrand : MissingOuterBrand) {
#t;
};
```
The `#t` in the heritage expression is invalid because `Inner.#t` is not yet in scope.
### After
The class ID is reserved first, preserving mapping IDs assigned in traversal order, but the class mapping is pushed only for the body:
```rust
let class_id = p.reserve_class();
p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);
p.enter_class(class_id);
self.body.print(p, ctx);
p.exit_class();
```
```text
Class-ID allocation Active private-name scope
──────────────────── ─────────────────────────────
reserve Outer ID enclosing class / none
reserve nested header IDs enclosing class / none
enter Outer ID Outer body
enter Inner ID Inner body
```
Separating reservation from activation matters: moving allocation until after heritage could shift IDs for nested classes encountered in a heritage expression and break the mangler's precomputed mapping association.
### Regression coverage
The existing minifier mangler integration harness now snapshots this case with private-member mappings enabled. It confirms that the generated heritage expression uses Outer’s mapping (`#e`) while Inner’s field uses its distinct mapping (`#t`):
```js
class Outer {
#e;
create(e) {
return class t extends (#e in e ? HasOuterBrand : MissingOuterBrand) {
#t;
};
}
}
```
Preserve block statements inside labeled statements when they contain an
iteration statement (for/while/do-while).
Previously, these blocks were removed, which caused continue label
syntax errors to not be preserved in the minified output:
```js
label: {
for (let i = 0; i < 10; i++) {
continue label; // SyntaxError: 'label' does not denote an iteration statement
}
}
```
fixes#25215
------------------
this guard is broad as we are not checking if inner loop has labeled
continue, this optimization could be implemented but its better if we
wait for #24155
```js
label: for (let i = 0; i < 10; i++) continue label;
```
Slots were assigned to non-manglable symbols as well. This made the slot re-use non-optimal.
This PR fixes that.
I used AI to find out this bug and to generate the code and then reviewed the code.