Commit Graph

1065 Commits

Author SHA1 Message Date
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
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
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
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
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
Dunqing 76b19f503b fix(minifier): avoid duplicating large folded strings (#25532)
Constant evaluation can fold a string addition through a tracked constant even when direct identifier inlining rejects that value as too large. This materializes the large string at every reference and can double chunks containing embedded assets.

This change shares constant materialization eligibility between direct inlining and string-add folding. A fold that would grow the expression through a non-inlineable constant is skipped, while single-read and cheap constants remain foldable. The policy follows the conservative strategy used by esbuild and SWC; permanent source links are included beside the helper.

## Example

Input:

```js
const p = "…1 MB payload…";
export const a = atob(p);
export const b = "y" + p;
```

Before, Rolldown 1.2.3 emitted two copies of the payload, growing the output from about 1 MB to 2 MB. After, `p` remains a binding and the payload is stored once.

Verified with `cargo test -p oxc_minifier`, `just minsize` (no minsize or allocation snapshot changes), `cargo clippy -p oxc_minifier -- -D warnings`, and `just fmt`. Regression tests cover both full compression and DCE.

Fixes rolldown/rolldown#10656.

AI-assisted. The contributor remains responsible for reviewing, understanding, and submitting the changes under Oxc's AI usage policy.
2026-08-13 00:53:00 +00:00
Dunqing c3e99d160d fix(minifier): avoid invalid octal escapes in template folds (#25495)
Inlining constant template expressions can join a decimal digit onto a
preceding raw `\0`. The resulting `\00` is a forbidden octal escape
inside template literals, so DCE can emit JavaScript that fails to
parse. This surfaced in rolldown/rolldown#10661.

When a fold would create that boundary, serialize the null as `\x00` and
continue merging the template fragments. This preserves the cooked value
and the optimization, matches Terser and esbuild, and leaves
escaped-backslash cases unchanged.

## Example

Input:

```js
return `${messageId}\0${correlationId}\0${0}`;
```

Before:

```js
return `${messageId}\0${correlationId}\00`;
```

After:

```js
return `${messageId}\0${correlationId}\x000`;
```

Verified with `cargo test -p oxc_minifier` (643 passed, 35 ignored), the
DCE example's `--twice` idempotency check, a runtime equivalence check,
`just minsize` with unchanged size/allocation snapshots, Clippy with
warnings denied, and formatting. `just ready` additionally passed
dependency, typo, generation, formatting, workspace checks, and all
minifier tests; the remaining workspace run stopped on three unrelated
Oxlint ANSI-color snapshots because the environment sets `NO_COLOR=1`,
and a color-enabled rerun was blocked by the filesystem running out of
space.

Refs rolldown/rolldown#10661.

Implemented with AI assistance (OpenAI Codex). The contributor remains
responsible for reviewing, understanding, and submitting the change
under the Oxc AI usage policy.
2026-08-12 09:46:26 +08:00
Armano 5fcf6839ce fix(minifier): correct issue with try finally termination (#25185)
fix bug with termination detection for `try finally`, 

when try has termination stmt and finally is missing it and we do not
have catch stmt current code passes it and allows stmt to fold

```js
// base
function f() {
	if (a) try { return g(); } finally {}
	else i();
}
// main
function f() {
	if (a) try { return g(); } finally {}
	i();
}
// this branch
function f() {
	if (a) try { return g(); } finally {}
	else i();
}
```

this issue has been introduced by
https://github.com/oxc-project/oxc/pull/24441 and copied over by
https://github.com/oxc-project/oxc/pull/24950


fixes #25214
2026-08-11 10:27:54 +08:00
Armano e0fb2d782a fix(minifier): avoid unsafe return removal in labeled blocks (#25406)
revert #24813 currently there is no way to validate if stmt can be
removed if its inside a block

correct issue with incorrect removal of `return` stmt not at termination
points


```js
(function() {
  label: {
    console.log('before return');
    return;
  }
  console.log('unreachable!');
})();
```

ref: https://github.com/rolldown/rolldown/issues/10555
2026-08-09 12:59:54 +01:00
camc314 a33788e13e feat(ast)!: group class heritage into ClassHeritage (#25360)
## Summary

- Groups `Class::super_class` and `Class::super_type_arguments` into `Class::heritage`.
- Introduces `ClassHeritage` containing the superclass expression and its optional TypeScript type arguments.
- Makes superclass type arguments without a superclass expression unrepresentable.

## Breaking Change

The Rust AST changes from:

```rust
pub struct Class<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub r#type: ClassType,
    pub decorators: Vec<'a, Decorator<'a>>,
    pub id: Option<BindingIdentifier<'a>>,
    #[ts]
    pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
    pub super_class: Option<Expression<'a>>,
    #[ts]
    pub super_type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
    #[ts]
    pub implements: Vec<'a, TSClassImplements<'a>>,
    pub body: Box<'a, ClassBody<'a>>,
    #[ts]
    pub r#abstract: bool,
    #[ts]
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}
```

to:

```rust
pub struct Class<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub r#type: ClassType,
    pub decorators: Vec<'a, Decorator<'a>>,
    pub id: Option<BindingIdentifier<'a>>,
    #[ts]
    pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
    pub heritage: Option<ClassHeritage<'a>>,
    #[ts]
    pub implements: Vec<'a, TSClassImplements<'a>>,
    pub body: Box<'a, ClassBody<'a>>,
    #[ts]
    pub r#abstract: bool,
    #[ts]
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub struct ClassHeritage<'a> {
    pub expression: Expression<'a>,
    #[ts]
    pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```

## Why?

ECMAScript defines the complete `extends` clause as `ClassHeritage`:

```text
ClassHeritage : extends LeftHandSideExpression
```

TypeScript additionally permits type arguments on the heritage expression:

```ts
class Derived extends Base<T> {}
```

Previously, the AST stored the expression and type arguments independently:

```text
Class {
    super_class: Some(Base),
    super_type_arguments: Some(<T>),
    ...
}
```

This allowed an invalid state to be constructed:

```text
Class {
    super_class: None,
    super_type_arguments: Some(<T>),
    ...
}
```

Superclass type arguments cannot exist without an `extends` expression.

The parser already treated these fields as a pair. It parsed a class heritage entry and then split its expression and type arguments into two independent fields. Builders and transforms therefore had to preserve their relationship manually.

Grouping them into `ClassHeritage` makes the syntactic dependency explicit and prevents consumers from observing or constructing contradictory class heritage.

## Class heritage forms

A class without an `extends` clause has no heritage:

```js
class Base {}
```

```text
Class {
    heritage: None,
    ...
}
```

A class extending an expression has heritage without type arguments:

```js
class Derived extends Base {}
```

```text
Class {
    heritage: Some(ClassHeritage {
        expression: Base,
        type_arguments: None,
    }),
    ...
}
```

TypeScript type arguments belong to the same heritage structure:

```ts
class Derived extends Base<T> {}
```

```text
Class {
    heritage: Some(ClassHeritage {
        expression: Base,
        type_arguments: Some(<T>),
    }),
    ...
}
```

The heritage expression may be any valid class heritage expression:

```js
class Derived extends mixin(Base) {}
class NullDerived extends null {}
```

## How to migrate

Code that previously accessed the superclass expression directly:

```rust
if let Some(super_class) = &class.super_class {
    // ...
}
```

can use the compatibility accessor:

```rust
if let Some(super_class) = class.super_class() {
    // ...
}
```

Code that needs both parts should access the grouped heritage:

```rust
if let Some(heritage) = &class.heritage {
    let expression = &heritage.expression;
    let type_arguments = heritage.type_arguments.as_deref();
}
```

Mutating consumers should update the nested fields:

```rust
if let Some(heritage) = &mut class.heritage {
    heritage.type_arguments = None;
}
```

Common migrations:

| Previous representation | New representation |
| --- | --- |
| `class.super_class` | `class.heritage.as_ref().map(\|h\| &h.expression)` or `class.super_class()` |
| `class.super_type_arguments` | `class.heritage.as_ref()?.type_arguments` or `class.super_type_arguments()` |
| `class.super_class.is_some()` | `class.heritage.is_some()` |
| Mutate `class.super_class` | Mutate `class.heritage.expression` |
| Clear `class.super_type_arguments` | Clear `class.heritage.type_arguments` |
| Pass superclass and type arguments separately to a `Class` builder | Construct and pass `ClassHeritage` |
| Clone both fields independently | Clone `class.heritage` |

```ts
superClass: Expression | null;
superTypeArguments?: TSTypeParameterInstantiation | null;
```

No `heritage` property or `ClassHeritage` node is added to the ESTree output.

fixes https://github.com/oxc-project/backlog/issues/216
2026-08-07 14:18:59 +00:00
Armano 697c6294b0 fix(minifier): preserve Annex B labeled-function bindings in implicit-return rewrite (#25349)
fixes #22223
2026-08-07 19:13:42 +09:00
camc314 5c5cdcd522 feat(ast)!: narrow TSInterfaceHeritage::expression to TSTypeName (#24360)
## Summary

Narrow `TSInterfaceHeritage` from a general JavaScript `Expression` to `TSTypeName`.

```rust
pub struct TSInterfaceHeritage<'a> {
    pub type_name: TSTypeName<'a>,
    pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```

## Breaking Change

The Rust AST field changes from:

```rust
expression: Expression<'a>
```

to:

```rust
type_name: TSTypeName<'a>
```

## Why?

The current AST can represent shapes such as:

```ts
interface A extends foo() {}
interface A extends A + B {}
interface A extends new Foo() {}
interface A extends true {}
```

Which is an overly wide type since all of the above variants are invalid.

This PR tightens up the AST, making these invalid variants irrepresentable.

## How to migrate?

This should be a trivial migration:
1. instead of accessing `expression` on `TSInterfaceHeritage`, now access `type_name`
2. change any pattern matching on the `type_name` field to use `TypeName` (this is a smaller subset so should allow deleting code!)

closes https://github.com/oxc-project/backlog/issues/215
2026-08-07 06:00:42 +00:00
Armano 6529d1ae13 perf(minifier): short-circuit scope check when alternate already determines result (#25318)
add a short-circuit to avoid iterating over remaining statements when
the `alternate` branch already determines
`can_move_branch_condition_outside_scope` is `false`.

llvm is not able to short circuit it:
[godbolt](https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1DEArgoKkl9ZATwDKjdAGFUtEywYgATKUcAZPAZMADl3ACNMYm8AdlIAB1QFQjsGFzcPb3jE5IEAoNCWCKivWKtMGxShAiZiAjT3Tx8yioEqmoI8kPDImMtq2vqMpv6OwK7CnpKASktUE2Jkdg44kzCAakZ3NbaCTDZBNYBSaIAhQ40AQTXrtZPaVGQAax29xgtzq5uANRq8JjD6AARcoGYhMWwCCBhVCuGYfG5rADyBAQkVIH2OgIxlxW6yoDDW5nBr0EAH1RMRMApSf85gRSQo0HFMBBzCwCCAjl4AGwvfYEKZrAC0hwAzNg1tDXEdTvCbixwcgEISCOyZWdLgiEXy3iAQHcHs9qrt%2BUdRVjxWsqGIlOjNVrrjrBHqfsQ/gDMMDkKDwSkIHhqQA3GqCsUWiVgMAB0nBshyh2ks3htYEUyYO2fa6YjHRLGXDFeUWHACsJ0CtDGECCgciUxLeYuuKtBJIeGAgTEEDEu2IzF2nMRcQhDDFTkOPKdBDF2FIKvZCk54%2B5JZOk/rs4oazMeAAXphBSLLVLaOr42t6AQ1iwTJfKQoTLRL2G1pHu5E%2B5gAHTRhSoNg0hh0FZY0SXpCkqRpaEbwZJl9zFDVMzWPAqHPTBLyEP8WTvAUkznAgFE/YA0P9LkTjWLhP0/UNZXtB1%2BGIPCkIJbDT1oh0bmQlViX5ckagg2loMZVBmWA9lqIQ9jJLWO8Hyfc0rRtTB4LPKSbjCSkmEeZS2Mk7MdJuPTEMMhEZMfHNAQ4GZaE4YteE8DgtFIVBOAAJTMS9f3mRYuVFHhSA5BzLNICAkDQFg4joNEKAgMKIvoKJiC4ABOaIND4OgewXKFNF4MIO2IABPTg/Ly5hCsRMJtHKAK/LC/lEQYWgisC0gsDCExgCcMRaAXbheCwBUjHEFr8EpCoa16xzMFUcobyWPzAl2ayWordSagKlwsGK3hUzwFhttIGtiGhJRgUG4AKyMHKZioAxgAUL48EwAB3QdGAO/hBBEMR2CkGRBEUFR1Ba3QuH0QxjHc/Q8DCBdIBmYTh16oUnDWRGhXoGtaDDUVeFQI63SwOGoGYNgQEwfBh0OsQTCWLwNC8SRLJmZphwcQDBk8MH/DGAoij0BIkmHTmBeyYdOj5nowdZyoRhF6XMGsYc2lqCXuiiaW5dcBo9CJVXefViQWbmBZfqsmy7JypzOGk9zkHIz8Us/DQ1ggXBCBIHyuCmHbrpmR5vG5T9uR5UUNCSpLku5UViySjQAA59E4JnSH2rgNDS%2BzHOcjheAXNKAq0GY4FgUKMBwSmSHISgaHoUn5t4T7hFEcQ/qbwG1Ct3QfAMK7THMSxFeqlJ2ecbWhl8QC1YmYowcFnJUnHxosiFlJp/5rwFaV2X2hFpoh5aBgVdGfJDc3vpd6XzI9ZP8YN%2B92YvKWJtNhYbYQNNY4JJuA0nknDMESundECEENRfSQmPHCNiyJURxnzLmbEjZVjNi4iaN4vE7yQTpDBYSLI2Qci5LyD%2BbwDzTklDCE8X8zwKgIEqRiVD9KOmIc6fU9w/7MLksma0PV0wqRuJOF0vx/ggO9GA4c/ogwhiTGQyM0ZYwAPYomMMZDUy0wUVmBB8CGwFiLKWcslZqy1nrB8Js%2BIkLUl/P%2BQw6AYxCMEF2R875iQDiHCkUcS41zilnPghchCVxrmLICDcnJtx7lIUeChrFEKvkcb2Yk34LGYQAkBIkaCyTgWpAJekQlmR1kYX4nkL4wA%2BMIsRPApEHZUQSQyJJ1iIDHCcNhBpUTVLXGwt%2BHsEApifkMAVYC3F0EZKwYJWCeTEIIkxGMzEQVlq2VIFnPGrl3KEhNt5ccoovC%2B0CsXMu4VIpkGirFfZIBEopTSrXTKlAwhW1KutA6tzyqVWqgdOqbwGpNStm1DqXVaA9QOgNCGw1HKjWHhNK201Zr9j6uQQQisrarTBIVTaSxHK7X2tCo6J1PR7EBYEUA2y%2BB3Qek9V6zJ7J%2BSbt9Vu0h25KE7iDTIvdIYD1WsTBGriBDI1RujTGiscZ4wJngIm8AqysHYBTBe1M3B0wZkzKYLMD5swgI4eWk90DrylivBeqr57iwNjPXWiqd4DCvlvYerQRgao1hfE16QuY2tvpLDWxsn7eHNhwOZCzrYcFtuYe2FEnYuzdpXBi6yvA%2B38n7UgqImBYCiF00gAcvDFk/KKDZjNojRC8JHUU8c/rLRTmnDO8yrY5zziAAu11gowEQCAfAVAqDV2oLIKlv0aWyA7sDRytAEALjBj2hQ9aqAEAKsyCtpBiC9syJOwdyFh2jvYBod1nrS2cEBHOtYj0XqRDWG5cwKyn5e19bQh2gbhTcqHBjTAWMcZrEDAoXdyzPKmx8l4Y9/rHbRGduetGl7eXY3NLjSN2yZgxrjZQf2IBJDx2dpIKQkhCzFi4F4ZNSVuRJw4CnL1ZbLDjsLszDDmyS0tRw/h%2BVh1IhJHsJIIAA%3D)

alternatively we could just add
`can_move_branch_condition_outside_scope` check before `if let
Some(stmts) = stmts.get(i + 1..) {`, but that generates eq assembly
2026-08-06 15:05:00 +01:00
Armano f3c7f118d4 refactor(minifier): use slice pattern instead of indexed access in for-in handler (#25323)
replace `declarations.len() == 1` + `declarations[0]` with
`declarations.as_slice()` and a `[decl]` slice pattern and merge
`is_block_scoped` and `has_side_effectful_initializer` conditions

in production code, llvm is smart enough to rewrite this to eq asm, but
that is not a case for napi see
[godbolt](https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1DEArgoKkl9ZATwDKjdAGFUtEywZ7HAGTwNMAHLuAEaYxCAATAAcpAAOqAqEdgwubh568Ym2Ar7%2BQSyh4dGWmNbZDEIETMQEqe6eXCVlyZXVBLmBIWGRMeZtdemNfTUd%2BYU9AJSWqCbEyOwcAKQRAMyLAKwAQljEeABumBBOtAKYExsAIosaAILmpjYA1BeYyLSPiwDsm48A1n7oAD6eAUgL21RAj2CqFcpEefkIgIQTFBiSwgMwVCorwICkh0NcH0%2BV1u1xujHcj28mIIRM2ZMejMeADVqngmMF6C83tUmOUIMzXosVk5ubRhdgpgymQB5AgIMKkMlfEk3ZWrDabPy0PyHfwHYjndaq2ImYKPKgMR4kPDAPxiCD0KgESHLABs1OdcKUAEcTIx5nioTDaBNHgBaCXBwlfem3JnwqiPX3%2BhiBunShOM%2Bi05GovDozHYmxUEy0YEMJJiPAALzCHxWF0eLD5yAQjydtNjmazCc9LpArN2HK5rwMxD5yQgWDeCjDwquK2wGfjvbXjLwSZntAUADp6AwIPPGwvHlwVzd11emduFJqNJdd/8GECQWDqh8Im73Y9b/fHwiBBIiigJopgGJYjid6rteCYqo8pRKBesFXgQpiYD2KEqpha7YTBV79iAIBygqxANouy5UGIShKvhcHEsKcaXmuObwqCnKoMgvygWgsSYOg5HNq2CoKGAkCdnChGDmyI6YGKvL8reYabo8Ym3ruNDEOYR67i2sSAiQEBof6cJfE46Bmb%2BT4AsCoLgoa5wrEx65iXmoEFuBRY4qW5aAeyOp1mR7o/mJb4cVxPGoHxFn4fBiGYMha5UTuGGxQxpLpTcHBTLQnDrLwngcFopCoJwABKZi0goMxzAlywrDwpAukV2WkBASBoCwsR0IqFAQJ13X0OExBcAAnOsGh8HQBBhHiEDBJovDBPaxAAJ6cI1y3MGtMrBNoOIbbwnVsIIMoMLQ60taQWDBCYwBOGIO6HddmAtkY4hXfgxA4vsmB4ldmCqK8Jgzc9fgzblV06sEE5rS4WDPWheAsM9BrQkoLxvcAOpGItUxUAYwAKMyeCYAA7jKfGFY1/CCCIYjsFIMiCIoKjqFduiNAYuOmOY%2Bh4MEeKQFMUXlP94ZONasQEOG9AHOKjY1rwqAGrsWBC1AzBsCA/FJAIpDgm4CwRBoESSNlUxWD99gQI4AyeJ8bqkD4upjN06yjXECR6ykrj1CAjte1kySjF04Qe001sVFUNT23oVs2C0MftK7Yfx8ncdDMnocFN0XCWzV8wSDleUFYtJWcI8vMEMgZ67uNu4aI8EC4IQJCfg1Ey8M1WgTFMvyRG6u5ul%2BKwaKNo1jW6KwexoMSQ%2BbpAo1wGiTYVxWlRwvB4pNPetXAsAdRgOD4EQZB9TQB6sAsNOyPT4hM7T8hKGo5e6BE%2BiGMYlWR4nNt237QYjQXZ5DTlwGImQfaZ0aJA8oOdxjgN/uUVosdAENCGKUKOKCU6gNzuERBwxahoIkFnNo8C85RALrMIuSwNRbB2L9I4Jx/BGlVPcEwTwxR0j%2BDZN89l8QhjhIBYC%2BZCyQRsEGAk7w8JqluBSFgVIaSJUZEOdknI5JjgUlOQUyBhSijHBKKU%2BESKKmVJldUawtjal1BAfUYRWFklNOaS0UtbAsFrPxR0NJXRfn7N6TAfoAx/QEa4MMkYlzRmkd8HsKkUxBIUMorMrFRAMArDjOqjYhI13bJ2RJ64pKqNkvJCcikxxznIlGFs2TfxlN3CBBQOp5hHjybBTU25LgVIiduayL5bLviCl%2BEKYBunCLcmBCCxZcS0WYihRkgJOmUWopgaZWFiQrNglJExQUTwROMssnCjJsJOQOY8FJaTdQ9jiilFpjJkpKEueYzKrVIb5VIOvZW5VKrJkLnVVYERu54zakfLqPVz6UAGqCkAI1xqTUvjNLSlAFpXS2tUS6m0VqrV2vtGwz1jqMAIGdC65cbp3QerQJ63BeBYCxh9YqX0foHH%2BsVQGwNQaUvIIIUo5doaw1WvDBYxUkYo3ZWjBIGiaV%2BFAC1fGhNiakwplTZ6T976M2kE/Vmr8OaRE/jzH%2B0MNYi2lskcWktRay0wPLBcSsSqqw8hrWx18dan2SAbMQ/pOAmzNhbJByQHAvkzh/EBnQ8ESE9rA5IAag4%2B3Ifgz2CdkEZ2IRg5oAhsExvTv0JNlhs6pxDfnaY1D2ARBLhwV57yK4cCrpVWuXB67rEbs3VuZ8O7FoBdKqYComA7EoP3SI9bRoRDGp8LgnxJCjRWKNN0khTb6E4IvZeq83nl03tvEAu9AUH0QCAfAWJyCUGVaIB%2BarZAavZsVWgCA8Rc0vTu50q0%2BJrtIMQS92rn0KFvQQe97ANAlrLcuzgFxNxJhJuTesFVzDfMLR3c81ca11obRLVx5rLWK0eHsBJ4Gqo/JbVW8wcGG5N0Q2auWpQrVtt7h2zAXbuhHlIAPSQnxdySHWCO8BKwx6TsdqNaQC8y5XRXZYR9e8%2B6zo4P8pd/HODke9QaRI9hJBAA%3D%3D%3D)
2026-08-06 12:01:48 +01:00
camc314 6be314f226 feat(ast)!: remove duplicated VariableDeclarator::kind (#25319)
## Summary

- Removes `VariableDeclarator::kind`.
- Makes `VariableDeclaration::kind` the single source of truth for every declarator in its declaration list.
- Makes contradictory parent and child declaration kinds unrepresentable.

## Breaking Change

The Rust AST changes from:

```rust
pub struct VariableDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub kind: VariableDeclarationKind,
    pub declarations: Vec<'a, VariableDeclarator<'a>>,
    #[ts]
    pub declare: bool,
}

pub struct VariableDeclarator<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    #[estree(skip)]
    pub kind: VariableDeclarationKind,
    #[estree(via = VariableDeclaratorId)]
    pub id: BindingPattern<'a>,
    #[ts]
    #[estree(skip)]
    pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
    pub init: Option<Expression<'a>>,
    #[ts]
    pub definite: bool,
}
```

to:

```rust
pub struct VariableDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub kind: VariableDeclarationKind,
    pub declarations: Vec<'a, VariableDeclarator<'a>>,
    #[ts]
    pub declare: bool,
}

pub struct VariableDeclarator<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    #[estree(via = VariableDeclaratorId)]
    pub id: BindingPattern<'a>,
    #[ts]
    #[estree(skip)]
    pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
    pub init: Option<Expression<'a>>,
    #[ts]
    pub definite: bool,
}
```

## Why?

A variable declaration has one declaration kind shared by every declarator in its declaration list:

```js
const first = 1, second = 2;
```

Previously, the AST stored that kind on both the parent declaration and every child declarator:

```text
VariableDeclaration {
    kind: Const,
    declarations: [
        VariableDeclarator { kind: Const, ... },
        VariableDeclarator { kind: Const, ... },
    ],
}
```

This allowed contradictory ASTs to be constructed:

```text
VariableDeclaration {
    kind: Const,
    declarations: [
        VariableDeclarator { kind: Var, ... },
    ],
}
```

Different consumers read different copies:

- Code generation used `VariableDeclaration::kind`.
- Semantic binding used `VariableDeclarator::kind`.
- Lint rules, transforms, and minifier passes used a mixture of both.

As a result, one AST could be emitted as `const` while its binding was treated as function-scoped `var`.

Transforms also had to keep the copies synchronized manually. For example, const-to-let normalization updated the parent and then looped through every declarator to update each child. Explicit resource management performed similar paired mutations for `using` declarations.

Keeping the kind only on `VariableDeclaration` removes this synchronization requirement and prevents consumers from observing conflicting declaration semantics.

## Declaration forms

The declaration kind now belongs exclusively to the node that owns the declaration list:

```js
let value;
```

```text
VariableDeclaration {
    kind: Let,
    declarations: [
        VariableDeclarator { id: value, ... },
    ],
}
```

Multiple declarators share the same parent kind:

```js
const first = 1, second = 2;
```

```text
VariableDeclaration {
    kind: Const,
    declarations: [
        VariableDeclarator { id: first, ... },
        VariableDeclarator { id: second, ... },
    ],
}
```

## How to migrate

Code that previously read the kind from a declarator:

```rust
if declarator.kind.is_const() {
    // ...
}
```

should now read it from the containing declaration:

```rust
if declaration.kind.is_const() {
    // ...
}
```

Consumers operating through an AST node store should retrieve the parent `VariableDeclaration`:

```rust
let AstKind::VariableDeclaration(declaration) =
    nodes.parent_kind(declarator.node_id())
else {
    unreachable!();
};

let kind = declaration.kind;
```

Common migrations:

| Previous representation | New representation |
| --- | --- |
| `declarator.kind` | Containing `declaration.kind` |
| Match `VariableDeclarator::kind` during semantic binding | Read the parent `VariableDeclaration` from ancestry |
| Pass `kind` to `VariableDeclarator::new` | Remove the `kind` argument |
| Update parent and every child kind | Update only `VariableDeclaration::kind` |
| Recover kind after detaching a declarator | Capture or pass the parent kind before detaching it |
| Inspect a symbol’s declarator kind in lint rules | Resolve its parent declaration through the node store |

When constructing declarators, use the generated kindless `VariableDeclarator` builders and place them inside a `VariableDeclaration` carrying the required kind.

fixes https://github.com/oxc-project/backlog/issues/237
2026-08-05 16:31:46 +00:00
camc314 44fd320324 feat(ast)!: split TS external modules & Namespace Declarations (#25284)
## Summary

- Splits `TSModuleDeclaration` into two dedicated declaration nodes:
  - `TSExternalModuleDeclaration` for string-literal modules and module augmentations.
  - `TSNamespaceDeclaration` for identifier-based `module` and `namespace` declarations.
- Removes `TSModuleDeclaration`, `TSModuleDeclarationName`, `TSModuleDeclarationBody`, and `TSModuleDeclarationKind`.
- Makes invalid combinations of names, bodies, and declaration kinds unrepresentable.
- Ensures only namespace declarations introduce a normal binding.
- Updates all AST consumers, generators, traversal, formatting, code generation, semantic analysis, lint rules, transforms, and serialization.
- Keeps normal and eager-raw ESTree output compatible: both declarations remain `TSModuleDeclaration` nodes.
- Allows experimental lazy raw deserialization to expose the two new node types directly.

## Breaking Change

The Rust AST changes from:

```rust
pub enum Declaration<'a> {
    VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
    FunctionDeclaration(Box<'a, Function<'a>>) = 33,
    ClassDeclaration(Box<'a, Class<'a>>) = 34,

    TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
    TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
    TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
    TSModuleDeclaration(Box<'a, TSModuleDeclaration<'a>>) = 38,
    TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 39,
    TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 40,
}

pub struct TSModuleDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: TSModuleDeclarationName<'a>,
    pub body: Option<TSModuleDeclarationBody<'a>>,
    pub kind: TSModuleDeclarationKind,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}
```

to:

```rust
pub enum Declaration<'a> {
    VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
    FunctionDeclaration(Box<'a, Function<'a>>) = 33,
    ClassDeclaration(Box<'a, Class<'a>>) = 34,

    TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
    TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
    TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
    TSExternalModuleDeclaration(Box<'a, TSExternalModuleDeclaration<'a>>) = 38,
    TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 39,
    TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 40,
    TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 41,
}

pub struct TSExternalModuleDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: StringLiteral<'a>,
    pub body: Option<Box<'a, TSModuleBlock<'a>>>,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub struct TSNamespaceDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: BindingIdentifier<'a>,
    pub body: TSNamespaceDeclarationBody<'a>,
    pub kind: TSNamespaceDeclarationKind,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub enum TSNamespaceDeclarationBody<'a> {
    TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 0,
    TSModuleBlock(Box<'a, TSModuleBlock<'a>>) = 1,
}

pub enum TSNamespaceDeclarationKind {
    Module = 0,
    Namespace = 1,
}
```

The corresponding inherited `Statement` variants, `AstKind` variants, builders, and visitor methods have also changed.

## Why?

`TSModuleDeclaration` previously represented two declaration families with different syntax and semantics:

```ts
declare module "foo" {}
declare module "*.css";

module Foo {}
namespace Foo {}
namespace Foo.Bar {}
```

The shared representation allowed invalid states such as:

- A namespace with a string-literal name.
- An external module with an identifier name.
- An identifier namespace without a body.
- A dotted external module.
- An external module containing another module declaration as its body.
- A declaration whose `kind` disagreed with its name.

Consumers also had to inspect `id` before determining whether the node introduced a binding or represented an external module.

The two declaration families have stronger independent invariants:

### External modules

- Always have a string-literal name.
- Never introduce a normal binding identifier.
- May omit their body.
- Cannot use dotted names.
- Use module-body import and export rules.

### Namespace declarations

- Always have a binding identifier.
- Always have a body.
- May contain nested namespace declarations for dotted names.
- Explicitly preserve the `module` or `namespace` spelling.
- Introduce a namespace/module symbol.
- Use namespace-body restrictions.

Dedicated nodes encode these differences directly.

## Declaration forms

External modules use `TSExternalModuleDeclaration`:

```ts
declare module "foo" {}
declare module "*.css";
module "foo" {}
```

Identifier-based declarations use `TSNamespaceDeclaration`:

```ts
module Foo {}
namespace Foo {}
```

Dotted namespaces are represented as nested namespace declarations:

```ts
namespace Foo.Bar {}
```

```rust
TSNamespaceDeclaration {
    id: Foo,
    body: TSNamespaceDeclarationBody::TSNamespaceDeclaration(
        TSNamespaceDeclaration {
            id: Bar,
            body: TSNamespaceDeclarationBody::TSModuleBlock(...),
            // ...
        }
    ),
    // ...
}
```

Only `TSExternalModuleDeclaration::body` is optional.

## ESTree compatibility

Normal serialization and eager raw deserialization continue to emit the existing ESTree shape:

```ts
interface TSModuleDeclaration {
  type: "TSModuleDeclaration";
  id: BindingIdentifier | StringLiteral | TSQualifiedName;
  body: TSModuleBlock | null;
  kind: "module" | "namespace";
  declare: boolean;
  global: false;
}
```

Dotted namespaces continue to serialize as a single `TSModuleDeclaration` with a `TSQualifiedName`.

Experimental lazy raw deserialization is not compatibility-preserving. It now exposes `TSExternalModuleDeclaration` and `TSNamespaceDeclaration` directly, with separate constructors, layouts, and visitor type IDs.

## How to migrate

Code that previously matched all forms through one variant:

```rust
if let Declaration::TSModuleDeclaration(declaration) = declaration {
    // ...
}
```

should now match the required declaration family:

```rust
match declaration {
    Declaration::TSExternalModuleDeclaration(declaration) => {
        // String-literal external module
    }
    Declaration::TSNamespaceDeclaration(declaration) => {
        // Identifier-based module or namespace
    }
    _ => {}
}
```

The same applies to `Statement` and `AstKind`:

```rust
Statement::TSExternalModuleDeclaration(declaration)
Statement::TSNamespaceDeclaration(declaration)

AstKind::TSExternalModuleDeclaration(declaration)
AstKind::TSNamespaceDeclaration(declaration)
```

Common field migrations:

| Previous representation | New representation |
| --- | --- |
| `TSModuleDeclarationName::StringLiteral(id)` | `TSExternalModuleDeclaration::id` |
| `TSModuleDeclarationName::Identifier(id)` | `TSNamespaceDeclaration::id` |
| `body: None` | Only valid on `TSExternalModuleDeclaration` |
| `TSModuleDeclarationBody::TSModuleBlock` | External `body` or `TSNamespaceDeclarationBody::TSModuleBlock` |
| `TSModuleDeclarationBody::TSModuleDeclaration` | `TSNamespaceDeclarationBody::TSNamespaceDeclaration` |
| `TSModuleDeclarationKind::Module` | External module, or `TSNamespaceDeclarationKind::Module` |
| `TSModuleDeclarationKind::Namespace` | `TSNamespaceDeclarationKind::Namespace` |
| Inspect `id` to determine binding behavior | Match `TSNamespaceDeclaration` |
| `declaration.id()` for string modules | Now returns `None` |
| `visit_ts_module_declaration` | `visit_ts_external_module_declaration` or `visit_ts_namespace_declaration` |

When constructing nodes, use the generated `TSExternalModuleDeclaration` or `TSNamespaceDeclaration` builders rather than recreating the previous shared representation.

fixes https://github.com/oxc-project/backlog/issues/218
2026-08-05 14:37:35 +00:00
overlookmotel 94cc345023 refactor(minifier): use impl GetAstBuilder instead of generic params (#25176)
Pure refactor.

Simplify methods of `KeepVar` trait, removing generic params. Also convert the `&impl GetAstBuilder<'a>` to an `&impl AstBuild<'a>` before calling other methods, to reduce polymorphism (affects compilation time, not run time).
2026-08-01 15:40:56 +00:00
camc314 067da8c4e7 feat(ast)!: store single parameter in TSIndexSignature::parameter (#25154) 2026-07-31 20:35:37 +00:00
camc314 a0c7788f47 feat(ast)!: change TSIndexSignatureName::name to Ident (#25150)
## Summary

Use `Ident<'a>` for `TSIndexSignatureName::name` instead of `Str<'a>`.

## Breaking Change

The Rust AST changes from:

```rust
pub struct TSIndexSignatureName<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    #[estree(json_safe)]
    pub name: Str<'a>,
    pub type_annotation: Box<'a, TSTypeAnnotation<'a>>,
}
```

to:

```rust
pub struct TSIndexSignatureName<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub name: Ident<'a>,
    pub type_annotation: Box<'a, TSTypeAnnotation<'a>>,
}
```

## Why?

Index-signature parameter names are identifiers, and the parser already produces an `Ident`. Storing the name as `Str` discarded its identifier-specific representation and precomputed hash.

`Ident` is also always JSON-safe, so the explicit `#[estree(json_safe)]` annotation is unnecessary. ESTree output remains unchanged.

## How to migrate

Use `Ident` wherever constructing or consuming `TSIndexSignatureName::name`.

| Previous representation | New representation |
| --- | --- |
| `name: Str<'a>` | `name: Ident<'a>` |
| Constructor input implementing `Into<Str<'a>>` | Input implementing `Into<Ident<'a>>` |
| Formatter/traversal accessors returning `Str` | Accessors returning `Ident` |
| Explicit JSON-safe serialization | Serialization through `Ident` |
| Generic string access | `name.as_str()` |

Related to oxc-project/backlog#232.
2026-07-31 14:22:29 +00:00
camc314 1bdedd11ca feat(ast)!: introduce ExportDeclaration, ExportFromDeclaration (#25095)
## Summary

Split the overloaded Rust `ExportNamedDeclaration` into syntax-specific AST nodes:

- `ExportDeclaration` for exported declarations
- `ExportNamedDeclaration` for local exports
- `ExportFromDeclaration` for re-exports

## Breaking Change

The Rust AST changes from:

```rust
pub struct ExportNamedDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub declaration: Option<Declaration<'a>>,
    pub specifiers: Vec<'a, ExportSpecifier<'a>>,
    pub source: Option<StringLiteral<'a>>,
    pub export_kind: ImportOrExportKind,
    pub with_clause: Option<Box<'a, WithClause<'a>>>,
}

pub enum ModuleDeclaration<'a> {
    // ...
    ExportNamedDeclaration(Box<'a, ExportNamedDeclaration<'a>>),
    // ...
}
```

to:

```rust
pub struct ExportDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub declaration: Declaration<'a>,
}

pub struct ExportNamedDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub specifiers: Vec<'a, ExportSpecifier<'a>>,
    pub export_kind: ImportOrExportKind,
}

pub struct ExportFromDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub specifiers: Vec<'a, ExportSpecifier<'a>>,
    pub source: StringLiteral<'a>,
    pub export_kind: ImportOrExportKind,
    pub with_clause: Option<Box<'a, WithClause<'a>>>,
}

pub enum ModuleDeclaration<'a> {
    // ...
    ExportDeclaration(Box<'a, ExportDeclaration<'a>>),
    ExportNamedDeclaration(Box<'a, ExportNamedDeclaration<'a>>),
    ExportFromDeclaration(Box<'a, ExportFromDeclaration<'a>>),
    // ...
}
```

## Why?

The previous node represented three distinct syntax forms through optional and mutually exclusive fields:

```text
export const value = 1;       -> ExportDeclaration
export { value };             -> ExportNamedDeclaration
export { value } from "mod";  -> ExportFromDeclaration
```

This allowed invalid combinations such as declarations with specifiers or sources, and import attributes without a source. The new nodes make these states unrepresentable and remove repeated `declaration`/`source` branching from consumers.

## How to migrate

Match the syntax-specific `ModuleDeclaration`, `Statement`, or `AstKind` variant instead of inspecting optional fields. Generated builders, visitors, traversal ancestors, and formatter wrappers now expose corresponding APIs for all three nodes.

Common field migrations:

| Previous representation | New representation |
| --- | --- |
| `declaration: Some(declaration)` | `ExportDeclaration { declaration }` |
| `declaration.as_ref()` | `&export.declaration` |
| `source.is_none()` for local exports | Match `ExportNamedDeclaration` |
| `source: Some(source)` | `ExportFromDeclaration { source }` |
| `source.as_ref().unwrap()` | `&export.source` |
| `with_clause` | Only available on `ExportFromDeclaration` |
| Stored `export_kind` on exported declarations | `ExportDeclaration::export_kind()` |
| `ExportNamedDeclaration::boxed_plain_declaration(...)` | `ExportDeclaration::boxed(...)` |
| `ExportNamedDeclaration::boxed(..., None, specifiers, None, ...)` | `ExportNamedDeclaration::boxed(..., specifiers, ...)` |
| `ExportNamedDeclaration::boxed(..., None, specifiers, Some(source), ...)` | `ExportFromDeclaration::boxed(..., specifiers, source, ...)` |

closes https://github.com/oxc-project/backlog/issues/219
2026-07-31 10:25:47 +00:00
Armano 6c9ee14ad7 fix(minifier): preserve if-chain idempotency and correct side effects when alternate should not be moved (#25041)
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>![P1
Badge](https://img.shields.io/badge/P1-orange?style=flat)</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>
2026-07-31 10:34:15 +08:00
Dunqing fb402b010c perf(minifier): stop re-deriving printed numeric spellings (#25103)
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.
2026-07-30 10:52:55 +00:00
camc314 8541e18e91 feat(ast_tools): generate minifier scope collector (#25088)
## Summary

- generate `ChildScopeCollector` for both `oxc_traverse` and `oxc_minifier` from the same AST tools generator
- synchronize the minifier collector with the current AST schema
- prevent the two generated collector implementations from drifting
2026-07-29 16:25:26 +00:00
camc314 c917f204a7 feat(ast)!: introduce ArrowFunctionBody enum (#24987)
## Summary

- Introduces `ArrowFunctionBody` to represent concise expression and block bodies explicitly.
- Removes the `ArrowFunctionExpression::expression` boolean.
- Makes invalid arrow body combinations unrepresentable.

## Breaking Change

The Rust AST changes from:

```rust
pub struct ArrowFunctionExpression<'a> {
    pub expression: bool,
    pub body: Box<'a, FunctionBody<'a>>,
}
```

to:

```rust
pub enum ArrowFunctionBody<'a> {
    FunctionBody(Box<'a, FunctionBody<'a>>) = 64,
    INHERIT(Expression<'a>),
}

pub struct ArrowFunctionExpression<'a> {
    pub body: ArrowFunctionBody<'a>,
}
```

Concise arrow bodies are now stored directly as expressions instead of synthetic `FunctionBody` and `ExpressionStatement` nodes. Block bodies continue to use `FunctionBody`.

## Why?

The previous representation required consumers to maintain this implicit invariant:

```text
expression == true
→ no directives
→ exactly one statement
→ that statement is an ExpressionStatement
```

Invalid combinations could cause panics, skipped work, or fallback behavior. The enum encodes the grammar directly and makes those invalid states unrepresentable.

Concise expressions also now have `ArrowFunctionExpression` as their direct traversal ancestor, without synthetic `FunctionBody` or `ExpressionStatement` nodes.

## How to migrate

Use the arrow body accessors or match on `ArrowFunctionBody` rather than inspecting `expression` and synthetic statements.

Common field migrations:

| Previous representation | New representation |
| --- | --- |
| `arrow.expression` | `arrow.is_expression()` |
| `arrow.get_expression()` | `arrow.get_expression()` |
| `&arrow.body.statements` | `arrow.get_function_body().map(\|body\| &body.statements)` |
| `&mut arrow.body.statements` | `arrow.get_function_body_mut().map(\|body\| &mut body.statements)` |
| Synthetic expression statement in `arrow.body` | Direct `ArrowFunctionBody` expression variant |
| `arrow.body = function_body` | `arrow.body = ArrowFunctionBody::FunctionBody(function_body)` |
| Expression-to-block conversion | Create a `FunctionBody` containing a return statement |
| Block-to-expression conversion | Assign the expression directly to `arrow.body` |
| Builder `(expression, body)` arguments | Pass a single `ArrowFunctionBody` |

closes https://github.com/oxc-project/backlog/issues/211
2026-07-29 13:32:32 +00:00
Dunqing e6734a5738 fix(minifier): fold typeof comparisons with known object strings (#25017)
Calling `right.to_js_string(ctx)` to get the string of the expression and check whether its string belongs to any `typeof` value to keep compressing `typeof v == [1]` to `!1`.

I may be overthinking this case since this kind of use is quite rare in real-world scenarios. However, I didn't find it hard to implement, so I just got it to work.
2026-07-29 02:12:20 +00:00
Dunqing cfec06e9cb refactor(minifier): restructure typeof comparison folding (#25016)
Simplify handling, reduce repeated boolean literal creativity, and make the code cleaner.
2026-07-29 02:12:19 +00:00
sapphi-red 3a9d5a5db9 fix(minifier): keep typeof foo == ['object'] as-is (#24941)
Keep `typeof foo == ['object']` as-is as it may be `true`(when `foo` is `{}`) or `false`.
2026-07-29 02:12:19 +00:00
overlookmotel 7e1199cbbb feat(ast)!: remove conversion to Box from AST builder methods (#25038)
AST builder methods accepted an `IntoIn<ArenaBox<T>>` for all params where an `ArenaBox<T>` is required. You could pass a plain `T`, and the builder method would automatically allocate space for it, and copy it into the arena.

This was originally intended as a convenience, but in fact it's a footgun - it encourages allocating late, holding types on the stack during other work, which results in register pressure, and churn of copying to/from the stack.

Remove the automatic conversion. All places where we were using auto-allocation are already fixed in previous PRs #25034, #25035, #25036, #25037.

Additional benefits of this change:

1. AST builder methods become less polymorphic which should help with compile times.
2. We can get rid of the `NONE` type, which was a hacky workaround, necessary for passing as params where an `IntoIn<Option<ArenaBox<T>>>` is required. Now these all become plain `None`.

Most of the diff in this PR is just replacing `NONE` with `None` all over the place.

Note: AST builder methods still define params which are `ArenaVec`s as `IntoIn<ArenaVec<T>>`, as this does add genuine value - it allows passing `[]` instead of the much more verbose `ArenaVec::new_in(self)`.
2026-07-28 22:40:04 +00:00
Dunqing fbe8d318ff fix(minifier): respect targets when folding RegExp source (#24790)
RegExp literal `.source` folding used a hard-coded approximation of ES2015 support. It rejected syntax that configured targets support, such as the `u` flag for ES2015, while allowing newer syntax without checking the configured engines.

The same target compatibility mapping already exists in `oxc_ecmascript` for RegExp constructor analysis. Expose one parsed-syntax predicate from that layer and reuse it for `.source` folding. This removes the duplicate policy and the minifier's direct `oxc_regular_expression` dependency. An unspecified target remains conservative, while an explicit supporting target unlocks the fold.

Covered by `test_fold_regex_source` with adjacent unsupported and supported targets for `y`, `u`, `s`, `d`, `v`, named groups, Unicode property escapes, lookbehind, duplicate named groups, and pattern modifiers.

Verified with `cargo test -p oxc_ecmascript --features side_effects`, `cargo test -p oxc_minifier` (631 passed, 42 ignored), `just minsize` (no minsize or allocation snapshot changes), affected-crate clippy with warnings denied, and `just fmt`.

Follow-up to #24712 and [its non-blocking review](https://github.com/oxc-project/oxc/pull/24712#discussion_r3619967432).

Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and submitting these changes under the repository AI usage policy.
2026-07-28 03:37:44 +00:00
Dunqing 70d6ecedb3 feat(minifier): fold safe integer exponentiation (#25009)
Integer exponentiation remains uncompressed even when its Number result is an exact safe integer and a shorter literal can represent it. This also prevents the existing `Math.pow` rewrite from completing beyond the exponentiation form.

Fold these cases only when both operands have side-effect-free integer-valued numeric values, the result is within the Number safe-integer range, and the serialized literal is no longer than a conservative lower bound for the original expression. Number exponentiation is implementation-approximated, so fractional operands remain unchanged. BigInt, observable coercions, side effects, unsafe results, and size regressions are rejected.

## Example

Input:

```js
use(2 ** 3);
```

Before:

```js
use(2 ** 3);
```

After:

```js
use(8);
```

The minsize corpus reduces Three.js by 2 raw bytes and 1 byte with gzip, below the reported 0.01 kB snapshot precision. The tracked kitchen-sink fixture records six additional arena node allocations from the new replacement; heap metrics and arena size are unchanged.

Verified with the full `oxc_minifier` test suite, `just minsize`, Clippy with warnings denied, formatting, and `git diff --check`.

Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
2026-07-28 01:41:03 +00:00
Armano 73fc747699 feat(minifier): enable termination statement optimization for do while (#24921)
- allow removing trailing unlabeled `continue;` in `do...while` block
bodies (same as other loop block bodies).
- allow removing trailing unlabeled `break;` in `do...while` block
bodies **only** when the loop test is side-effect-free and statically
`false`.
- enable and update `do...while` exit-point tests.

#### changes in stack
1. https://github.com/oxc-project/oxc/pull/24920
2. https://github.com/oxc-project/oxc/pull/24921 < This PR
2026-07-28 09:39:05 +08:00
Dunqing 5eadfdba26 feat(minifier): fold safe integer division and remainder (#24981)
Finite division and remainder expressions remain uncompressed even when their Number result is an exact safe integer and a shorter literal can represent it. For example, `360 / 360` and `3 % 2` remain as binary expressions.

Fold these cases only when both operands have side-effect-free numeric values, the result is within the Number safe-integer range, and the serialized literal is no longer than a conservative lower bound for the original expression. The existing `NaN` and `Infinity` paths remain unchanged. This preserves `-0` while rejecting BigInt, fractional or unsafe results, observable coercions, and size regressions.

## Example

Input:

```js
use(360 / 360);
```

Before:

```js
use(360 / 360);
```

After:

```js
use(1);
```

The minsize corpus has no change at its reported 0.01 kB precision. The tracked kitchen-sink fixture records four additional arena node allocations from the new replacements; heap metrics and arena size are unchanged.

Verified with the full `oxc_minifier` test suite, `just minsize`, Clippy with warnings denied, and formatting.

Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
2026-07-27 15:10:38 +00:00
Armano c73500ce79 refactor(minifier): unify removable termination statement detection (#24920)
Extract duplicate checks for removable trailing `continue;` and
`return;` statements into a single `can_remove_termination_statement()`
helper.

The helper now includes `while` loop bodies alongside
`for`/`for-in`/`for-of` for `continue` statement but under normal flow
`while` should not be needed as we always convert it to `for(;a;)`


#### changes in stack
1. https://github.com/oxc-project/oxc/pull/24920 < This PR
2. https://github.com/oxc-project/oxc/pull/24921
2026-07-27 20:58:54 +08:00
Dunqing c7c56962e4 feat(minifier): fold parenthesized boolean ternary tests (#24968)
Boolean-literal ternaries whose tests already require parentheses—such as assignments and nested conditionals—are currently left uncompressed. The size guard checks the rewritten logical operand, but does not account for parentheses that were already required around the original ternary test.

Compare the parenthesis cost before and after the rewrite. Equality comparisons are negated by inverting their operator; other binary and logical tests remain rejected. The existing branch, `??`, and `||`-under-`&&` guards are unchanged.

## Example

**Input**

```js
use((flag = a === b) ? false : value);
```

**Minified output**

```diff
-use((flag = a === b) ? !1 : value);
+use(!(flag = a === b) && value);
```

The rewritten output is one byte shorter. The current minsize corpus contains no matching cases, so its size and allocation snapshots are unchanged.

Stacked on #24943.

Verified with the full `oxc_minifier` test suite, `just minsize`, Clippy with warnings denied, and formatting.

Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
2026-07-27 09:01:04 +00:00
Dunqing 82c5806081 feat(minifier): fold sequence branches in boolean ternaries (#24943)
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.
2026-07-27 09:01:04 +00:00
Armano b31713a693 feat(minifier): add new IsTerminated trait and use it in exit-point optimization (#24950)
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).
2026-07-27 16:03:47 +08:00
Armano cb5acfb20a fix(minifier): optimize termination statements in labeled statements (#24813)
optimizes labeled stmts with continue and break statements 
```ts
// 0. base 
function f() { g: if (a()) { return; } else { return; } return; }
// 1. current
function f() { g: if (a()) return; else return;}
// 1. current
function f() { g: if (a()) return; return;}
// 3. new
function f() { g: a(); }
```

this PR includes test changes from #24687

this change should have minimal impact on actual code as alot of other
changes are needed before we see it being used

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-27 08:28:32 +08:00
Dunqing 46183007c2 fix(minifier): avoid longer numeric constant folds (#24875)
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.
2026-07-26 14:12:00 +00:00
sapphi-red f7971f181f fix(minifier): keep side effects in typeof comparisons (#24940)
Avoid removing `typeof f() == 1`.
2026-07-26 14:04:41 +00:00