Commit Graph

233 Commits

Author SHA1 Message Date
Cameron Clark 7784ad92d5 perf(parser): avoid allocation for single grouped expressions 2026-09-08 09:21:15 +01:00
leaysgur b26bf56cd5 refactor(formatter): route every assignment RHS shape rule through a cast-aware accessor (#26430)
Follow up on #26429, apply cast-aware behavior to assignment call sites.
2026-09-08 06:02:01 +00:00
Armano 5303f5c884 perf(minifier): unify merging of last expression into target sequence (#26264)
adds new helper `merge_last_expression_into_sequence` and uses it
everywhere when we want to merge last expression to target expression
`a; b;` -> `a, b;`

this change avoids creation of 2 dummy nodes and unnecessary
`drop_statement` on dummy stmt per merge
2026-09-07 20:40:49 +08:00
Armano 2ca7d29b7a perf(minifier): consume nodes in minimize_statements in reverse order (#26258)
changes `minimize_statement`s to consume statement via a single
`reverse()` + `pop()` pass instead of repeatedly calling `take_in` on
each slot.

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

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

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-09-07 17:24:05 +08:00
Armano d19c42ac87 perf(minifier): merge nested if stmt in place instead of creating dummies (#26351)
reduce allocations when converting `if (a) if (b) x;` to `if (a && b)
x;`
2026-09-05 22:51:23 +01:00
Armano a17de58e30 feat(minifier): process of return/throw in non last position and fix async * inlining (#26214)
This Pr brings couple optimizations and refactor,

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

fixes #26255
ref #24331
2026-09-03 23:05:40 +08:00
leaysgur 9f81002229 fix(formatter): re-add suppressed statement terminators per options.semi (#26220)
```js
// input (semi: true)
// oxfmt-ignore
const a = x

// before
// oxfmt-ignore
const a = x

// after
// oxfmt-ignore
const a = x;
```

```js
// input (semi: false)
let a = 1;
// oxfmt-ignore
[b].sort();

// before
let a = 1
// oxfmt-ignore
[b].sort(); // <- ASI failure

// after
let a = 1
// oxfmt-ignore
;[b].sort() // <- OK
```
2026-09-01 07:25:43 +00:00
Armano 784e9faad2 feat(minifier): invert !0 and !1 in place for boolean context to 1 and 0 (#26050)
improve negation of 1 and 0 to 0 and 1 in boolean context

previously only `!!0` and `!!1` could be inlined to `1` and `0` and now
`!0` and `!1` will also be inlined,

this change allows us to flip numeric values in-place instead of having
to first to create unary and remove it afterwards

```
!0 was converted before to 1 in some cases:
1. negate 1
2. !1 - add unary
3. 0 - remove unary

now:
1. negate 1
2. 0
```
2026-08-29 15:36:17 +08:00
Armano 1e902cc1ad feat(minifier): expand fold leading assignments into the var decl (#26142)
expands leading assignment inlining into variable declarations for
`return`, `switch`, `if`, `throw`, `for`, `for in`, `for of`
```js
// return
var a; return a = b(), c;
var a = b(); return c;
// throw
var a; throw a = b(), c;
var a = b(); throw c;
// switch
var a; switch (a = b(), c) {}
var a = b(); switch (c) {}
// for
var a; for (a = b(), c; ;) d;
var a = b(); for (c; ;) d;
// if
var a; if (a = b(), c) d;
var a = b(); if (c) d;
```

fixes #25949
2026-08-29 04:27:01 +08:00
Armano 3ed4f6ad90 feat(minifier): expand de morgan's optimization to allow move of ! (#25930)
Expand negation to include move of `!` operator to inner children

```js
!(a == b && c == 3 && a)
a != b || c != 3 | !a
```

delta calculation is dependent on boolean context
- if we expect truthly/falsly value negation of `!a` will be converted
to `a` (-1 delta)
- in ambiguous context `!a` will be converted to `!!a` (+1 delta)

```js
a = !!(!b || !c);
//   ^ ^     ^ -3
a = !(b || c)
//  ^ ^    ^ +1 - if we could detect if parens can be removed it would be -1
//  ^^^    ^^ -1 - if we would know that we can remove parens
```
2026-08-29 02:08:57 +08:00
leaysgur 676b7e19e5 fix(formatter): keep comments in their for-head slot, before an empty-statement body and do-while (#26073)
Keep comments inside `for (init; test; update)`.
2026-08-25 09:28:12 +00:00
leaysgur 3bbcea6d9c fix(formatter): keep comments between a head and its body out of the braces across all constructs (#26071)
Fix inconsisntet and also non-idempotent formatting around:

```js
  m3(tagName: string, rect: number[]): void // THIS should stay
  {
    // ...
  }
```
2026-08-25 09:28:12 +00:00
leaysgur 57537a2223 fix(formatter): place comments between a head and open paren (#26058)
Fixes inconsistency and idempotency issue for control statements.

e.g.

```js
while (x // THIS should stay here
) {
  // ...
}
```
2026-08-25 09:28:11 +00:00
leaysgur e2d53e7f18 fix(formatter): place comments between a statement head and its body (#26043)
Closes #25832 and more idempotency issues.
2026-08-25 09:28:11 +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
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 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 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
leaysgur 44231fb5f7 fix(formatter): fix comment and indent issues around unions (#25665)
Fixes comment-placement bugs, idempotency and also inconsistency around unions/intersections, type aliases, conditional types, and assignment-likes.

- Fix invariant violations where comments crossed `=`, the value, or `;`
  - removed the relocation logic emulating old Prettier 3.8 behavior
- Normalize a union's leading comments to one canonical form behind the synthesized leading `|`, independent of the source shape
- Comments around a formatter-added `(` keep their source side of the `(`
- Hug conditional-type branch comments behind `?`/`:` with the content one level under the operator, matching conditional expressions
- Extend `= // c` position preservation from type aliases to all of JS
2026-08-16 15:04:28 +00:00
leaysgur 2adb8fb329 feat(formatter): implement operatorPosition option (#25581)
NOTE: For now, behavior is the same as Prettier.

- Default is `end`
- This works on intersection(`&`) type, but has no effect on union(`|`) type

For future refs: https://github.com/oxc-project/oxc/issues/16367
2026-08-14 08:12:16 +00:00
camc314 4922141d0a feat(mangler): deduplicate private accessor names (#25601)
## Summary

Fix private-name mangling for `get #x()` / `set #x(value)` accessor pairs.

JavaScript treats an accessor pair as one private name, but the mangler previously assigned a candidate per element before collecting into a name-keyed map. The setter overwrote the getter’s mapping, while still consuming an extra candidate.

### Before

```js
class Outer {
  get #x() {}
  set #x(value) {}
  #longName;
  nested() {
    return class {
      #nestedName;
    };
  }
}
```

```js
class Outer {
  get #t() {}
  set #t(e) {}
  #n;
  nested() {
    return class {
      #r;
    };
  }
}
```

### After

```js
class Outer {
  get #e() {}
  set #e(e) {}
  #t;
  nested() {
    return class {
      #n;
    };
  }
}
```

## Implementation

- Deduplicate private names during map construction with `HashMap::entry`.
- Derive nested-class offsets from already-built lexical parent mappings, preserving collision avoidance.
- Pre-size mappings from the number of private elements.
2026-08-14 00:44:46 +00:00
leaysgur 465b723d04 refactor(formatter,oxfmt): unify host services as SessionServices on FormatSession (#25388)
- dispatcher
- string_embedder
- tailwind_sorter

Group these 3 together as `SessionServices`, make them available to `FormatSession`, and pass them around.
2026-08-09 14:32:10 +00:00
camc314 9d97da14c8 chore(minifier): update minifier allocs snapshot (#25439) 2026-08-09 13:38:14 +00:00
leaysgur 281124b43b refactor(formatter_core): introduce FormatSession with shared GroupId space (#25329)
In theory, the group id needs to function across embedded boundaries.

To achieve this, a "session" mechanism that can be carried over those boundaries was required.
2026-08-09 12:33:40 +00: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 e4208164e1 perf(parser): optimize trailing comma tracking (#25376)
## Summary

- Track trailing-comma spans only for array and object literals ending in a spread.
- Consume the stored span while covering a rest assignment target.

## Rationale

Avoids parser-state map work for ordinary trailing commas while retaining constant-time diagnostics for invalid rest-target trailing commas.
2026-08-07 21:30:28 +00: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
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
leaysgur a56b7d059b refactor(tasks/allocations): do not print redundunt traling blank line (#25268)
Otherwise, our new YAML formatter trims it, then remains as diff.
2026-08-04 00:49:27 +00:00
camc314 067da8c4e7 feat(ast)!: store single parameter in TSIndexSignature::parameter (#25154) 2026-07-31 20:35:37 +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 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
Pitiș Radu ca1ba71595 fix(formatter): hug arrows with type-reference return annotations (#25044)
An arrow function whose return type annotation is a plain type reference
(e.g. `(): Foo => …` as a call argument) was not hugged the way Prettier
hugs it, forcing a break in call arguments
(`crates/oxc_formatter/src/print/call_like_expression/arguments.rs`).

Before/after (real output, `printWidth: 80`):

```ts
// input
a.map((): A => ({
  name,
}));
```

```ts
// main
a.map(
  (): A => ({
    name,
  }),
);

// this PR — matches Prettier byte-for-byte
a.map((): A => ({
  name,
}));
```

Conformance (verified on this branch, rebased on current main):

- ts: 621 → **622**/659 — fixes `typescript/arrow/return-type/18588.ts`
- js: 774/810, unchanged; zero regressions
- `cargo test -p oxc_formatter` 318 passed

Diff is 2 files: the code change in `arguments.rs` (+5/−46) and the
one-line conformance snapshot update.

## AI Usage Disclosure

This change was developed with AI assistance (Claude). I have reviewed
and tested it, per the AI usage policy.

---------

Signed-off-by: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com>
Co-authored-by: Pitis <radu.pitis@chatlyn.com>
Co-authored-by: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com>
2026-07-29 18:33:21 +09: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
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
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
Armano 34f555945c feat(minifier): convert switch with one case to if statement (#24611)
Inline switch with 1 case to if statement or in case of default to
statement expressions + block statement


https://github.com/oxc-project/oxc/actions/runs/29759622291#summary-88410679091

change extracted from #17523
ref #17544
2026-07-26 08:01:40 +08:00
leaysgur 9e8ee0e471 refactor(formatter_core): move debug names out of (Group|Label)Id to unify debug|release layout (#24826)
In #24558, a large amount of arena memory usage was discovered, but this snapshot was taken with `debug_assertion = true`.

Since `FormatElement` is 40B when this is enabled, but typically 24B, it means the actual usage was not being measured accurately.

In this PR, I have adjusted it so that the size is always consistent regardless of whether `debug_assertion` is enabled or not.
2026-07-23 06:37:48 +00:00
leaysgur cee91ddc84 refactor(formatter_core): remove thread-local scratch cache (#24824)
Revert #24583.

After re-measuring, I found no significant improvement. It is not worth the amount of code required.

Also, the fundamental issue is that the end-side formatter, rather than Oxfmt itself, is performing cross-file caching, which is an undesirable side effect.
2026-07-23 05:48:25 +00:00
leaysgur bb73b23c54 perf(formatter_core): bound the thread-local scratch cache (#24793)
Measures to address this.

> NOTE: checked-in vectors keep their high-water capacity for the thread's lifetime

The 8 MB covers the largest file (checker.ts) that is likely to be created and formatted manually.
2026-07-22 06:32:03 +00:00
leaysgur a5f7b155f7 perf(formatter): stage assignment-like left hand side on the heap (#24613)
The last section where arena stranding was taking place.

---

There was a suggestion to use a dedicated temporary-arena as a temporary data storage; however, as with the previous JSX PR and this PR has become clear that this does not meet the current requirements.
2026-07-22 06:32:03 +00:00