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
```
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`
`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>
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.
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.
do not rewrite if else where `alternate` could produce invalid code and
change behavior
```js
function g(a,b){if(a){}else if(b){return()=>typeof f}else function f(){}}
```
https://github.com/oxc-project/oxc/pull/24997/changes#r3659223987
> **<sub><sub></sub></sub>
Preserve Annex B branch scoping when wrapping terminated blocks**
>
> In sloppy scripts, broadening this condition from direct jumps to any
terminated statement lets a block-returning branch trigger the synthetic
wrapper. For `function g(a,b){if(a){}else if(b){return()=>typeof f}else
function f(){}}`, the subsequent `handle_if_statement` pass detaches the
`else` into that wrapper, emitting the equivalent of
`if(!a){if(b)return()=>typeof f;function f(){}}`. The Annex B function
is then instantiated when the block is entered, so `g(false,true)()`
changes from `"undefined"` to `"function"`; avoid flattening
scope-sensitive alternates or otherwise preserve their original branch
scope.
based on changes from #24997
---------
Co-authored-by: Cameron <cameron.clark@hey.com>
The parser cannot produce negative or non-finite NumericLiterals: `-1` parses as unary negation, and codegen prints `Infinity` as `1/0`, which parses as a division. The loop's constant folds re-derive the literal from either spelling and record a change for it, one phantom change per such number on every run over printed output.
Collapse `-` into a negative literal in the Normalize pass — the same treatment `NaN` and `Infinity` identifiers already get — and keep `1/0` and `-1/0` unfolded like the existing `!0` and `void 0` guards: they are the canonical printed spellings of the non-finite numbers.
The guard shifts an inlining decision in lodash.js, making it 20 bytes smaller; all other minsize outputs are unchanged.
Boolean-literal ternaries with a sequence in the non-literal branch are left uncompressed because the logical-operand size guard counts sequence parentheses as newly added. A sequence already requires parentheses in a conditional branch, so this misses shorter `&&` / `||` forms.
Treat sequence branches as having no additional parenthesis cost while keeping the existing guard for assignments, nested conditionals, arrows, `yield`, and incompatible logical operators. The change is confined to full expression compression and does not affect statement or tree-shaking passes.
## Example
```js
// input
use(flag ? false : (touch(), true));
// before
use(flag?!1:(touch(),!0));
// after
use(!flag&&(touch(),!0));
```
The minsize corpus improves GZIP by 0.01 kB each for Victory, ECharts, Ant Design, and TypeScript, with no regressions or iteration changes. The additional folds add 3–42 arena allocations per tracked fixture (1,152 arena bytes at most) and no system allocations.
Verified with `cargo test -p oxc_minifier`, `just minsize`, Clippy with warnings denied, formatting checks, and `just ready`.
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
Added a new IsTerminated trait and wired it into handle_if_statement in
minimize-exit-points.
before this, we only treated direct jump statements as terminating. Now
we also handle nested terminated branches, so cases like nested if/else
termination optimize correctly.
for now this is only used in this one optimization path, but we should
reuse IsTerminated in more places (e.g. switch-case and other
exit-point/termination optimizations).
Constant evaluation can replace a short numeric expression with a longer exact floating-point literal, increasing minified output.
Using the serializer shared by #24876 and optimized by #24889, the minifier compares the folded literal with a conservative lower bound for the original numeric expression before accepting numeric addition, subtraction, and multiplication folds. Parentheses are omitted from the estimate, so an accepted fold cannot make output longer. Measuring the original tree instead of evaluated operand values also preserves profitable outer folds such as `.8` without regressing nested expressions such as `0+(.1+.05)`.
## Example
Input:
```js
console.log(0.1 + 0.05);
console.log(0.1 + 0.2 + 0.1 + 0.4);
console.log(0.1 * 1 + 0.05);
console.log(0.1 * 1 + 0.7);
console.log(0.3 - 0.1);
```
Before:
```js
console.log(.15000000000000002),console.log(.7999999999999999),console.log(.15000000000000002),console.log(.8),console.log(.19999999999999998);
```
After:
```js
console.log(.1+.05),console.log(.7+.1),console.log(.1+.05),console.log(.8),console.log(.3-.1);
```
Verified with the full `oxc_minifier` test suite, minsize and allocation snapshots, Clippy, and formatting.
Closes#24863.
AI assistance: Codex was used to implement and verify this change.
Integer-valued comparisons with zero remain verbose in value contexts, even when the minifier can prove that the left-hand side is an `int32` or `uint32`. Applying the same rewrite to every Number-valued expression would be incorrect for `NaN`, while bitwise expressions with unknown operands may still evaluate as BigInt.
Reuse the existing `IsInt32OrUint32` proof to lower equality with zero to logical negation and inequality with zero to double negation. The optimization remains inside binary-expression compression and does not change statement transforms or DCE.
```js
// before
f((a & 1) != 0);
f((a ^ 2) === 0);
// after
f(!!(a & 1));
f(!(a ^ 2));
```
Verified with `just ready`, `just minsize`, and `just allocs`. On the latest `main`, the minsize snapshot improves Three.js raw output from 642.46 kB to 642.45 kB and TypeScript GZIP output from 852.94 kB to 852.81 kB; allocations and compression-pass iterations are unchanged.
AI disclosure: Codex was used to research, implement, and verify this change.
Closes the migration tracked in #23043.
The new builder API (methods on the AST types themselves) has been in place for a while, all Oxc crates are migrated, and every crate which uses `AstBuilder` already enables `disable_old_builder` in its tests. The module docs said "after a few weeks, we will remove `AstBuilder`'s own methods entirely" — this does that.
## What's removed
- `crates/oxc_ast/src/generated/ast_builder.rs` and `builder/methods.rs` — `AstBuilder`'s own node and primitive constructors.
- `AstBuilderGenerator`, which produced the former.
- The `disable_old_builder` Cargo feature, plus the dev-dependency hacks it required across 33 manifests. Eight crates depended on `oxc_ast` *purely* to turn the feature on; those deps and their `cargo-shear` ignore entries are gone too.
## One thing worth reviewing
`AstBuilderGenerator` owned the `#[builder(default)]` / `#[builder(skip)]` attr registration that `BuilderMethodsGenerator` only *reads* — its doc comment said as much. Deleting it naively would have silently dropped `skip`/`default` handling for the 12 AST types using those attrs, so `attrs()` and `parse_attr()` move to `BuilderMethodsGenerator`.
The evidence this was faithful: after regenerating, `builder_methods.rs` and every other generated file are **byte-identical**. The only generated change in the diff is the deletion.
## Breaking changes
- `AstBuilder`'s own methods are gone. Use the AST type methods: `Statement::new_expression_statement(span, expr, &builder)` instead of `builder.statement_expression(span, expr)`.
- `AstBuilder` is no longer `Copy`/`Clone`, and its `allocator` field is private — use the `allocator` method from `GetAllocator`.
- `AstBuilder` and `NONE` are no longer re-exported from the crate root; import them from `oxc_ast::builder`. (The `disable_old_builder` feature already gated these off, so nothing in-tree used the root path.)
- The `disable_old_builder` feature no longer exists — remove it from any manifest that sets it.
A migration note covering all of the above is in `crates/oxc_ast/src/builder/mod.rs`.
---
Implemented with AI assistance (Claude Code); design, review, and verification driven by the author per the repo AI-usage policy.
A locally read-only identifier is not necessarily stable across a side-effecting call. ESM imports are live bindings, Script-root bindings can be changed by another script, and computed assignment keys bypassed the existing closed-over-TDZ guard. Single-use substitution could therefore move a binding read before a call and change program behavior.
The same incomplete stability query affected logical-assignment folding. A live imported member base could be cached before a call that reassigns the export, making `||=` or `??=` update the old object instead of the current binding.
Build on the shared reorder predicates from #24699. Imports, Script-root bindings, unresolved references, syntactically mutated symbols, and closed-over block-scoped reads are treated conservatively across ordinary reads, assignment-target objects, computed keys, and member-base folding. Stable module bindings and pure replacements remain optimizable.
## Example
```js
var key = "x";
function init(obj) {
let value = mutate();
obj[key] = value;
}
```
The previous substitution could produce:
```js
var key = "x";
function init(obj) {
obj[key] = mutate();
}
```
That captures `key` before `mutate()` runs, although another script can change the global binding during the call. The declaration is now retained so the read remains after `mutate()`.
For imported member bases, main can rewrite `x.y || (mutate(), x.y = 3)` to `x.y ||= (mutate(), 3)`. When `mutate` reassigns the exported `x`, the original two-module runtime repro produces `3`, while the rewritten program produces `0`. The shared stability guard now prevents that fold.
Plain computed assignments still inline stable identifiers and literals. ECMAScript evaluates the key expression and `GetValue` before the right-hand side, but defers `ToPropertyKey` until the final `PutValue`, so arbitrary key expressions remain conservative without losing safe simple-key substitutions.
This improves the existing reorder guard; it does not add a general TDZ model. Sloppy mapped `arguments` mutation remains a pre-existing unsupported case and is outside this stack.
Verified with the full `oxc_minifier` suite (622 passed, 42 ignored), clippy with warnings denied, formatting, minsize, allocation snapshots, runtime ordering probes, and repeat-compression checks. `cargo minsize` measured 18 additional raw bytes across `echarts.js` and `typescript.js`, with no iteration-count changes. System-allocation metrics are unchanged; arena changes are dominated by retained code with minor fold-path shifts.
## Stack
1. #24699 — centralize reorder stability checks
2. **#24698 — guard reordered identifier reads**
AI assistance: Codex and Claude were used for implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
After many trial and errors, this seem to be the best way to remove
break stmts in switch cases,
This change targets only last break in last case of switch smts if its
unlabelled
this change differs from #23914 as its no longer targets all branches as
that should not be no longer necessary if all cases are correctly
rewritten
https://github.com/oxc-project/oxc/issues/24672
-----
cases like `switch (a) { case 3: if (b) break; }` are not changed
-----
ref: #17544
Reference counts cannot remove unreachable recursive functions because their self- and mutual references keep the counts nonzero even when no executable code can reach them.
This change computes function reachability after scoping is flushed. Normalize registers function declarations and bindings with implicit runtime observers. References outside registered functions make their targets live; references owned by a live registered function propagate liveness to their targets.
Graph deadness is used only to remove function declarations. Other transformations continue to use ordinary reference counts together with implicit-observability checks. Direct `eval` disables the analysis for the whole program while present. Exports, Script-root bindings, Annex B aliases, and `using` disposal remain observable even when they have no resolved AST reference.
## Example
Input:
```js
function self() { self() }
function a() { b() }
function b() { a() }
export {};
```
Output:
```js
export {};
```
## Supported
- Self-recursive and mutually recursive function declarations.
- References nested in blocks, arrows, function expressions, classes, and parameter defaults. The nearest registered function declaration owns each reference.
- ES modules, CommonJS, and local functions in Scripts. Script-root bindings remain visible to later scripts.
- Full compression and DCE-only mode.
- Reads, writes, exports, `using` disposal, and direct `eval` preserve live functions.
- Exact self-recursive variable initializers whose value is a `FunctionExpression` or `ArrowFunctionExpression`, including classic `for` initializers.
## Conservatively retained
- Mutual cycles between variable declarators, such as `const a = () => b(); const b = () => a()`.
- Recursive class cycles and mixed function/declarator/class cycles.
- Wrapped or arbitrary function-valued initializers, such as `const f = (effect(), () => f())`.
- Recursive declarators in `for-in` and `for-of` heads.
- Sloppy unhoisted Annex B block functions, whose runtime alias write is not represented by their block-scoped symbol.
## Performance
[CodSpeed's report for `e71c392`](https://github.com/oxc-project/oxc/pull/24125#issuecomment-4877260138) measured a 3.74% regression on `binder.ts`, where the analysis finds no dead graph. On `kitchen-sink.tsx`, removing 39 function declarations covering 25.3 kB of source early improved the minifier benchmark by 21.01% and the full pipeline by 8.76%. A separate 15-run Script/CommonJS corpus measured a 1.67% median increase overall, below the 2% acceptance threshold.
Verified with all-feature workspace tests, lint, rustdoc, formatting, AST generation, minsize and allocation snapshots, and the repeat-compression corpus.
Closes#13105Closes#17364
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository's AI usage policy.
remove empty cases:
- before default or at end (if no default).
- only if they don't affect behavior.
refactored implementation from #17523
ref #17544
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Folding a define-replaced branch can leave `if (true) { ... return fn; }` as a bare block: the lexical declaration inside pins the block (flattening it could collide with sibling bindings), and the statements after it survived even though the block always returns. rolldown's per-module tree-shake hits this shape and ships the dead branch (rolldown/rolldown#10184).
`minimize_statements` only flagged dead control flow on direct jump statements, each arm setting the flag by hand. The driver now flips the flag whenever the statement it just emitted never completes normally: a direct jump, a block ending in a jump, an if/else where both branches jump, or a try where every path jumps. That single check subsumes the per-arm writes, so the `&mut bool` threaded through the return/throw handlers is gone. The removal reuses the existing dead-zone machinery, so `var` hoisting and kept function declarations behave as before, and both DCE and full-minify modes are covered.
A minimized dead zone keeps hoisting survivors after the jump — `function` declarations and the initializer-less `var` stub KeepVar re-emits — so the block predicate skips those from the back instead of testing only the literal last statement; nothing after an aborting statement ever runs, so they cannot change how the block completes.
The helper is conservative on purpose: labeled statements never count (`a: { break a; }` completes normally), and loops and switches are left to their specific folds. Terser removes the same tails; esbuild keeps them. The leftover `{ ... }` block wrapper is also kept — flattening a block whose lexical declarations may collide with siblings is a separate rewrite, and terser and esbuild keep the braces too.
## Example
`__SSR__` defined to `true` (rolldown treeshake, `minify: false`):
```js
export function test1() {
if (true) {
const fn = () => console.log("ssr");
fn.stop = fn;
return fn;
}
const fn = () => console.log("client");
fn.stop = fn;
return fn;
}
```
Before, the unreachable client tail stayed in the output. After:
```js
export function test1() {
{
const fn = () => console.log("ssr");
fn.stop = fn;
return fn;
}
}
```
Verified with the minifier unit suite, test262/babel minifier conformance (no changes), DCE `--twice` idempotency, and `just minsize` (antd gzip 455.51 kB -> 455.50 kB, iteration counts unchanged).
Fixesrolldown/rolldown#10184.
Implemented with AI assistance (Claude Code); design, review, and verification driven by the author per the repo AI-usage policy.
### What
Terser rewrites `!(a==b||c==d)` into `a!=b&&c!=d`. oxc only removed the `!` when a later pass could consume it for free, such as swapping if-else branches. In guards that end in a jump (`throw`, `return`, `break`) and in loop tests there is nothing to swap, so the negation stayed in the output.
### How
`minimize_unary` gets a `LogicalExpression` arm that distributes the `!` over `&&`/`||` chains. It only fires when every comparison in the chain can invert its operator in place (`==`, `!=`, `===`, `!==`; relational operators are excluded because `!(a<b)` is not `a>=b` under NaN) and when flipping `&&`/`||` does not add parentheses. Under those rules the fold always saves bytes and is its own inverse, so shapes that re-negate the test later (branch swaps, `!!` collapses) still reach their old output.
DCE mode is untouched; `minimize_unary` only runs in full minify.
### Example
Before:
```js
export function f(e,t,n,r){if(!(e==t||n==r))throw Error(`x`)}export function g2(e,t,n,r){for(;!(e==t||n==r);)h()}
```
After:
```js
export function f(e,t,n,r){if(e!=t&&n!=r)throw Error(`x`)}export function g2(e,t,n,r){for(;e!=t&&n!=r;)h()}
```
### Size
jquery, d3, three, victory, echarts, typescript, react and bundle.min each shrink by 0.01–0.02 kB minified; no fixture grows. jquery's gzip column ticks up 0.01 kB even though its raw output shrank — a compression artifact of an equal-or-smaller input. Arena allocation counts rise slightly (+1 to +11 per fixture) from a transient `!` wrapper that re-negation builds and immediately unwraps.
## What
Drop `o.x = value` when `o` is a local variable that is never read — by default. Until now this only happened with the `treeshake.property_write_side_effects: false` opt-in. terser does this by default; esbuild does not do it at all.
This matters because one leftover write keeps a lot of code alive. antd bundles 820 icon modules that each end with `o.displayName = 'UserOutlined'`; that single write kept each module's whole require-chain alive. **antd.js: 2.22 MB -> 2.13 MB minified** (458 kB -> 455 kB gzip). oxc now beats esbuild on antd by ~180 kB.
## How
The drop itself uses machinery that already exists for the opt-in path (`is_member_assign_to_unused_binding`: the object must be a fresh value — function/class/object/array literal — not exported, never written as a whole, and every read of it must be the target of a property write).
What is new is making it safe without the opt-in:
- A program-wide fact table (`PersistentSymbolFacts`, bit `SymbolFact::MEMBER_WRITE_HAZARD`) is filled once by `Normalize`, before the compress loop. The container is generic on purpose: monotone symbol facts, seeded pre-loop, never cleared — a stale set bit only forgoes an optimization. A follow-up migrates `proto_write_symbols` into it as `PROTO_WRITTEN`, fixing that field's execution-order TODO. A symbol goes in when any of its property writes could be observed: read-modify ops (`o.x += 1` reads the property), chained writes (`a.b.c = 1` reads `a.b`, so dropping `a.b = {}` would throw), and `__proto__` or computed keys (may install setters). A hazarded symbol never gets its writes dropped.
- The set also grows mid-loop when a pass rewrites `o.x = o.x + 1` into `o.x += 1`.
- Only plain `=` with a single-level, non-`__proto__` key qualifies. `o[k()] = v` bails.
- A side-effectful right side is kept: `o.x = impure()` becomes `impure()`.
Once the write is gone, the variable is unused and the existing passes delete the declaration and everything only it referenced.
## Behavior change
The only assumption this makes (documented in `docs/ASSUMPTIONS.md`): setters installed on `Object.prototype` / `Function.prototype` / `Array.prototype` are not observed by these writes — the same assumption the existing opt-in flag makes, and terser's default.
A 98-case audit (node ground truth vs oxc vs terser, strict and sloppy) hardened everything else. Writes that throw or have exotic effects are now kept even though the binding is unused, via a kind-aware key denylist: `caller`/`arguments`/`name`/`length` on function- and class-valued bindings, `prototype` on class-valued (class prototypes are non-writable; plain function `prototype` writes still drop — they are writable and a real size win), `length` on array-valued (the array length setter coerces its RHS and can throw RangeError), private-field writes (brand-check TypeError), and classes with static blocks, decorators, or static accessors are not treated as fresh. Post-audit the matrix shows zero unsound cases beyond the documented prototype-setter class — stricter than terser, which for example still drops `a.length = -1` (losing the RangeError) where oxc now keeps it. No size cost: the minsize corpus is byte-identical, antd keeps the full 90 kB win.
## Example
```js
// input
(function() {
var r = require('react');
var i = require('./asn/UserOutlined');
var o = function(e, t) { return r.createElement(i.a, e, t); };
o.displayName = 'UserOutlined';
})();
```
```js
// before (everything kept)
(function(){var e=require("react"),t=require("./asn/UserOutlined"),n=function(n,r){return e.createElement(t.a,n,r)};n.displayName=`UserOutlined`})();
// after (only the side effects remain)
(function(){require("react"),require("./asn/UserOutlined")})();
```
Found by re-minifying oxc's minsize output with `terser -c`; this was the single largest gap between oxc and terser. Full conformance suite passes with zero snapshot changes; minsize stays a fixed point (antd takes one extra compress iteration).
Implemented with Claude Code, reviewed and verified by me.
## What
Fold a ternary with one boolean literal branch into `&&` / `||`, in value positions (call arguments, returns, assignments).
| Input | Output | Condition |
|---|---|---|
| `c ? false : x` | `!c && x` | always safe |
| `c ? x : true` | `!c \|\| x` | always safe |
| `c ? true : x` | `c \|\| x` | only when `c` is a boolean |
| `c ? x : false` | `c && x` | only when `c` is a boolean |
We already do this inside `if` tests. This PR does it everywhere else. The first two rows produce the exact same value for any `c`. The last two rows would return `c` itself instead of `true`/`false`, so they need a boolean-typed `c`.
## How
New match arms in `minimize_conditional_expression`, next to the existing `a ? true : false` => `!!a` block. The dropped branch must be side-effect free (same check the existing block uses). The fold is skipped when it would make the output longer, e.g. `a || b ? false : c` would need `!(a || b)`, so it stays as is. Equality tests invert for free: `a === b ? false : x` => `a !== b && x`.
## Example
```js
// input
use(foo() ? false : bar());
use(a === b ? false : compute());
```
```js
// before
use(foo()?!1:bar()),use(a===b?!1:compute());
// after
use(!foo()&&bar()),use(a!==b&&compute());
```
Found by re-minifying oxc's minsize output with `terser -c`: typescript.js alone has ~290 of these. All minsize fixtures stay equal or smaller.
Implemented with Claude Code, reviewed and verified by me.
## Summary
- The single-use-variable inliner could merge `let num = await f(); g(v, num)` into `g(v, await f())`, moving the read of a closed-over lexical `v` ahead of the `await`. When the enclosing function runs while `v` is still in its Temporal Dead Zone (called before `let v` executes), the merged form throws `ReferenceError: Cannot access 'v' before initialization` while the original does not — a Svelte production-only crash surfaced via rolldown ([rolldown/rolldown#9959](https://github.com/rolldown/rolldown/issues/9959), [sveltejs/svelte#18454](https://github.com/sveltejs/svelte/issues/18454)).
- The fix adds a guard, `is_tdz_closed_over_read`, that blocks the reorder only for a **block-scoped binding closed over from an enclosing function** — the one shape that can observe the TDZ across an `await`/`yield`. Same-function lexicals, `var`, and parameters still inline; the member assignment-target path (`v.x = await f()`) gets the same guard.
- Covered by `inline_single_use_variable.rs::test_inline_read_before_await_tdz` (closed-over `let`/`const`/`using`/`class`, generator `yield`, member targets, two-level nesting, plus the safe `var` / parameter / same-function cases). Full `oxc_minifier` suite passes; `minsize` unchanged except `bundle.min.js` (+~10 bytes).
## The bug
The crash needs `init()` to run *before* `let v` initializes — here it is called in `p`'s initializer (circular imports cause the same shape), so `v` is in its TDZ while `init` runs:
```js
let p = init(), v = ext();
async function init() {
let num = await foo();
bar(v, num); // reads `v` AFTER the await: the await suspends, module
// eval runs `v = ext()`, then `init` resumes → `v` is set
}
export { p };
```
Inlining merges the temp into the argument, moving the read *before* the await:
```js
// before this PR (incorrect)
async function init() {
bar(v, await foo()); // reads `v` before the await → still in TDZ →
} // ReferenceError: Cannot access 'v' before initialization
// after (correct): the temp is preserved, the read stays after the await
async function init() {
let num = await foo();
bar(v, num);
}
```
(With a *normal* late call — `init` imported and invoked after this module finishes evaluating — `v` is already initialized and there is no crash. The inliner still keeps the temp because it cannot prove the call is late.)
## How it works
A reorder turns a **working** program into a **crashing** one in exactly one shape: the moved read is a **block-scoped binding** (`let`/`const`/`using`/`class`/`enum`) that is **closed over from an enclosing function**. The mechanism:
1. The enclosing `async`/generator function is called *before* the binding's declaration has run, so the binding is in its TDZ.
2. The original reads it *after* an `await`/`yield`. During that suspension, outer code runs the declaration and initializes the binding — so the read succeeds.
3. Inlining moves the read *before* the `await` (before the suspension), so it now observes the TDZ and throws.
A binding declared in the **same** function can't trigger this: the body runs top-to-bottom on every call, so the binding is always initialized before the read — it can't be initialized "mid-suspension" by the function's own later code. `var` / parameters have no TDZ at all.
So the guard blocks the reorder exactly when the read is **block-scoped and closed over**, and detects "closed over" structurally — walking from the read's scope out to the binding's declaration scope and checking whether a **function boundary** is crossed first (suspensions only live inside function scopes):
```rust
// unsafe to reorder iff: block-scoped binding AND the read crosses a
// function boundary before reaching the binding's declaration scope
is_block_scoped(symbol) && read_crosses_function_boundary(read_scope, decl_scope)
```
### Blocked — closed over (kept)
Each is kept because `v` is a closed-over block-scoped binding and the inliner can't prove `init` is never called early (see **The bug** above). For the call-argument case, with that early call spelled out:
```js
let p = init(), v = ext(); // init() runs before `v = ext()` → `v` in TDZ
async function init() {
let num = await foo();
bar(v, num); // kept: merging would read `v` before the await → ReferenceError
}
```
The same guard covers a member assignment target (the object is read before the write) and generators (`yield` is a suspension point too):
```js
// member target
let v = ext();
export async function init() { let num = await foo(); v.x = num; } // kept
// generator
let v = ext();
export function* init() { let num = yield foo(); bar(v, num); } // kept
```
### Still inlined — no TDZ hazard
```js
// parameter: not block-scoped, no TDZ
export async function init(v) {
let num = await foo();
bar(v, num); // → bar(v, await foo())
}
// `var`: function-scoped, no TDZ
export async function init() {
var v = ext();
let num = await foo();
bar(v, num); // → bar(ext(), await foo())
}
// same-function lexical: initialized before the read
export function outer() {
const v = ext();
return bar(v, ext2()); // → return bar(ext(), ext2())
}
```
## Comparison with other minifiers
Verified by minifying each case and running the output (early-call repro where `init()` runs before `let v` initializes):
| Case | Sound to fold? | this PR | esbuild | terser | SWC |
|---|---|---|---|---|---|
| closed-over `let v` as call arg `g(v, num)` | no (TDZ) | keep ✓ | keep ✓ | keep ✓ | fold ✗ |
| closed-over `let v` as member target `v.x = num` | no (TDZ) | keep ✓ | keep ✓ | keep ✓ | fold ✗ |
| plain assign `v = num` | yes | fold | fold | keep | fold |
| same-function `const` skipped past | yes | inline | keep | inline | inline |
| parameter `v` as call arg | yes | fold | keep | keep | fold |
esbuild and terser already decline the unsound reorder; this PR aligns oxc with them on the two TDZ cases while keeping oxc's existing (sound) folds on the rest. SWC currently performs the same unsound reorder and reproduces the `ReferenceError`.
Part of #23043.
We don't enable `oxc_ast`'s `disable_old_builder` Cargo feature in any of our published crates, or crates that are depended on by published crates.
Tasks are private to the repo, so enable this feature on them, to ensure no usage of legacy `AstBuilder` methods sneak in during the transition period before we delete these methods.
## What
A `var` initialized to a falsy constant and never reassigned is always falsy. oxc already folds such a `let`/`const` everywhere, but a `var` is held back by a hoisting check (a read before the declarator runs sees the hoisted `undefined`), so a multi-read `var` flag past a non-trivial declarative prelude was never folded.
In **boolean context** that check is unnecessary: a pre-init `var` read yields `undefined`, which is indistinguishable from the falsy init inside `if (x)` / `x ? … : …` / `!x`. So fold such reads to `false` there; the existing `if (false)` dead-code pass removes the branch.
This is the bundled-flag shape behind this issue. Svelte's `var hydrating = false` is read by `if (hydrating)` throughout the runtime; once a client-only bundle tree-shakes the `set_hydrating` setter, `hydrating` is a write-once falsy `var` and the whole hydration path becomes dead and is eliminated.
## Why it is sound
- `SymbolValue::boolean_falsy` is set only for a write-once binding with a falsy constant initializer.
- It folds to `false` **only** in boolean context; value-context reads keep their hoisting-correct behavior (never folded here).
- Gated off for a script's top-level `var` (a global another script can reassign, so an in-module write count of 0 doesn't prove write-once) and for `eval` scopes.
## Verification
- `oxc_minifier` tests pass; idempotent.
- On a real client-only Svelte (Vite/Rollup) bundle, every `if (hydrating)` check is eliminated.
- `minsize`: react 23.14 → 23.12 kB, lodash 70.96 → 70.92 kB (gzip down too). `allocs_minifier.snap`: +20 sys allocs on antd (from evaluating `var` initializers; arena allocs unchanged).
- Cross-validated for soundness: on the Svelte bundle, terser `--module` and esbuild `--format=esm` independently fold the same write-once flag (16 → 0), agreeing with oxc. On a Vue 3 client bundle the runtime's flags are multi-write (e.g. `isInSSRComponentSetup` has two assignments, so it can be truthy) and oxc correctly leaves them untouched — the write-once gate is the right line.
## Scope (partial fix)
This handles the real-world **bundled** shape — a write-once falsy `var` flag read in boolean context (the Svelte/Vue hydration case, where the bundler has already tree-shaken the setter). It does **not** fold the issue's literal source snippet:
```js
let foo = false;
function bar(val) { foo = val; }
if (foo) { bar(true) }
console.log(foo);
```
There `foo` is still reassigned via `bar` (so it isn't write-once), and proving the write dead requires self-referential / flow-sensitive analysis — out of scope here (see #22753 for that direction). So this is a partial step.
Refs #14001.
Prepared with AI assistance.
## Summary
Mark side-effect-free `new` constructions pure so they are dropped when unused (and annotated for downstream tree-shaking):
- **TypedArrays** with a non-negative numeric-literal length (`new Int8Array(8)`, `new Uint8Array(1024)`). The buffer allocation runs no user code; a valid length is pure and a too-large length only throws a maximum-length `RangeError`, which the minifier is allowed to drop (see `docs/ASSUMPTIONS.md`). `new Int8Array(-1)` (negative-length `RangeError`), `new Int8Array(0n)` (BigInt `TypeError`), object, and non-literal arguments are kept.
- **`new Set([...])` / `new Map([[..], ..])`** with array-literal arguments; element side effects are preserved when the construction is dropped. `WeakSet`/`WeakMap` are excluded (their keys must be objects, so `new WeakSet([1])` throws), and `Map` entries must be array literals (`new Map([1])` throws).
This matches esbuild's per-constructor handling — the only other minifier that keeps every throwing form; Rollup, Terser (`unsafe`), and SWC drop some of these incorrectly. Cross-verified against the ES spec, esbuild, Terser, Rollup, and SWC.
Minified-size impact: react.development.js −0.04 kB; size-neutral elsewhere (real code uses dynamic buffer lengths, not numeric literals).
## Summary
The minifier skipped constant-inlining for **every** `var`, blaming TDZ. The real constraint is narrower — `var` *hoisting* — and most `var`s are still safe to inline. This lifts the blanket skip and inlines a `var`'s constant whenever no read can observe its hoisted `undefined`. Closes#13051.
## Why
A `var x = <literal>;` binds `x` to `undefined` at the top of its scope and assigns the literal only in source order. A read in that window sees `undefined`, so inlining the literal everywhere would change behavior there — which is why the old code skipped all `var`s. But that also blocked a common, perfectly safe pattern from optimizing in DCE mode:
```js
var DEBUG = false;
function check() { if (DEBUG) console.log('debug'); }
console.log(check());
```
vite 8's `topLevelVar: true` rewrites top-level `let`/`const` to `var`, so users' flag patterns silently stopped tree-shaking — vitejs/vite#22363 → rolldown/rolldown#9279.
## The safety principle
Inline only when the value at the read is provably the assigned literal, never the hoisted `undefined`. Concretely, no user code may run between scope entry and the assignment that could read `x`. A `var x = <literal>;` qualifies when all of:
- **It is in a still-declarative body.** It sits directly in the current function/program body, and every preceding statement is *declarative* — runs no user code (`is_declarative_body_statement`: function/empty/type-only declarations, module re-exports, and literal-initialised simple `var`s). The first non-declarative statement ends the body's "declarative prelude"; this is also enforced per-declarator for multi-declarator statements.
- **Its initializer is a `ConstantValue`.**
- **It is not a top-level script var** (those alias the global object).
- **At program scope, the module loads no other module** (`import` / `export … from` / `export * from`) — a cyclic importer could call into our exports and observe a captured var before assignment, regardless of export status.
- **It has exactly one read, crossing a function boundary** — the gap that `substitute_single_use_symbol` (same call frame) and the small-value inliner don't fill. This also keeps the change byte-positive: dropping `var x = L;` and replacing one read with `L` always saves bytes.
The prelude flag is a per-body stack pushed/popped at function boundaries, so the analysis runs independently for nested functions. Reassignment is handled by the existing `write_references_count` guard, and direct-eval scopes are excluded.
**Verification.** A battery of hoisting / closure / reassignment programs was run through the minifier and then *executed*, checking every output reproduces the original's observable behavior. All pass — output is semantically equal to source on each, matching Terser.
## Comparison with other minifiers
Behaviour checked against Terser 5.48, esbuild 0.24, and SWC 1.15:
- **esbuild** only propagates `const` primitives — it never inlines a `var` cross-scope. So `const DEBUG = false; …` folds to `console.log(void 0)` but `var DEBUG = false; …` stays. That is exactly the regression path: `topLevelVar: true` rewrites `const`→`var`, defeating const-only propagation. oxc had the same blanket-`var` skip; this PR lifts it for the provably-safe subset.
- **Terser / SWC** reach the same result (and more — flow-sensitive reassignment, call inlining) via multi-pass `reduce_vars`/`collapse_vars`. This PR recovers the common flag pattern with one syntactic prelude check instead of that machinery, staying single-pass.
## Results
`minsize` shows small gzip improvements on lodash / victory / antd / typescript; jquery is byte-identical.
Closes#13051
## Summary
Introduces `oxc_diagnostics::Diagnostics` — a `Vec<OxcDiagnostic>` newtype that knows the difference between errors and warnings — and makes it the standard diagnostics return value across oxc.
- **New type**: `Diagnostics` with `has_errors()`, `has_warnings()`, `errors()`, `warnings()`, plus `Vec`-like ergonomics (Deref, `IntoIterator`, `Extend`, `From`/`Into<Vec>`) so most call sites are unchanged.
- **Return types**: `ParserReturn`, `SemanticBuilderReturn`, `IsolatedDeclarationsReturn`, the React Compiler `TransformResult`/`LintResult`, and `TransformerReturn` now expose `diagnostics: Diagnostics` instead of `errors: Vec<OxcDiagnostic>` (the React Compiler's separate `errors`/`warnings` are merged into one severity-tagged list).
- **Behaviour**: a React Compiler **error** now stops the transform (no output), while **warnings** flow through and the transform continues. `compile()` aborts codegen only when `diagnostics.has_errors()` — warnings alone no longer abort (previously any diagnostic did).
## Breaking change
Consumers reading `*.errors` on these return values must read `*.diagnostics`. React Compiler consumers that read `.errors`/`.warnings` separately should use `diagnostics.has_errors()` / `diagnostics.errors()` / `diagnostics.warnings()`.
Parser/semantic/isolated-declarations fatality is otherwise unchanged. Builds on the React Compiler transform feature (#23201).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What
`a-- > b` was minified as `a-- >b` (a space before `>`) to avoid producing the `-->` token sequence. But `-->` is only an HTML close comment (`SingleLineHTMLCloseComment`) at the **start of a line**, and a postfix `--` always has an operand before it — so `a-->b` is never a comment and always parses as `(a--) > b`.
| Input | Before | After |
| --- | --- | --- |
| `while (i-- > 0) f()` | `while(i-- >0)f()` | `while(i-->0)f()` |
| `x = a-- > b` | `x=a-- >b` | `x=a-->b` |
Verified in node (`while(i-->0)` yields `[2,1,0]`). This goes one step beyond esbuild, which keeps the conservative space.
The `< ! --` guard (avoiding `<!--`, which *can* appear mid-line) is left untouched.
## What
Found with the whitespace analyzer. The postfix branch of `UpdateExpression::gen_expr` called `print_space_before_operator(operator)` **before** printing the operand. For a postfix `x++` the `++` comes *after* the operand, so this treated the trailing `++` as if it immediately followed a preceding `+` (the `+`/`++` guard exists to avoid `+++` in prefix `x + ++y`).
Result, when minifying:
| Input | Before | After |
| --- | --- | --- |
| `x + y++` | `x+ y++` | `x+y++` |
| ``a + `-` + i[1]++`` | ``a+`-`+ i[1]++`` | ``a+`-`+i[1]++`` |
## Fix
Matches esbuild's `EUnary` printer: for postfix, print the operand first, then call `printSpaceBeforeOperator` immediately before the operator text. The guard `prev_op_end == code.len()` then makes it a no-op (the operand was printed since the last operator), so `x+y++` stays tight. Required spaces from the prefix/binary guard are unaffected:
- `x + ++y` → `x+ ++y`, `x - --y` → `x- --y`, `b + +c` → `b+ +c`
Output matches esbuild exactly. Codegen (116) + minifier (506) pass; minsize updated.
## 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.
## Summary
- Force plain string quotes for `"default"` / `"__esModule"` operands of `==`/`===`/`!=`/`!==` during minification, so `cjs-module-lexer` can recognise the `Object.keys(mod).forEach(key => key === "default" || ...)` re-export filter emitted by Babel / TypeScript / Rollup.
- Follow-up to #22475 (which fixed `require("…")`). Same root cause: `calculate_quote_maybe_backtick`'s equal-cost tie-breaker prefers backtick, which the lexer's heuristic parser doesn't accept.
- Caught by monitor-oxc's `whitespace` runner: minifying `@babel/types/lib/index.js` previously broke `import { isIdentifier } from "@babel/types"` under `--experimental-require-module`. Raw minified size is unchanged across all `minsize` fixtures.
## Example
Input:
```js
Object.keys(mod).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
});
```
Before (broken — `cjs-module-lexer` skips the re-export):
```js
Object.keys(t).forEach(function(e){if(e===`default`||e===`__esModule`)return});
```
After:
```js
Object.keys(t).forEach(function(e){if(e==="default"||e==="__esModule")return});
```
AI disclosure: drafted with Claude Code, reviewed manually.
`value_type` for `Expression::Identifier` returned `Undetermined` for
any non-global identifier, **even when the binding had zero writes and a
known constant initializer**. This causes code like `slot?.()` on a `let
slot;` with all writers tree-shaken away not being folded to `void 0`,
leaving dead code in the output.
This PR changes that by checking
`GlobalContext::get_constant_value_for_reference_id` for non-global
identifier references and mapping the resulting `ConstantValue` to a
`ValueType` via a new private helper.
Once the identifier resolves to a tracked nullish value, the existing
nullish branch in `fold_chain_expr` collapses the chain to `void 0`. The
same lookup also works for the opposite case (#21923).
The lookup is gated by making sure there are no write references, so
anything that is being written anywhere keeps its `Undetermined` type.
Closesrolldown/rolldown#9281
Related (but does not resolve) #21923
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Closes#22463
## Summary
- Add `try_print_require_call` to force plain string literals (no
backticks) for `require("...")` calls during minification
- Prevents `cjs-module-lexer` from failing to detect CJS reexport
sources
### Problem
When minifying, `StringLiteral::gen` passes `allow_backtick: true`,
which can convert:
```js
__exportStar(require("./decorators"), exports);
```
into:
```js
__exportStar(require(`./decorators`),exports);
```
Node.js uses `cjs-module-lexer` for CJS/ESM interop. This lexer uses a
fast heuristic parser that does **not** recognize template literals as
`require()` arguments, causing named reexports to silently disappear
from the module's export list.
### Fix
Add `try_print_require_call` (following the existing
`try_print_cjs_define_property_call` pattern) that detects
`require("...")` calls via the existing
`CallExpression::common_js_require()` helper and prints the string
argument with `allow_backtick: false`.
## Test plan
- [x] `require("./decorators")` stays quoted when minified
- [x] `require('./foo')` normalizes to double quotes (not backticks)
- [x] `require(foo)` (dynamic) is unaffected
- [x] Non-require calls like `foo("./bar")` still get backtick
optimization
- [x] `cargo test -p oxc_codegen` passes
- [x] Follows existing CJS special-case pattern in codegen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: zennnnq <ennanzhou@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Fix `cjs-module-lexer` failing to detect `Object.defineProperty(exports, "name", ...)` named exports after oxc minify.
`calculate_quote_maybe_backtick` favored backtick when all three quote costs tied at zero, so a plain ASCII property name like `"getInclusionReasons"` was emitted as a backtick template. [`cjs-module-lexer`](https://github.com/nodejs/cjs-module-lexer) (used by Node for CJS/ESM interop) only matches plain string literals there, so the affected exports silently dropped out of its export set — downstream ESM consumers then failed with `Named export 'X' not found`.
In `CallExpression::gen_expr`, detect `Object.defineProperty` / `Reflect.defineProperty` and print the 2nd argument via `print_string_literal(name, false)` (the existing `allow_backtick: false` path). Other arguments — and nested strings inside the descriptor — still benefit from backtick optimization. Minify-only: outside minify, `print_string_literal` uses `self.quote` (never backtick), and `print_arguments` is needed there to preserve argument comments.
### Before / after
```js
// input
Object.defineProperty(exports, "getInclusionReasons", { ... });
```
```js
// before
Object.defineProperty(exports,`getInclusionReasons`,{...});
// after
Object.defineProperty(exports,"getInclusionReasons",{...});
```
Closes#22342
AI assisted.
fixes https://github.com/oxc-project/oxc/issues/21296 by treating `this` as potentially having side effects.
We can reduce the case that is bailed out by checking whether `this` is written inside `constructor` or not in the future.
Fixes#20802
This fixes a bug in single-use variable inlining where object literals
with non-computed string keys like `"-a"` were incorrectly treated like
computed properties and stopped further substitution.
Closes#20288
Adds numeric conditional expression optimization to the minifier:
- `x ? 1 : 0` → `+x` when `x` is known boolean (saves 3 chars)
- `x ? 1 : 0` → `+!!x` when `x` is unknown type and no parens needed
(saves 1 char)
- `x ? 0 : 1` → `+!x` when no parens needed (saves 2 chars)
- Skips when parentheses would make the result the same length or longer
(e.g., `a+b?1:0`)
Profitability checks are based on the lessons from swc-project/swc#9920.
Implementation note: uses `DetermineValueType` to check if the test
expression is boolean, and a simple `test_needs_parens` helper (matching
`BinaryExpression`, `LogicalExpression`, etc.) to determine if wrapping
would negate the savings.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## Summary
close: #13067
Part of https://github.com/oxc-project/oxc/issues/19673
Depends on #20217.
Port known-globals lists from Rolldown to Oxc's side-effect detection:
- **Global identifiers** (Math, Array, console, DOM classes, etc.) are now considered side-effect-free to access via `is_known_global_identifier`, matching Rolldown's `GLOBAL_IDENT` set
- **Property reads** like `Math.PI`, `console.log`, `Object.keys` are considered side-effect-free via `is_known_global_property`
- **3-level chains** like `Object.prototype.hasOwnProperty` are supported via `is_known_global_property_deep`
## Test plan
- [x] Added tests for known global identifiers (Math, Array, Object, console, etc.)
- [x] Added tests for known global property reads (Math.PI, Object.keys, etc.)
- [x] Added tests for 3-level chains (Object.prototype.hasOwnProperty, etc.)
- [x] Existing minifier test expectations updated for improved side-effect detection
- [x] All existing tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Disable flipping of negated `if` statement when `alternate` is `if`
statement
when `if else if` is encountered, flipping always produces at-least one
extra character (in many cases 5) as new `consequent` has to be wrapped
in `{}`, this also prevents `remove_dead_code` and `minimize_statements`
to be run to covert if statement's to ternary expression
## Summary
- Adds minification optimization to remove empty `case` clauses that precede a trailing `default` clause when those cases contain only primitive literals
- Port of https://github.com/evanw/esbuild/commit/add452ed51333953dd38a26f28a775bb220ea2e9
### Example
```javascript
// Before:
switch (x) { case 0: foo(); break; case 1: default: bar() }
// After:
switch (x) { case 0: foo(); break; default: bar() }
```
## Test plan
- [x] Added tests for basic case with empty literal before default
- [x] Added tests for multiple empty literal cases
- [x] Added tests for non-literal identifiers (should NOT be removed)
- [x] Added tests for default not at end (preserved)
- [x] Added tests for string, null, and BigInt literals
- [x] All existing minifier tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)