Commit Graph

210 Commits

Author SHA1 Message Date
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
leaysgur 94de05f153 perf(formatter): accumulate JSX child-list builders on the heap (#24585)
The JSX child-list builders (`MultilineBuilder` / `FlatBuilder` in `child_list.rs`) were the last IR staging path still growing vectors in the arena.

They accumulate elements across the whole child-list loop while arbitrary child content is
formatted in between, so every growth stranded its old allocation (the same pattern #24582 removed from `intern` / `BestFitting` / grouped arguments).

They can't use `HeapVecBuffer`: the flat and multiline builders write *alternately* for each child, which would interleave their segments in the shared scratch vector and break its LIFO watermark discipline. The thread-local cache behind it has no such requirement though.

So each builder now checks out its own `ScratchBuffer` and writes into it through a small private `Buffer` adapter (`ChildListBuffer`); the finished result reaches the arena as one exactly-sized copy via the new `Formatter::intern_elements`, which clears the source so its capacity returns to the cache on drop.

Now arena reallocs for `RadixUIAdoptionSection.jsx` is 0.
2026-07-22 06:32:02 +00:00
leaysgur 7810e8ac4f perf(formatter_core): share one thread-cached scratch vector across staging buffers (#24583)
Apply #24539, share `ScratchBuffer`'s buffer.

NOTE: checked-in vectors keep their high-water capacity for the thread's lifetime (same policy as #24539). This is ~KB for regular sources, and fine.
2026-07-22 06:32:02 +00:00
leaysgur c191f51f1d perf(formatter_core): stage IR buffers on the heap to reduce arena memory (#24582)
Part of #24558

Adds `HeapVecBuffer` to `oxc_formatter_core`: a staging twin of `VecBuffer` that grows on a pooled heap vector and moves the finished sequence into the arena as one exactly-sized allocation.

Converted JS formatter call sites: `Formatter::intern`, `BestFitting` variant construction, and the three grouped-call-argument variant builders.

The other formatters would automatically benefit from this as well, and there was no need to make any individual changes.

---

TIL: `antd.js` is a bundled file and is not intended for general formatting.
2026-07-22 06:32:02 +00:00
Boshen baf022d74d refactor(ast)!: remove old AstBuilder methods (#24563)
Closes the migration tracked in #23043.

The new builder API (methods on the AST types themselves) has been in place for a while, all Oxc crates are migrated, and every crate which uses `AstBuilder` already enables `disable_old_builder` in its tests. The module docs said "after a few weeks, we will remove `AstBuilder`'s own methods entirely" — this does that.

## What's removed

- `crates/oxc_ast/src/generated/ast_builder.rs` and `builder/methods.rs` — `AstBuilder`'s own node and primitive constructors.
- `AstBuilderGenerator`, which produced the former.
- The `disable_old_builder` Cargo feature, plus the dev-dependency hacks it required across 33 manifests. Eight crates depended on `oxc_ast` *purely* to turn the feature on; those deps and their `cargo-shear` ignore entries are gone too.

## One thing worth reviewing

`AstBuilderGenerator` owned the `#[builder(default)]` / `#[builder(skip)]` attr registration that `BuilderMethodsGenerator` only *reads* — its doc comment said as much. Deleting it naively would have silently dropped `skip`/`default` handling for the 12 AST types using those attrs, so `attrs()` and `parse_attr()` move to `BuilderMethodsGenerator`.

The evidence this was faithful: after regenerating, `builder_methods.rs` and every other generated file are **byte-identical**. The only generated change in the diff is the deletion.

## Breaking changes

- `AstBuilder`'s own methods are gone. Use the AST type methods: `Statement::new_expression_statement(span, expr, &builder)` instead of `builder.statement_expression(span, expr)`.
- `AstBuilder` is no longer `Copy`/`Clone`, and its `allocator` field is private — use the `allocator` method from `GetAllocator`.
- `AstBuilder` and `NONE` are no longer re-exported from the crate root; import them from `oxc_ast::builder`. (The `disable_old_builder` feature already gated these off, so nothing in-tree used the root path.)
- The `disable_old_builder` feature no longer exists — remove it from any manifest that sets it.

A migration note covering all of the above is in `crates/oxc_ast/src/builder/mod.rs`.

---

Implemented with AI assistance (Claude Code); design, review, and verification driven by the author per the repo AI-usage policy.
2026-07-21 15:22:46 +00:00
sapphi-red d85276c894 fix(minifier): avoid incorrect logical assignment transformation when base object may be mutated (#24718)
Similar to #16802 but for cases without the returned value.
2026-07-21 03:31:04 +00:00
sapphi-red 87823d0d64 feat(minifier): merge combined assign expression in conditional expression (#24707)
Compress `x ? a += 0 : a += 1` to `a += x ? 0 : 1`.

refs #8345
2026-07-21 03:31:04 +00:00
sapphi-red 03a54c9e40 feat(minifier): merge assign expression against member expression in conditional expression (#24706)
Compress `x ? a.b = 0 : a.b = 1` to `a.b = x ? 0 : 1`.

refs #8345
2026-07-21 03:31:03 +00:00
Dunqing 64c224117c fix(minifier): align class heritage removal with assumptions (#24533)
#24349 made unused-class removal conservatively keep every class whose heritage resolves to a class, lexical, or `var` binding. That preserves class-evaluation errors which the minifier assumptions explicitly allow optimizations to ignore, and it leaves `(class extends Base {});` residue in Rolldown chunks after tree-shaking ([rolldown#10274](https://github.com/rolldown/rolldown/issues/10274)).

The minifier already documents both `No TDZ Violation` and `No side effects from extending a class`, and `oxc_ecmascript` explicitly ignores invalid-superclass errors so removable derived classes are not broadly deoptimized. Treat resolved identifier heritage like other pure reads while retaining the pre-existing literal-arrow exception, including its paren/sequence unwrap. Effects from evaluating the heritage expression, decorators, and effectful class elements remain observable.

The runtime harness already has minifier-only exclusions for unsupported inputs. Six Test262 fixtures covered by these assumptions are skipped only before the minifier variant, with the reason documented beside the list; codegen and transformer still execute, and `runtime.snap` remains unchanged.

## Example

Input:

```js
export var Base = class {};
export var Keep = class extends Base {};
var REMOVE = class extends Base {};
```

Before:

```js
var Base = class {}, Keep = class extends Base {};
(class extends Base {});
export { Base, Keep };
```

After:

```js
var Base = class {}, Keep = class extends Base {};
export { Base, Keep };
```

Verified with `cargo test -p oxc_minifier`, `cargo coverage runtime`, Clippy with warnings denied for `oxc_minifier` and `oxc_coverage`, `just minsize`, `just allocs`, formatting, and `just ready`. Minsize and `runtime.snap` are unchanged; the allocation snapshot records fewer allocations for `kitchen-sink.tsx`.

Implemented with AI assistance (Claude Code and OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
2026-07-20 09:48:52 +00:00
Dunqing f145d73b6a fix(minifier): guard reordered identifier reads (#24698)
A locally read-only identifier is not necessarily stable across a side-effecting call. ESM imports are live bindings, Script-root bindings can be changed by another script, and computed assignment keys bypassed the existing closed-over-TDZ guard. Single-use substitution could therefore move a binding read before a call and change program behavior.

The same incomplete stability query affected logical-assignment folding. A live imported member base could be cached before a call that reassigns the export, making `||=` or `??=` update the old object instead of the current binding.

Build on the shared reorder predicates from #24699. Imports, Script-root bindings, unresolved references, syntactically mutated symbols, and closed-over block-scoped reads are treated conservatively across ordinary reads, assignment-target objects, computed keys, and member-base folding. Stable module bindings and pure replacements remain optimizable.

## Example

```js
var key = "x";
function init(obj) {
  let value = mutate();
  obj[key] = value;
}
```

The previous substitution could produce:

```js
var key = "x";
function init(obj) {
  obj[key] = mutate();
}
```

That captures `key` before `mutate()` runs, although another script can change the global binding during the call. The declaration is now retained so the read remains after `mutate()`.

For imported member bases, main can rewrite `x.y || (mutate(), x.y = 3)` to `x.y ||= (mutate(), 3)`. When `mutate` reassigns the exported `x`, the original two-module runtime repro produces `3`, while the rewritten program produces `0`. The shared stability guard now prevents that fold.

Plain computed assignments still inline stable identifiers and literals. ECMAScript evaluates the key expression and `GetValue` before the right-hand side, but defers `ToPropertyKey` until the final `PutValue`, so arbitrary key expressions remain conservative without losing safe simple-key substitutions.

This improves the existing reorder guard; it does not add a general TDZ model. Sloppy mapped `arguments` mutation remains a pre-existing unsupported case and is outside this stack.

Verified with the full `oxc_minifier` suite (622 passed, 42 ignored), clippy with warnings denied, formatting, minsize, allocation snapshots, runtime ordering probes, and repeat-compression checks. `cargo minsize` measured 18 additional raw bytes across `echarts.js` and `typescript.js`, with no iteration-count changes. System-allocation metrics are unchanged; arena changes are dominated by retained code with minor fold-path shifts.

## Stack

1. #24699 — centralize reorder stability checks
2. **#24698 — guard reordered identifier reads**

AI assistance: Codex and Claude were used for implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
2026-07-20 09:07:50 +00:00
Armano 7b045cd417 feat(minfier): drop last break from last switch case (#24673)
After many trial and errors, this seem to be the best way to remove
break stmts in switch cases,

This change targets only last break in last case of switch smts if its
unlabelled

this change differs from #23914 as its no longer targets all branches as
that should not be no longer necessary if all cases are correctly
rewritten

https://github.com/oxc-project/oxc/issues/24672

-----

cases like `switch (a) { case 3: if (b) break; }` are not changed

-----

ref: #17544
2026-07-20 13:21:03 +08:00
Boshen 884d9eb6eb perf(parser): pre-size cover-grammar assignment target buffers (#24683)
The array/object destructuring assignment-target cover conversion
(`ArrayAssignmentTarget::cover` / `ObjectAssignmentTarget::cover`) built its
target buffer from empty and let it grow via arena reallocs as targets were
pushed. Each source element or property yields at most one target, so the
final length is a known upper bound — reserve it up front with
`with_capacity_in`.

Removes a handful of arena reallocs on the tracked parser fixtures:

- `checker.ts`: −2
- `pdf.mjs`: −17
- `kitchen-sink.tsx`: −7

No change to arena size or system allocations.
2026-07-20 03:17:21 +00:00
Boshen bcc9de077e perf(parser): defer diagnostic creation until parse exit (#24663)
> [!NOTE]
> Draft. Builds on the merged diagnostics cleanup (#24661), and has been through two `/simplify` review passes.
>
> One design point a perf reviewer may ask about: `ParserCheckpoint` (copied on every `checkpoint`/`rewind`/`lookahead`) embeds `FatalError` **inline**, not boxed. Boxing it would shrink the checkpoint from 3 → 2 cache lines, but only by adding a heap allocation — counter to this PR's goal. The deferral already shrank the checkpoint substantially for free: `FatalError` previously held an inline `OxcDiagnostic` (~150 B), now a 72 B POD `ParserDiagnostic`.

Parser diagnostics are now stored in a deferred (unmaterialized) form during parsing and materialized into `OxcDiagnostic` only once, at parse exit.

## Design

A `parser_diagnostics!` macro in `diagnostics.rs` generates, from one entry per diagnostic:
- an enum variant `ParserDiagnostic::Foo { .. }` holding only the POD arguments (`Span`, `&'a str`, `Copy` types);
- a `#[cold] into_diagnostic()` match arm holding the eager construction (box + label strings + `format!`) — all the allocation/formatting work concentrated in one cold function;
- an `#[inline]` constructor with the **same name and signature as before**, so the ~330 `diagnostics::foo(...)` call sites are unchanged.

The parser/lexer error vecs and `FatalError` now carry `ParserDiagnostic<'a>`, so pushing an error, snapshotting the error set on `checkpoint()`, and discarding it on `rewind()` never allocate. Materialization happens at the two `parse()` / `parse_expression()` exit points. Foreign already-materialized diagnostics (from `oxc_regular_expression`) ride an `Eager(Box<OxcDiagnostic>)` escape hatch; the two module-record diagnostics that are built once at exit stay eager.

## Why

This eliminates the eager boxing of diagnostics that are created and then thrown away during speculative parsing (arrow-function / type-vs-expression backtracking). On real-world files — all valid, so nearly every one of these allocations was a speculation-discarded diagnostic:

| file | sys allocs before → after |
|---|---|
| checker.ts | 1818 → 23 |
| antd.js | 3661 → 190 |
| kitchen-sink.tsx | 2051 → 190 |
| App.tsx | 360 → 23 |
| binder.ts | 85 → 1 |

Diagnostic output is byte-identical — parser conformance (Test262, Babel, TypeScript) snapshots are unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-19 11:06:04 +00:00
Dunqing 7492af4830 refactor(minifier): merge persistent symbol metadata (#24639)
Pure-call summaries and member-write effects used separate sparse hash maps even though both are keyed by `SymbolId` and remain valid across peephole iterations.

This stores both values in one `PersistentSymbolMetadata` entry. Summary replacement and monotone member-effect strengthening remain independent, and clearing a dynamic-binding summary updates an existing entry without allocating or discarding its member effect.

The regenerated allocation snapshot records fewer system allocations: checker 155→152, pdf 2469→2467, antd 1044→1038, and kitchen-sink 1872→1867. `minsize.snap` remains byte-identical.

The PR tip passed the full `oxc_minifier` suite independently. The completed stack also passed clippy with warnings denied, formatting, minsize and allocation regeneration, and `just ready`.

## Stack

1. #24636 — function-summary correctness
2. #24637 — explicit symbol effect types
3. #24638 — grouped reference counts
4. **#24639 — merged persistent metadata**
5. #24640 — SymbolState facade

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-18 16:52:42 +00:00
Dunqing 7d3c178756 feat(minifier): remove unreachable recursive functions (#24125)
Reference counts cannot remove unreachable recursive functions because their self- and mutual references keep the counts nonzero even when no executable code can reach them.

This change computes function reachability after scoping is flushed. Normalize registers function declarations and bindings with implicit runtime observers. References outside registered functions make their targets live; references owned by a live registered function propagate liveness to their targets.

Graph deadness is used only to remove function declarations. Other transformations continue to use ordinary reference counts together with implicit-observability checks. Direct `eval` disables the analysis for the whole program while present. Exports, Script-root bindings, Annex B aliases, and `using` disposal remain observable even when they have no resolved AST reference.

## Example

Input:

```js
function self() { self() }
function a() { b() }
function b() { a() }
export {};
```

Output:

```js
export {};
```

## Supported

- Self-recursive and mutually recursive function declarations.
- References nested in blocks, arrows, function expressions, classes, and parameter defaults. The nearest registered function declaration owns each reference.
- ES modules, CommonJS, and local functions in Scripts. Script-root bindings remain visible to later scripts.
- Full compression and DCE-only mode.
- Reads, writes, exports, `using` disposal, and direct `eval` preserve live functions.
- Exact self-recursive variable initializers whose value is a `FunctionExpression` or `ArrowFunctionExpression`, including classic `for` initializers.

## Conservatively retained

- Mutual cycles between variable declarators, such as `const a = () => b(); const b = () => a()`.
- Recursive class cycles and mixed function/declarator/class cycles.
- Wrapped or arbitrary function-valued initializers, such as `const f = (effect(), () => f())`.
- Recursive declarators in `for-in` and `for-of` heads.
- Sloppy unhoisted Annex B block functions, whose runtime alias write is not represented by their block-scoped symbol.

## Performance

[CodSpeed's report for `e71c392`](https://github.com/oxc-project/oxc/pull/24125#issuecomment-4877260138) measured a 3.74% regression on `binder.ts`, where the analysis finds no dead graph. On `kitchen-sink.tsx`, removing 39 function declarations covering 25.3 kB of source early improved the minifier benchmark by 21.01% and the full pipeline by 8.76%. A separate 15-run Script/CommonJS corpus measured a 1.67% median increase overall, below the 2% acceptance threshold.

Verified with all-feature workspace tests, lint, rustdoc, formatting, AST generation, minsize and allocation snapshots, and the repeat-compression corpus.

Closes #13105
Closes #17364

Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository's AI usage policy.
2026-07-18 03:23:27 +00:00
Boshen 2338c13f60 feat(track-memory-allocations): track heap deallocs, alloc bytes, and peak growth (#24619)
Adds three heap-side metrics to the allocation snapshots, using the metric machinery from #24587:

- `sys deallocs` (exact count) — system deallocations per stage; read against `sys allocs` it shows whether a stage churns or retains heap.
- `sys alloc bytes` — cumulative bytes requested from the system allocator per stage (allocation sizes plus reallocation growth).
- `sys peak growth` — high-water mark of live heap during a stage relative to the live bytes at stage start, via live-bytes accounting in the `GlobalAlloc` wrapper. Captures transient heap a stage needs even when it frees it before finishing.

Arena chunk operations are excluded from all `sys` metrics at the source: `oxc_allocator` marks each chunk allocation/deallocation immediately before calling the system allocator (task-only `track_allocations` feature, compiled out everywhere else), and the task's tracking allocator consumes the marker and skips the call. This replaces the previous global chunk counter + per-stage subtraction: a subtraction can correct counts but not a high-water mark, and chunk sizes are quantized and platform-dependent (whether an arena needs one more chunk depends on byte totals that vary across architectures with type layout — the #22621 mechanism). The first CI run demonstrated this concretely: kitchen-sink's minifier peak differed deterministically by 720 kB between aarch64 and x86_64 because the mangler's temporary arena crossed a chunk-growth boundary only on x86_64; and antd's formatter "peak" was 762 MB of main-arena chunk-doubling history rather than formatter behavior (now 13.8 MB of actual heap use).

Cross-platform tolerances: the cumulative byte metrics still drift with the number of heap hashbrown tables allocated (their control-group width is architecture-dependent), so they use a relative tolerance (1%, 4 kB floor) via the committed-value comparison from #24565; `arena size` keeps its tight 1024-byte tolerance.

Sample values for checker.ts (2.9 MB source): parser needs only 2.4 kB of transient heap (fully arena-based), semantic peaks at 3.9 MB, the minifier is the pipeline's largest heap consumer at 10.2 MB, and the formatter requests 5.9 MB total while peaking at 3 MB (heavy alloc/free churn).

🤖 Implemented with [Claude Code](https://claude.com/claude-code).
2026-07-17 12:51:14 +00:00
Boshen ba16a00e4b refactor(track-memory-allocations): centralize metric declarations for heap and arena (#24587)
Follow-up to #24565. Restructures the task so that adding a new heap or arena metric is a small, local change instead of edits scattered across measurement, diffing, rendering, and committed-value lookup:

- `HeapTracker` groups the global-allocator atomics into one static `HEAP` with `read()`/`reset()`; its doc comment describes how to add a heap counter.
- `Counters { heap: HeapCounters, arena: ArenaCounters }` replaces the flat `AllocatorStats`, with `record()` / `diff_since()` (the arena-chunk exclusion logic from #22621 is unchanged, just relocated).
- `StageStats::metrics()` is now the single declaration of what a snapshot contains: an ordered list of `Metric`s, each a label plus a `MetricKind`.
- `MetricKind` encodes the per-metric snapshot policy: `Count` (platform-exact), `ExactBytes` (exact, human-readable comment), `ApproxBytes` (tolerates cross-architecture layout noise by keeping the committed value within `APPROX_BYTES_TOLERANCE`, renamed from `ARENA_SIZE_TOLERANCE` since it is no longer specific to arena size).
- `write_snapshot` / `render_file_stats` are fully metric-driven; nothing downstream needs touching when a metric is added.

Adding e.g. a `sys deallocs` count is now: one `AtomicCounter` in `HeapTracker`, one field in `HeapCounters`, one `Metric::count(...)` entry. A new byte gauge (heap peak, arena capacity) is one gauge read plus one `Metric::approx_bytes(...)` entry.

Behavior is unchanged: the committed snapshots are byte-identical when regenerated with the refactored code.

🤖 Implemented with [Claude Code](https://claude.com/claude-code).
2026-07-16 12:39:14 +00:00
Armano 3acf4c1789 feat(minifier): expand switch optimiation to remove empty cases (#24520)
remove empty cases:
- before default or at end (if no default).
- only if they don't affect behavior.

refactored implementation from #17523 

ref #17544

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-07-16 10:36:03 +08:00
Boshen b1165f9c63 fix(track-memory-allocations): make arena size snapshots platform-independent (#24565)
The `Allocations` CI job fails whenever the snapshots are regenerated on a different CPU architecture than CI (e.g. https://github.com/oxc-project/oxc/actions/runs/29420345285/job/87368903834): every `arena size` value differs by 32–128 bytes between aarch64 and x86_64. The cause is target-dependent type layout — hashbrown's table control groups are 16 bytes wide on x86_64 (SSE2) but 8 bytes on aarch64 (NEON) — so each live `HashMap` in the arena shifts `used_bytes()` slightly. This is the same root cause that #22621 handled for chunk counts; the `arena size` gauge added in #24553 re-exposed it. Allocation counts are unaffected and stay exact.

Exact byte equality across architectures isn't achievable (the arena genuinely occupies a different number of bytes), so instead the snapshot writer tolerates layout noise:

- Snapshots are now YAML (`allocs_*.yaml`) with the same layout as before: one value per line, file names at column 0 for git hunk headers, exact numbers as the diffed values, and human-readable sizes moved to trailing `#` comments.
- When regenerating, `cargo allocs` parses the committed snapshot (with `saphyr`, already a workspace dependency used by `oxc_coverage`) and keeps the committed `arena size` when the measured value is within `ARENA_SIZE_TOLERANCE` (1024 bytes, ~8× the observed worst-case cross-arch drift). Differences beyond the tolerance — real regressions such as growing an AST node type change sizes by orders of magnitude more — rewrite the value and fail the `git diff --exit-code` check as before.
- The trailing comments are rendered from the recorded value, so a snapshot regenerated on any platform stays byte-identical while within tolerance. No CI workflow changes are needed, and drift can't accumulate silently: each run compares against the committed value, so creeping sub-tolerance changes eventually cross the threshold and surface.

The committed values here are aarch64-measured; x86_64 CI measures 32–128 bytes higher and keeps them. Open PRs that touch the old `.snap` files will need a rebase and a `cargo allocs` rerun.

🤖 Implemented with [Claude Code](https://claude.com/claude-code).
2026-07-15 16:07:06 +00:00
camc314 54cc121250 feat(ast)!: split MetaProperty into ImportMeta and NewTarget (#24557)
## Summary
- replace the generic Rust MetaProperty AST node with dedicated ImportMeta and NewTarget nodes
- preserve the ESTree MetaProperty shape and generated public TypeScript types
- update parser, formatter, codegen, lint, transform, minifier, traversal, and React compiler consumers

## Summary

Remove `MetaProperty` AST node, in favour of two separate nodes (`ImportMeta` and `NewTarget`)

## Breaking Change

The Rust AST field changes from:

```rust
pub struct MetaProperty<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub meta: IdentifierName<'a>,
    pub property: IdentifierName<'a>,
}
```

to:

```rust
/// /// `import.meta` in `console.log(import.meta);`.
pub struct ImportMeta {
    pub node_id: Cell<NodeId>,
    pub span: Span,
}

/// `new.target` in `function F() { return new.target; }`.
pub struct NewTarget {
    pub node_id: Cell<NodeId>,
    pub span: Span,
}
```

## Why?

The current AST could represent shapes such as:

```
foo.bar
```

By constructing the node manually. However this doesn't make sense, and is invalid.

## How to migrate?

1. Replace any `Expression::MetaProperty` matches/checks with the dedicated `Expression::ImportMeta` and `Expression::NewTarget` varients
2. Replace any `new_meta_property` construction with the new `new_import_meta` or `new_new_target` APIs.
3. Inspecting the `meta`/`property` identifier names is no longer required.
2026-07-15 14:48:32 +00:00
Boshen 510c435743 feat(track-memory-allocations): add arena size (#24553)
First of three PRs splitting the byte-metrics work (followed by chunk-alloc-bytes and heap-alloc-bytes).

Adds an `arena size` value to each per-file block: the measured `Allocator`'s used bytes (`Allocator::used_bytes()`) at the end of the stage. It is a point-in-time gauge read after the operation, not a diffed counter — the arena is not reset between stages, so e.g. the transformer's value includes the AST the parser built.

```
checker.ts
  file size: 2.92 MB
  sys allocs: 1818
  sys reallocs: 10
  arena allocs: 264366
  arena reallocs: 22860
  arena size: 12985208 (12.99 MB)
```

Structurally, this introduces the counter/gauge split (`StageStats` wraps the diffed `AllocatorStats` counters plus point-in-time gauges), and byte values print as the exact number plus a human-readable size.

**Platform note**: byte totals vary between platforms (type layouts differ, e.g. hashbrown tables are wider on x86_64 than aarch64) while counts are identical everywhere, so snapshots record byte values from CI's platform (Linux x64). Regenerating on another platform diffs byte-value lines only; take those lines from the CI job's output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 13:21:08 +00:00
Boshen 31281d3d52 refactor(track-memory-allocations): switch snapshots to vertical per-file blocks (#24541)
Replaces the wide table in `tasks/track_memory_allocations/allocs_*.snap` with one vertical block per file, one `label: value` line per metric:

```
checker.ts
  file size: 2.92 MB
  sys allocs: 1818
  sys reallocs: 10
  arena allocs: 264366
  arena reallocs: 22860
```

Motivation:

- A changed counter now produces a one-line diff for exactly that metric, instead of a rewritten table row where you have to spot which of six columns moved. File names stay at column 0 so they show up in git hunk headers.
- No column alignment means adding a metric never reformats existing lines. New values are a single entry in the `(label, value)` array in `format_stats`, so the format extends without churn — more values coming later.

Snapshot values are byte-identical to before; only the layout changed. The header/width plumbing in `lib.rs` is gone (net −30 lines).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 12:20:33 +00:00
leaysgur 27f57d8ef7 fix(formatter): type alias comment idempotency (#24483)
```ts
type A1 = /* c */ { key: string };
```

Keep this code inline, previously line-break caused by comment.
2026-07-14 06:28:49 +00:00
Dunqing 616bfa24df feat(minifier): remove unreachable code after terminating statements (#24441)
Folding a define-replaced branch can leave `if (true) { ... return fn; }` as a bare block: the lexical declaration inside pins the block (flattening it could collide with sibling bindings), and the statements after it survived even though the block always returns. rolldown's per-module tree-shake hits this shape and ships the dead branch (rolldown/rolldown#10184).

`minimize_statements` only flagged dead control flow on direct jump statements, each arm setting the flag by hand. The driver now flips the flag whenever the statement it just emitted never completes normally: a direct jump, a block ending in a jump, an if/else where both branches jump, or a try where every path jumps. That single check subsumes the per-arm writes, so the `&mut bool` threaded through the return/throw handlers is gone. The removal reuses the existing dead-zone machinery, so `var` hoisting and kept function declarations behave as before, and both DCE and full-minify modes are covered.

A minimized dead zone keeps hoisting survivors after the jump — `function` declarations and the initializer-less `var` stub KeepVar re-emits — so the block predicate skips those from the back instead of testing only the literal last statement; nothing after an aborting statement ever runs, so they cannot change how the block completes.

The helper is conservative on purpose: labeled statements never count (`a: { break a; }` completes normally), and loops and switches are left to their specific folds. Terser removes the same tails; esbuild keeps them. The leftover `{ ... }` block wrapper is also kept — flattening a block whose lexical declarations may collide with siblings is a separate rewrite, and terser and esbuild keep the braces too.

## Example

`__SSR__` defined to `true` (rolldown treeshake, `minify: false`):

```js
export function test1() {
  if (true) {
    const fn = () => console.log("ssr");
    fn.stop = fn;
    return fn;
  }
  const fn = () => console.log("client");
  fn.stop = fn;
  return fn;
}
```

Before, the unreachable client tail stayed in the output. After:

```js
export function test1() {
	{
		const fn = () => console.log("ssr");
		fn.stop = fn;
		return fn;
	}
}
```

Verified with the minifier unit suite, test262/babel minifier conformance (no changes), DCE `--twice` idempotency, and `just minsize` (antd gzip 455.51 kB -> 455.50 kB, iteration counts unchanged).

Fixes rolldown/rolldown#10184.

Implemented with AI assistance (Claude Code); design, review, and verification driven by the author per the repo AI-usage policy.
2026-07-13 08:43:59 +00:00