Commit Graph

1520 Commits

Author SHA1 Message Date
oxc-guard 60fa13878c release(crates): oxc v0.149.0 (#26397)
### 💥 BREAKING CHANGES

- 66744f0 parser: [**BREAKING**] Rename `panicked` to `fatal_error` in `ParserReturn` (#26382) (overlookmotel)
- 2c9a947 parser: [**BREAKING**] Reduce `MAX_LEN` to 256 bytes below `u32::MAX` (#26352) (overlookmotel)

### 🚀 Features

- 32bdc5b allocator: Construct `ArenaHashSet` with any `Default` hasher (#26372) (Dunqing)
- a17de58 minifier: Process of return/throw in non last position and fix async * inlining (#26214) (Armano)
- bfb4c57 parser: Distinguish unapplied no-side-effects comments (#26120) (碳苯 Carbon)
- 1d9b9d3 ast: Add `GetNodeId` trait (#26145) (camc314)

### 🐛 Bug Fixes

- b156333 codegen: Order accessibility before abstract on accessors (#26392) (camc314)
- 116c8b8 transformer/class-properties: Keep private methods in class-expression scope (#26308) (Changsu Seong)
- 1400a0f parser: Require a string source after `export ... from` (#26389) (camc314)
- 2da73b7 codegen: Print matching quoted import names as identifiers (#26386) (camc314)
- 0a81d29 codegen: Preserve private-in left operand precedence (#26383) (camc314)
- 9a02337 parser: Correctly round large nondecimal literals (#26379) (camc314)
- 3d95477 transformer: Gate async-only rewrites by owning transform (#26326) (Dunqing)
- f45a9ee transformer: Limit arguments capture to lowered async functions (#26317) (Dunqing)
- 789969f transformer: Gate this capture by owning async transform (#26227) (Dunqing)
- affeb14 minifier: Mangle class private members in Node API (#26118) (碳苯 Carbon)
- 8b35abd semantic: Detect duplicate private class elements (#26361) (camc314)
- 457ed57 semantic: Reject await and yield in rest parameter defaults (#26359) (camc314)
- 4b89c7b semantic: Reject jumps across arrow functions (#26357) (camc314)
- c966aca parser: Do not omit `<` token opening type argument list (#26328) (overlookmotel)
- f07dca5 ast_visit: Add the trimmed prefix length to translation table offsets (#26173) (Bharadwaj Pendyala)
- 3ab9bd1 parser: Do not re-lex template substitution tail after fatal error (#26230) (overlookmotel)
- 07851b9 parser: Fix debug assert failure when lexer error with tokens enabled (#26229) (overlookmotel)
- 00dea7a allocator: Gate `Allocator::data_end_ptr` behind fixed_size feature (#26248) (camc314)
- 853ffab ecmascript: Avoid `charAt` panic on 32-bit (#26244) (camc314)
- 34feccb runtime: Remove stale regenerator exports (#26243) (camc314)

### ⚡ Performance

- 34a242e minifier: Use `Ident` for property mangler (#26334) (sapphi-red)
- 5303f5c minifier: Unify merging of last expression into target sequence (#26264) (Armano)
- 2ca7d29 minifier: Consume nodes in minimize_statements in reverse order (#26258) (Armano)
- d2354b4 mangler: Use `Ident` for variable names to keep (#26333) (sapphi-red)
- 5b3e335 minifier: Use `Ident` instead of `Str` in `KeepVar` (#26332) (sapphi-red)
- 51366fb minifier: Use `IdentHashSet` in `PrivateMemberUsageStack` (#26331) (sapphi-red)
- d19c42a minifier: Merge nested if stmt in place instead of creating dummies (#26351) (Armano)
- 356def6 parser: Shrink annotation comment ranges (#26356) (overlookmotel)
- a5be474 parser: Shave instruction off `parse_jsx_element_name` (#26355) (overlookmotel)
- 9780663 parser: Remove fatal error guard from `parse_jsx_element_name` (#26354) (overlookmotel)
- 766e12f parser: Remove `token` field from `LexerCheckpoint` (#26350) (overlookmotel)

### 📚 Documentation

- d4b4e61 transformer: Document ES2015 target floor (#26378) (Dunqing)
- 67dd054 ast_visit: Correct out-of-date comments on `Utf8ToUtf16` converter (#26285) (overlookmotel)
- a8ed2a7 minifier: Fix stale validation instructions (#26251) (camc314)
2026-09-07 14:29:34 +00:00
sapphi-red 34a242e134 perf(minifier): use Ident for property mangler (#26334)
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.
2026-09-07 12:58:02 +00:00
Armano 5303f5c884 perf(minifier): unify merging of last expression into target sequence (#26264)
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
2026-09-07 20:40:49 +08:00
overlookmotel 66744f0eb9 feat(parser)!: rename panicked to fatal_error in ParserReturn (#26382)
`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.
2026-09-07 09:41:52 +00:00
Armano 2ca7d29b7a perf(minifier): consume nodes in minimize_statements in reverse order (#26258)
changes `minimize_statement`s to consume statement via a single
`reverse()` + `pop()` pass instead of repeatedly calling `take_in` on
each slot.

potential pitfall: result is in forward order, old stmts are in reverse,
thus we have to be careful in places where we want to take all of them

| File | Arena allocs | Delta | Arena size | Delta |
|---|---:|---:|---:|---:|
| `checker.ts` | `144024 -> 77557` | `-66467` | `19.13 MB -> 17.52 MB` |
`-1.61 MB` |
| `App.tsx` | `15675 -> 9622` | `-6053` | `2.88 MB -> 2.74 MB` | `-0.14
MB` |
| `RadixUIAdoptionSection.jsx` | `29 -> 16` | `-13` | `35.63 kB -> 35.63
kB` | `~0` |
| `pdf.mjs` | `44801 -> 26527` | `-18274` | `6.31 MB -> 5.89 MB` |
`-0.42 MB` |
| `antd.js` | `351645 -> 222308` | `-129337` | `48.77 MB -> 45.76 MB` |
`-3.01 MB` |
| `binder.ts` | `6809 -> 4124` | `-2685` | `1.16 MB -> 1.10 MB` | `-0.06
MB` |
| `kitchen-sink.tsx` | `64956 -> 34571` | `-30385` | `9.39 MB -> 8.74
MB` | `-0.65 MB` |

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-09-07 17:24:05 +08:00
Armano 0d31f58659 refactor(minifier): inline sequence handling to minimize_conditional from minimize_conditional_after_if (#26029)
`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>
2026-09-07 10:45:22 +08:00
Armano 98a8fe3390 test(minifier): add test cases for not verified behavior (#26307)
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
2026-09-07 09:55:22 +08:00
sapphi-red 5b3e335484 perf(minifier): use Ident instead of Str in KeepVar (#26332)
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.
2026-09-06 07:32:33 +00:00
sapphi-red 51366fb9ff perf(minifier): use IdentHashSet in PrivateMemberUsageStack (#26331)
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.
2026-09-06 07:27:08 +00:00
Armano d19c42ac87 perf(minifier): merge nested if stmt in place instead of creating dummies (#26351)
reduce allocations when converting `if (a) if (b) x;` to `if (a && b)
x;`
2026-09-05 22:51:23 +01:00
overlookmotel 2c9a947363 feat(parser)!: reduce MAX_LEN to 256 bytes below u32::MAX (#26352)
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.
2026-09-05 15:33:52 +00:00
Armano a17de58e30 feat(minifier): process of return/throw in non last position and fix async * inlining (#26214)
This Pr brings couple optimizations and refactor,

- Disable merging: `if (a) return x; return;` and `if (a) return; return
x;` when is descendant of `async* function`
- Handle empty chains: `if (a) return; return;` → `a; return;`
- Process returns/throw anywhere in block via handler merge `if (a)
return; return; var b` → `a; return; var b`

fixes #26255
ref #24331
2026-09-03 23:05:40 +08:00
camc314 a8ed2a7837 docs(minifier): fix stale validation instructions (#26251)
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.
2026-09-02 15:49:32 +00:00
camc314 853ffabf34 fix(ecmascript): avoid charAt panic on 32-bit (#26244)
## 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.
2026-09-02 09:07:00 +00:00
oxc-guard 894c8f9cd8 release(crates): oxc v0.148.0 (#26198)
### 💥 BREAKING CHANGES

- a31567a allocator: [**BREAKING**] Make `Allocator::cursor_ptr` and `data_end_ptr` private (#26081) (overlookmotel)

### 🚀 Features

- 784e9fa minifier: Invert `!0` and `!1` in place for boolean context to `1` and `0` (#26050) (Armano)
- 1e902cc minifier: Expand fold leading assignments into the var decl (#26142) (Armano)
- 3ed4f6a minifier: Expand de morgan's optimization to allow move of `!` (#25930) (Armano)
- 5672585 parser: Attach all comments to nodes (#25944) (camc314)

### 🐛 Bug Fixes

- 4adfb4c semantic: Validate chained continue labels (#26157) (camc314)
- b874f48 ecmascript: `Math.round` only exact half ties (#26150) (camc314)
- a625378 minifier: Coerce omitted `indexOf` search argument (#26149) (camc314)
- dc7398b ecmascript: Trim trailing whitespace in string to number (#26148) (camc314)
- 243b685 transformer/object-rest: Lower multiple declarators correctly (#26147) (camc314)
- dc09a3a parser: Avoid panic on escaped string export names (#26146) (camc314)
- e412cf2 linter: Clamp invalid JS plugin locations (#26144) (camc314)
- c3dedc9 semantic: Skip function body bindings in parameters (#26099) (Dunqing)
- e74de61 transform-react: Match Babel diagnostic reporting (#26128) (Boshen)
- d5163d0 parser: Correctly classify unapplied pure annotations (#26084) (camc314)
- 8531b9b linter/plugins: Alter method for obtaining mutable `Program` when sending AST to JS plugins (#26077) (overlookmotel)
- 9b51658 regular_expression: Allow oversized decimal escape for Annex B (#26070) (leaysgur)
- c8de4df regular_expression: Reject oversized backreferences (#26055) (camc314)

### ⚡ Performance

- ac4785a diagnostics: Use fixed ANSI styles (#26130) (Boshen)
- 07a0793 packages/codegen: Flatten output in chunks (#26109) (overlookmotel)
- 487427a packages/codegen: Faster string flattening (#26108) (overlookmotel)
- 7785583 packages/codegen: Ensure indent strings are flattened (#26107) (overlookmotel)
- 0dd4db3 packages/codegen: Store mapping positions in an `Int32Array` (#26085) (overlookmotel)
2026-09-01 08:15:07 +00:00
Armano 115955d048 feat(minifier): drop cases whose condition is statically false (#26172)
Add logic to drop switch cases whose test is never reached

fixes #26141
2026-08-31 17:16:40 +08:00
Armano 784e9faad2 feat(minifier): invert !0 and !1 in place for boolean context to 1 and 0 (#26050)
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
```
2026-08-29 15:36:17 +08:00
Armano 1e902cc1ad feat(minifier): expand fold leading assignments into the var decl (#26142)
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
2026-08-29 04:27:01 +08:00
Armano 3ed4f6ad90 feat(minifier): expand de morgan's optimization to allow move of ! (#25930)
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
```
2026-08-29 02:08:57 +08:00
camc314 b874f480c9 fix(ecmascript): Math.round only exact half ties (#26150)
[ECMA-262 §21.3.2.29, `Math.round`](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math.round).

- If `0 < n < 0.5`, return `+0`.
- If `-0.5 ≤ n < -0`, return `-0`.
- Otherwise choose the closest integer, preferring `+∞` only for an actual tie.
2026-08-28 14:26:10 +00:00
camc314 a625378066 fix(minifier): coerce omitted indexOf search argument (#26149)
## 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
```
2026-08-28 14:18:01 +00:00
camc314 dc7398beae fix(ecmascript): trim trailing whitespace in string to number (#26148)
## 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.
2026-08-28 14:07:56 +00:00
oxc-guard 4e258430cd release(crates): oxc v0.147.0 (#26046)
### 🚀 Features

- b6e9e5e minifier: Fold switch stmt whose parent is not block stmt (#25403) (Armano)

### 🐛 Bug Fixes

- 8a9bdbd estree: Include decorators in `FormalParameterRest ` spans (#26021) (camc314)
- 6f0c7cf minifier: Avoid merging `if` to `for` in sloppy mode when containing function declaration (#25638) (sapphi-red)
- 2cde1f6 rust: Address nightly deprecations (#25998) (Boshen)
- 6c5ad1b codegen: Only add mapping names for `PrivateIdentifier`s which have changed (#25958) (overlookmotel)
- 2dad1e0 parser: Track irregular line terminators in trivia (#25947) (camc314)
- 58ba651 minifier: Avoid merging `if` to `for` when the body contains a function declaration (#25637) (sapphi-red)
- a185ac3 codegen: Print `#` before private identifier in TS signature key (#25938) (overlookmotel)
- af82a07 minifier: Keep unary `+` when the other operand has side effects (#25645) (Kotaro Chikuba)

### ⚡ Performance

- 53f6270 packages/codegen: Eagerly initialize `mapNames` (#25979) (overlookmotel)
- 5b43c60 packages/codegen: Split recording mappings into multiple functions (#25978) (overlookmotel)
- cc947b5 packages/codegen: Reduce property lookups (#25977) (overlookmotel)
- c22a9cc packages/codegen: Combine condition checks (#25976) (overlookmotel)
- 215a6a5 packages/codegen: Add functions for writing/marking unnamed mappings (#25975) (overlookmotel)
- fe444cc codegen: Avoid allocations for single comment anchors (#25598) (camc314)
- a0aee81 minifier: Do not rebuild expression when substituting void to null (#25964) (Armano)
- 1004439 packages/codegen: Merge `mark` and `write` calls in `printImportAttributes` (#25972) (overlookmotel)
- 94c8a34 packages/codegen: Remove `node` param from `printString` and `printNonNegativeFloat` (#25971) (overlookmotel)
- 7e2b4b3 packages/codegen: `printNonNegativeFloat` write mapping directly (#25968) (overlookmotel)
- f09c8ab minifier: Avoid creation of unnecessary conditional expressions (#25931) (Armano)
- 53ff080 packages/codegen: Remove `lastWasPostfixClose` (#25888) (overlookmotel)

### 📚 Documentation

- 07de646 packages/codegen: Fix JSDoc comments on `write*` methods (#25966) (overlookmotel)
- 3b09c04 packages/codegen: Clarify comment (#25959) (overlookmotel)
2026-08-24 11:19:12 +00:00
Armano b6e9e5efda feat(minifier): fold switch stmt whose parent is not block stmt (#25403)
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`
2026-08-23 22:08:10 +08:00
sapphi-red 6f0c7cf31b fix(minifier): avoid merging if to for in sloppy mode when containing function declaration (#25638)
```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.
2026-08-23 02:07:08 +00:00
Armano 30e612dd73 refactor(minifier): remove old code for folding expressions to throw/return (#25974)
drop dead code related to folding expressions to throw/return as is no
longer can be executed

this code is no longer executed and its a duplication of
`handle_throw_statement` and `handle_return_statement`

we process all stmts first, and after that we try to process them again,
even if everything should be join't already, beside normal check, i'v
tried to execute this code on bunch of codebases / all tests cases and
its truly dead, we may want to move logic used for IF's also to
`handle_return_statement` and `handle_throw_statement`

originally this was added before handle_throw_statement and
handle_return_statement was a thing and its part of initial setup


https://github.com/oxc-project/oxc/blob/b016fd41101de5038a77484c11c0a19c1aa965a9/crates/oxc_minifier/src/peephole/minimize_statements.rs#L947-L956


https://github.com/oxc-project/oxc/blob/b016fd41101de5038a77484c11c0a19c1aa965a9/crates/oxc_minifier/src/peephole/minimize_statements.rs#L972-L981
2026-08-23 09:53:41 +08:00
Armano a0aee8187f perf(minifier): do not rebuild expression when substituting void to null (#25964)
when substituting undefined/void to null mutate only left/right
expression in loosely equals

```js
foo == void 0
//     ^~~~~~
foo != undefined
//     ^~~~~~~~~
```
2026-08-23 09:45:55 +08:00
sapphi-red 58ba651e5c fix(minifier): avoid merging if to for when the body contains a function declaration (#25637)
fixes https://github.com/oxc-project/oxc/issues/22222

I used AI to generate the code and then reviewed the code and changed the code and added tests.
2026-08-20 18:32:25 +00:00
Armano f09c8abf38 perf(minifier): avoid creation of unnecessary conditional expressions (#25931)
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
2026-08-20 10:05:06 +08:00
Kotaro Chikuba af82a0707a fix(minifier): keep unary + when the other operand has side effects (#25645)
`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>
2026-08-19 18:38:27 +09:00
oxc-guard 0389c4010e release(crates): oxc v0.146.0 (#25912)
### 🚀 Features

- 83cb516 minifier: Improve negation of expressions in boolean context (#25755) (Armano)

### 🐛 Bug Fixes

- 134fb22 isolated-declarations: Preserve unresolved parameter types (#25909) (Dunqing)
- 526c2b3 codegen: Remove deprecated test APIs (#25889) (camc314)
- 344a9b2 codegen: Align enum template sourcemaps (#25872) (camc314)

### ⚡ Performance

- 5ef0c93 minifier: Unwrap all unary exprs without creating dummy expressions (#25891) (Armano)
- 8783524 packages/codegen: Remove loop from `growMappingBuffer` (#25886) (overlookmotel)
- b6b14c0 packages/codegen: Remove redundant branch (#25885) (overlookmotel)
- be6adf0 packages/sourcemap: Make `location` passed to `recordSourceMapping` always a number (#25884) (overlookmotel)
- f720f15 packages/codegen: Take named-mapping only code off hot path in `recordSourceMapping` (#25883) (overlookmotel)
- a734af5 packages/codegen: Remove redundant setting of `last` in `printImportAttributes` and `printIf` (#25882) (overlookmotel)
- c6e44c6 packages/codegen: Combine 2 writes in `printJSXElement` (#25881) (overlookmotel)
- 8801662 packages/codegen: Remove redundant mapping from `printParams` (#25880) (overlookmotel)

### 📚 Documentation

- 5ad330e packages/codegen: Reformat comments (#25876) (overlookmotel)
2026-08-19 08:29:51 +00:00
Armano 83cb5163c0 feat(minifier): improve negation of expressions in boolean context (#25755)
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
2026-08-19 13:21:35 +08:00
Armano 5ef0c93295 perf(minifier): unwrap all unary exprs without creating dummy expressions (#25891)
This is a follow up for changes introduced in #25836 by applying
`replace_expression_with` helper to unused expressions and to substitute
2026-08-19 10:04:50 +08:00
oxc-guard[bot] 4a4a1fb2e7 release(crates): oxc v0.145.0 (#25865)
### 💥 BREAKING CHANGES

- 365274e packages/codegen: [**BREAKING**] `printSync` return an object
(#25720) (overlookmotel)

### 🚀 Features

- 6a7eb60 packages/codegen: Alter capitalization of `sourceFilename`
option (#25854) (overlookmotel)
- 300763f packages/codegen: Add full source map support (#25585)
(camc314)
- a169e4a napi/minify: Expose property name mangling options (#24741)
(Dunqing)
- 2f5cdb1 minifier: Add property name mangling (#24740) (Dunqing)
- 4922141 mangler: Deduplicate private accessor names (#25601) (camc314)
- 1c4f519 napi: Add Relay transform plugin (#25503) (Boshen)
- a4478e9 codegen: Add `oxc-codegen` package (#25488) (overlookmotel)

### 🐛 Bug Fixes

- 0fbcf64 codegen: Validate sourcemap options (#25860) (camc314)
- 345f981 react_compiler: Skip node_modules by default (#25859) (Boshen)
- ab81f3f minifier: Drop side-effect-free additions (#25639) (Dunqing)
- 241c559 napi/minify: Accept RegExp for property filters (#25827)
(Dunqing)
- 4cc7ea4 codegen: Reject invalid indent options (#25807) (camc314)
- 53f9295 isolated-declarations: Preserve undefined for defaulted any
(#25292) (camc314)
- 8ab883a codegen: Preserve property key annotations (#25766) (Dunqing)
- b13fd48 minifier: Model uninitialized module vars as undefined
(#25497) (Dunqing)
- f6000ac ecmascript: Fold `**` with `Number::exponentiate`, not IEEE
`pow` (#25644) (Kotaro Chikuba)
- b846abc isolated-declarations: Handle ambient expando properties
(#25655) (camc314)
- dec0a86 react-compiler: Standardize diagnostics (#25702) (Boshen)
- fca2e0c parser: Reject initialized lexical declarations in for-in
(#25700) (Boshen)
- ae9be8f parser: Forbid type parameters on quoted constructors (#25696)
(Boshen)
- c5f188a react-compiler: Use Babel v1 validation defaults (#25676)
(Boshen)
- db44651 napi: Disable reuseWorker in browser bindings (#25640)
(leaysgur)
- ce35d47 react_compiler: Preserve JSX import source pragmas (#25592)
(Boshen)
- 059784d semantic: Respect shadowed `Infinity` and `NaN` in enum
evaluation (#25604) (camc314)
- bb5a232 minifier: Keep variable declaration initilized with class when
keepNames is enabled (#25584) (sapphi-red)
- f49229d minifier: Keep side effects when rotating bitwise operands
(#25596) (Kotaro Chikuba)
- e82495b ecmascript: Derive `ToNumber` of `!x` from `ToBoolean`
(#25595) (Kotaro Chikuba)
- 509931b semantic: Classify global references per identifier (#25608)
(camc314)
- 80484ce mangler: Correct base54 safety comment (#25606) (camc314)
- c1369a7 codegen: Resolve private names in class heritage (#25588)
(camc314)
- c002f29 codegen: Escape sources for empty import specifiers (#25586)
(camc314)
- 0c68b7f estree: Emit `decorators` on `FormalParameterRest` (#25582)
(camc314)
- cdf1846 semantic: Allow legacy escapes in JSX attributes (#25576)
(Boshen)
- e75e102 minifier: Preserve block statement in labeled iteration
statements (#25162) (Armano)
- 771d79a mangler: Exclude non-manglable symbols from slot assignment
(#25539) (sapphi-red)
- 76b19f5 minifier: Avoid duplicating large folded strings (#25532)
(Dunqing)
- 59e8895 codegen: Validate starting indent level (#25550) (camc314)
- 88b34f2 clippy: Remove unneeded `unsafe` (#25551) (camc314)
- 23a7ad0 parser: Stop delimited lists at end of file (#25542) (Boshen)
- 73acba9 parser: Preserve fatal errors during await reparse (#25541)
(Boshen)
- c3e99d1 minifier: Avoid invalid octal escapes in template folds
(#25495) (Dunqing)
- b4e6a9e codegen: Output newline after `export default interface`
(#25487) (overlookmotel)
- 5fcf683 minifier: Correct issue with try finally termination (#25185)
(Armano)
- 1645d93 react_compiler: Preserve source spans (#25462) (Boshen)
- 8d7f9cf react_compiler: Honor eslint suppressions (#25394) (Boshen)

### ⚡ Performance

- 568203e ecmascript: Use binary search for known globals (#25817)
(Boshen)
- c84ede3 estree_tokens: Share JS token update entry point (#25826)
(Boshen)
- 9a6e862 minifier: Move owned statements directly (#25835) (Dunqing)
- 61b2aef minifier: Move owned AST nodes directly (#25837) (Dunqing)
- 673b04b minifier: Replace expressions without take_in dummies (#25836)
(Dunqing)
- 63ff8ef linter: Outline diagnostic construction (#25762) (Boshen)
- 621808e diagnostics: Measure graphemes lazily (#25723) (Boshen)
- f62ed0e diagnostics: Optimize graphical number rendering (#25715)
(Boshen)
- 7152834 diagnostics: Reduce graphical formatting overhead (#25714)
(Boshen)
- 10fc4b7 diagnostics: Reduce graphical rendering allocations (#25711)
(Boshen)
- 9c8abab diagnostics: Batch graphical reports (#25710) (Boshen)
- 757f3d4 mangler: Share allocated names across reused slots (#25605)
(camc314)
- 5444cbf codegen: Skip escaping harmless `<` tokens (#25564) (camc314)

### 📚 Documentation

- 58f7ab9 packages/codegen: Reformat docs and comments (#25848)
(overlookmotel)
- 627466e transform-react: Document all options (#25800) (Boshen)
- ce03ac1 packages/codegen: Fix JSDoc comments for `printSync` (#25722)
(overlookmotel)
- 386a699 packages/codegen: Correct JSDoc comment (#25716)
(overlookmotel)
- 31e571d mangler: Update code example (#25599) (camc314)
- fd62354 codegen: Revamp package readme (#25560) (camc314)
- ffa3153 codegen: Clarify raw transfer Node requirement (#25565)
(camc314)
- c5d4063 codegen: Clarify binary walk allocations (#25562) (camc314)
- 5703d7a codegen: Correct stale printer comments (#25561) (camc314)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
2026-08-18 22:26:40 +08:00
Dunqing ab81f3f7f8 fix(minifier): drop side-effect-free additions (#25639)
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.
2026-08-18 07:59:10 +00:00
Dunqing 9a6e86226e perf(minifier): move owned statements directly (#25835)
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.
2026-08-18 06:57:09 +00:00
Dunqing 61b2aefa94 perf(minifier): move owned AST nodes directly (#25837)
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.
2026-08-18 06:57:08 +00:00
Dunqing 673b04bc46 perf(minifier): replace expressions without take_in dummies (#25836)
`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.
2026-08-18 06:57:08 +00:00
Armano bfb0832d24 refactor(minifier): simplify if-statement minimization (#25178)
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`
2026-08-18 13:37:07 +08:00
Dunqing 8ab883afaa fix(codegen): preserve property key annotations (#25766)
`@__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.
2026-08-17 09:23:04 +00:00
Dunqing 2f5cdb1231 feat(minifier): add property name mangling (#24740)
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.
2026-08-17 07:50:51 +00:00
Dunqing b13fd48471 fix(minifier): model uninitialized module vars as undefined (#25497)
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.

Fixes rolldown/rolldown#10659

AI-assisted by OpenAI Codex.
2026-08-17 06:24:04 +00:00
Kotaro Chikuba f6000ac141 fix(ecmascript): fold ** with Number::exponentiate, not IEEE pow (#25644)
`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>
2026-08-17 12:21:54 +08:00
sapphi-red bb5a232424 fix(minifier): keep variable declaration initilized with class when keepNames is enabled (#25584)
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.
2026-08-14 01:23:31 +00:00
Kotaro Chikuba f49229d566 fix(minifier): keep side effects when rotating bitwise operands (#25596)
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>
2026-08-14 10:23:22 +09:00
camc314 4922141d0a feat(mangler): deduplicate private accessor names (#25601)
## 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.
2026-08-14 00:44:46 +00:00
Kotaro Chikuba e82495ba54 fix(ecmascript): derive ToNumber of !x from ToBoolean (#25595)
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>
2026-08-14 00:35:39 +00:00
camc314 c1369a71ff fix(codegen): resolve private names in class heritage (#25588)
### 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;
    };
  }
}
```
2026-08-13 13:17:31 +00:00
Armano e75e102c49 fix(minifier): preserve block statement in labeled iteration statements (#25162)
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;
```
2026-08-13 14:49:34 +08:00
sapphi-red 771d79a9e3 fix(mangler): exclude non-manglable symbols from slot assignment (#25539)
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.
2026-08-13 06:39:16 +00:00