Commit Graph

239 Commits

Author SHA1 Message Date
camc314 853ffabf34 fix(ecmascript): avoid charAt panic on 32-bit (#26244)
## Summary

- Return `OutOfRange` when a `StringCharAt` position cannot fit in `usize`, avoiding a panic on 32-bit targets.
- Cover the `u32` boundary in direct evaluation and minifier constant folding, alongside existing conversion and UTF-16 edge cases.
2026-09-02 09:07:00 +00:00
camc314 b874f480c9 fix(ecmascript): Math.round only exact half ties (#26150)
[ECMA-262 §21.3.2.29, `Math.round`](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math.round).

- If `0 < n < 0.5`, return `+0`.
- If `-0.5 ≤ n < -0`, return `-0`.
- Otherwise choose the closest integer, preferring `+∞` only for an actual tie.
2026-08-28 14:26:10 +00:00
camc314 a625378066 fix(minifier): coerce omitted indexOf search argument (#26149)
## Summary

- Apply `ToString(undefined)` when `String.prototype.indexOf` or `lastIndexOf` is called without a search argument.
- Keep folding strings without the resulting `"undefined"` substring to `-1`.
- Add focused helper and minifier regression coverage.

```js
"undefined".indexOf();     // 0
"undefined".lastIndexOf(); // 0
"abcdef".indexOf();        // -1
"abcdef".lastIndexOf();    // -1
```
2026-08-28 14:18:01 +00:00
camc314 dc7398beae fix(ecmascript): trim trailing whitespace in string to number (#26148)
## Summary

`StringToNumber` previously removed ECMAScript whitespace only from the beginning of a string. Legal trailing whitespace therefore remained in the value passed to Rust’s numeric parsers, causing valid JavaScript numeric strings to produce `NaN`.

For example:

```js
"1 " * 2       // incorrectly folded to NaN
"0x10 " - 0    // incorrectly folded to NaN
```

This changes `trim_start_matches` to `trim_matches`, applying the existing ECMAScript whitespace predicate to both ends of the string.

After the change:

```js
"1 " * 2       // folds to 2
"0x10 " - 0    // folds to 16
```

The custom predicate remains important because ECMAScript and Rust define whitespace differently. Tests cover every ECMAScript whitespace character and verify that non-ECMAScript whitespace such as U+0085 is not accepted.
2026-08-28 14:07:56 +00:00
Boshen 568203e24f perf(ecmascript): use binary search for known globals (#25817)
Replaces the 639-arm known-global string match with a sorted static table and binary search. The table is guarded for sortedness, uniqueness, and representative lookups.

Release `oxlint`: 12,491,264 → 12,474,736 bytes (−16,528 bytes, −0.13%); `__text` is reduced by 29,252 bytes.

Prepared with AI assistance.
2026-08-18 08:04:34 +00:00
Kotaro Chikuba f6000ac141 fix(ecmascript): fold ** with Number::exponentiate, not IEEE pow (#25644)
`f64::powf` implements IEEE 754 `pow`, which returns `1` for a base of
`1` whatever the exponent.
[`Number::exponentiate`](https://tc39.es/ecma262/#sec-numeric-types-number-exponentiate)
instead returns `NaN` when the exponent is `NaN` (step 1), and when the
base has magnitude `1` and the exponent is infinite (steps 9-10).

Constant folding called `powf` directly, so:

```js
1 ** Infinity      // spec: NaN    oxc folded to: 1
(-1) ** Infinity   // spec: NaN    oxc folded to: 1
1 ** -Infinity     // spec: NaN    oxc folded to: 1
1 ** NaN           // spec: NaN    oxc folded to: 1
```

`1` is truthy and `NaN` is not, so an `if` guarded by such an expression
takes the wrong branch.

The one case the two agree on — an exponent of `0` returning `1`
whatever the base, including `NaN ** 0` and `Infinity ** 0` — is
unchanged, since `powf` already gets it right and the spec checks the
exponent before the base.

`cargo minsize` shows no snapshot change.

Found by the minifier fuzzer (#25594).

---

Written with AI assistance (Claude Code). Reviewed by me, and every
expected value in the added test was checked against Node.js.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:21:54 +08:00
Kotaro Chikuba e82495ba54 fix(ecmascript): derive ToNumber of !x from ToBoolean (#25595)
Found by the semantic fuzzer added in #25594 (seed `15979`).

## What

`ToNumber(!x)` was computed from `ToNumber(x)` instead of
`ToBoolean(x)`:

```rust
Expression::UnaryExpression(unary) if unary.operator.is_not() => {
    let number = unary.argument.to_number(ctx)?;
    Some(if number == 0.0 { 1.0 } else { 0.0 })
}
```

`!x` is `ToBoolean(x)` negated, so the two predicates must agree — and
they don't. `NaN` is falsy but does not number to `0`, while `"0"`, `"
"`, `"0.0"` and `[]` all number to `0` yet are truthy.

## Why it was not caught

`minimize_conditional_expression` rewrites `a ? 1 : 0` into `+!!a`. That
rewrite is correct, but when a parent expression evaluated the freshly
built `+!!a` in the same pass, the broken branch flipped the result:

```js
(NaN ? 1 : 0) || 2   // 2, minified to 0
('0' ? 1 : 0) || 2   // 1, minified to 2
([]  ? 1 : 0) || 2   // 1, minified to 2
```

A `!!x` written in the source is folded correctly by `fold_unary_expr`
before any parent observes it, so only pass-generated nodes were
affected. That is why existing tests and the conformance suites pass.

refs https://github.com/oxc-project/oxc/pull/8267

## How

Derive the value from `to_boolean`. This is strictly more conservative:
when `to_boolean` returns `None` the fold is skipped rather than
answered wrongly.

## Testing

- New regression test `test_conditional_to_unary_plus_uses_to_boolean`
covering `NaN`, `"0"`, `" "`, `"0.0"` and `[]`.
- `cargo test -p oxc_minifier -p oxc_ecmascript`, `cargo clippy
--all-targets`, `cargo fmt --check` all pass.
- `just minsize`: no snapshot change, so no size regression.

## AI disclosure

This change was produced with AI assistance (Claude Code). I have
reviewed and understand every line; the root cause and each minimal
reproduction were verified by hand against Node.js.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 00:35:39 +00:00
camc314 88b34f2db2 fix(clippy): remove unneeded unsafe (#25551)
ah lint only runs on linux so this didn't fail previously in CI
2026-08-12 15:11:55 +00:00
CrazyboyQCD 48d5e9e47a refactor(ecmascript): use __jcvt() intrinsic on ARM64 for f64→i32 conversion (#24373)
- Simplify `cfg` by using `cfg_select` macro which is stabilized in 1.95
- Use `__jcvt()` intrinsic which is stabilized in 1.95
- Remove redundant `NAN` check since V8 has a set of test for it, which
claims that it works properly for `NAN` and `Infinity`:
https://github.com/v8/v8/blob/3b58a432df85d51bf1c439c30efcf0752de1076f/test/cctest/test-assembler-arm64.cc#L11416C1-L11468C48

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-12 09:15:55 +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
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
Dunqing 8a47ff3743 fix(ecmascript): do not treat lone-surrogate strings as constants (#25084)
The value of a StringLiteral and the cooked value of a template quasi encode lone surrogates with `\u{FFFD}` escapes and mark the node with a `lone_surrogates` flag. ToJsString and constant evaluation returned the encoded text, and consumers materialized it into new string literals without the flag, printing the escape encoding as literal text: `"\ud800" + "y"` folded to a string containing a literal `d800`.

Return `None` for such strings and template quasis so folds keep the original nodes.

```js
// source
console.log("\ud800" + "y");  // node prints: �y

// before (miscompiled)
console.log(`�d800y`);         // node prints: �d800y

// after
console.log(`\ud800`+`y`);     // node prints: �y
```
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
camc314 3f36b16556 refactor(ecmascript): impl MayHaveSideEffects for SequenceExpression (#25074)
simple refactor to extract out `SequenceExpression` side effects such that it now implements `MayHaveSideEffects` to allow us to call `SequenceExpression::may_have_side_effects`
2026-07-29 10:30:44 +00:00
camc314 5acdae22cb feat(ecmascript): skip transparent TS expressions (#25072)
This will be used by the linter.
2026-07-29 10:06:13 +00:00
Dunqing fbe8d318ff fix(minifier): respect targets when folding RegExp source (#24790)
RegExp literal `.source` folding used a hard-coded approximation of ES2015 support. It rejected syntax that configured targets support, such as the `u` flag for ES2015, while allowing newer syntax without checking the configured engines.

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

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

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

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

Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and submitting these changes under the repository AI usage policy.
2026-07-28 03:37:44 +00:00
Dunqing a086d91c83 perf(ecmascript): avoid number literal allocations (#24889)
The shared number-literal serializer can be called repeatedly by consumers that only need the emitted length. Its existing `Cow<str>` implementation allocates candidate strings for some hexadecimal and exponent forms, which would add avoidable allocation traffic to those callers.

Build candidates in an inline stack string while preserving the callback API and generated output. In allocation tracking, the unoptimized consumer added 56 system allocations for `antd.js` and 129 for `kitchen-sink.tsx`; this version removes those increases.

This PR depends on #24876. #24875 adds the minifier consumer.

Verified with `cargo test -p oxc_ecmascript -p oxc_codegen`, Clippy, formatting, and `just allocs` with no standalone snapshot changes.

AI assistance: Codex was used to implement and verify this change.
2026-07-26 12:11:48 +00:00
Dunqing dba374912a refactor(ecmascript): share number literal serialization (#24876)
Codegen is not the only consumer that needs the exact emitted form of a number, but the serializer currently lives in the codegen crate. Other consumers would otherwise need to duplicate its formatting policy or depend on codegen.

Move the callback helper extracted in #24888 from `oxc_codegen` to `oxc_ecmascript`, then reuse it from codegen. The serialization algorithm is unchanged; this PR only makes the helper public and relocates its dependencies.

This PR depends on #24888. #24889 handles allocation-free candidate construction, and #24875 adds the minifier consumer.

Verified independently with `cargo test -p oxc_ecmascript -p oxc_codegen`, Clippy, and formatting.

AI assistance: Codex was used to implement and verify this change.
2026-07-26 12:11:47 +00:00
sapphi-red 07e9acca08 fix(ecmascript): keep side effects in parent of toString calls (#24846)
Detect the side effect of`f()` in `(f(), 5).toString()`.
2026-07-24 01:16:58 +00:00
sapphi-red 16a40ad48c fix(ecmascript): detect side effects in unused arguments to charCodeAt (#24845)
Detect the side effect of`f()` in `'abcde'.charCodeAt(0, f())`.
2026-07-24 01:16:57 +00:00
sapphi-red 133bb35dee fix(ecmascript): detect side effects of foo().#bar correctly when PropertyReadSideEffects::None (#24769)
Side effects of`foo().#bar` was not detected correctly when PropertyReadSideEffects::None is used.

Repro: https://stackblitz.com/edit/oxc-project-website-l6tidw4j?file=src%2Fmain.js

refs https://github.com/oxc-project/oxc/pull/24707#discussion_r3614105531
2026-07-21 23:33:45 +00:00
Armano 9a59e1961d refactor(minifier): reuse is_typed_array_constructor from ecma (#24765)
refactor/reuse of `is_typed_array_constructor` from `oxc_ecmascript`
instead of redefining it it `oxc_minifier` as `is_typed_array_name`
2026-07-21 22:41:28 +08:00
Dunqing 79e6d775fb fix(minifier): preserve RegExp feature detection (#24712)
> **PR stack (3 of 3)**
> 1. #24739
> 2. #24742
> 3. **#24712 (this PR)**
>
> Review #24739 and #24742 first. This PR is based on #24742, so GitHub shows only the RegExp behavior change.

Stacked on #24742, which exposes strict engine-target capabilities to side-effect analysis.

RegExp constructor calls used for engine feature detection were removed by DCE when their pattern was valid according to the current Oxc RegExp parser. A pattern can be valid at build time but still throw a `SyntaxError` on the configured runtime, so removing the call changes the result of surrounding `try`/`catch` checks.

RegExp purity now requires both syntactic validity and support from every configured target. Consumers without concrete targets remain conservative, while an explicit target allows the minifier to remove constructors known to be safe for those engines.

Target-independent flag validation and pattern compatibility checks now live in `oxc_regular_expression`; side-effect analysis only maps target support to unsupported flags and patterns. String patterns, modern flags, named groups, property escapes, lookbehind, modifiers, invalid flags, and lone-surrogate strings are covered. A RegExp literal with replacement flags is removable only when the target supports both those flags and the dedicated `ES2015RegExpConstructorCanAlterFlags` compatibility feature. Explicitly targeting `esnext` opts into current features.

## Example

Input:

```js
export function supportsUnicodeRegExp() {
  try {
    new RegExp("\\p{Ll}", "u");
    return true;
  } catch {
    return false;
  }
}
```

Before:

```js
export function supportsUnicodeRegExp() {
  try {
    return true;
  } catch {
    return false;
  }
}
```

After:

```js
export function supportsUnicodeRegExp() {
  try {
    new RegExp("\\p{Ll}", "u");
    return true;
  } catch {
    return false;
  }
}
```

Verified with all `oxc_regular_expression`, `oxc_compat`, `oxc_ecmascript`, and `oxc_minifier` tests; the no-feature/`side_effects`/`constant_evaluation` `oxc_ecmascript` matrix; affected-crate clippy; formatting; and diff checks.

Fixes rolldown/rolldown#10279.

Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and submitting these changes under the repository AI usage policy.
2026-07-21 14:15:24 +00:00
Dunqing 01c3611d9b refactor(ecmascript): expose engine targets to side effects (#24742)
> **PR stack (2 of 3)**
> 1. #24739
> 2. **#24742 (this PR)**
> 3. #24712
>
> Review #24739 first. This PR is based on its branch, so GitHub shows only the engine-target plumbing.

Stacked on #24739, which introduces the `side_effects` and `constant_evaluation` feature boundary for `oxc_ecmascript`.

Side-effect analysis sometimes needs to know whether runtime syntax is supported by every configured engine. `MayHaveSideEffectsContext` currently has no access to target information, while `EngineTargets::has_feature` is intentionally permissive about missing compatibility data because it answers whether a transformation is needed.

`MayHaveSideEffectsContext::engine_targets()` exposes the configured targets through a general-purpose accessor. Its default is `None`, so consumers without target information remain conservative, while the minifier supplies its existing `CompressOptions::target`.

`EngineTargets::supports_es_feature` provides the corresponding strict capability query. Every configured engine must have compatibility data and meet the required version; empty targets and missing rows are not treated as proof of support. The existing transformation-oriented `has_feature` behavior remains unchanged.

Because `oxc_linter` enables `side_effects`, this also adds `oxc_compat` to the oxlint and language-server build graphs. It does not change side-effect decisions by itself; #24712 consumes the new capability for target-aware RegExp validation.

Verified with all `oxc_compat` tests, the no-feature/`side_effects`/`constant_evaluation` `oxc_ecmascript` matrix, a minifier check, formatting, and Clippy.

Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and maintaining these changes.
2026-07-21 14:15:23 +00:00
Dunqing 9a48d2308f refactor(ecmascript)!: gate analysis APIs (#24739)
> **PR stack (1 of 3)**
> 1. **#24739 (this PR)**
> 2. #24742
> 3. #24712
>
> Review and merge in this order.

The `oxc_ecmascript` crate currently compiles constant evaluation and side-effect analysis for every consumer, even when a consumer only needs the small shared ECMAScript value primitives. This makes optional analysis code and its dependencies part of unrelated builds.

This introduces two explicit features:

- `side_effects` enables side-effect analysis and its RegExp support.
- `constant_evaluation` enables constant evaluation and implies `side_effects`, because side-effect analysis uses constant folding for several purity decisions.

`ConstantValue`, `ValueType`, and `DetermineValueType` move to feature-independent root modules so `GlobalContext` remains available without either analysis feature. Existing paths through `constant_evaluation` remain re-exported when that feature is enabled.

This is a prerequisite for #24742, which exposes engine targets to side-effect analysis without making compatibility data part of every `oxc_ecmascript` consumer.

This is a breaking Cargo feature change for direct consumers of `oxc_ecmascript`: code using side-effect analysis or constant evaluation must enable the corresponding feature.

Verified locally with the no-feature, `side_effects`, and `constant_evaluation` configurations, crate tests, dependent linter and minifier tests, rustdoc with warnings denied, and `just ready`. The release oxlint binary on this commit is 13,350,096 bytes.

Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and maintaining these changes.
2026-07-21 14:15:23 +00:00
sapphi-red e750a8243a fix(ecmascript): fix false negative for may_have_side_effects on dynamic property access (#24709)
``[][`length${foo()}`]`` was treated as side effect free. But `foo()` can have side effects.
2026-07-20 09:13:59 +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
Lawrence Lin bd49aff795 perf(ecmascript): avoid heap-allocating Math.min/max/imul operands (#23941)
## Summary

`try_fold_math_variadic` constant-folds `Math.min` / `Math.max` /
`Math.imul(...)` by
collecting the operand values into a `Vec<f64>`. That is a **system
allocation on every
foldable call**, on the minifier's constant-folding hot path.

The evaluation context already exposes the arena
(`ConstantEvaluationCtx: GetAllocator`,
and the module already imports `ArenaVec`), so collect into an
`ArenaVec` instead. The
list is transient and only iterated / indexed (`.iter()`, `.first()`,
`.get()`), so the
change is behaviour-identical — the system allocation is simply gone.

## Results — `cargo allocs` (minifier system allocations)

| fixture | before | after |
| --- | ---: | ---: |
| kitchen-sink.tsx | 2576 | **2312** (−264) |
| antd.js | 1040 | **870** (−170) |
| pdf.mjs | 2569 | **2476** (−93) |
| App.tsx | 78 | 72 |

## Tests

- `cargo test -p oxc_ecmascript --lib --tests` — pass
- `cargo test -p oxc_minifier --lib --tests` — 539 pass

## AI disclosure

Prepared with AI assistance (Claude). I located the hotspot with a
per-call-site allocation
profile across real files, confirmed the list is
transient/iterated-only, and verified the
reduction with `cargo allocs` plus the test suites.

---------

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-07-05 14:21:15 +01:00
Dunqing e2744150c1 fix(minifier): don't drop global calls that throw despite pure arguments (#23917)
## What

`is_pure_global_method_call` (and the bare-global path) in `CallExpression::may_have_side_effects` treated a call as side-effect-free whenever its arguments were, ignoring that several of these functions **throw** at runtime. Unused calls that throw were therefore dropped as dead code. terser and esbuild both keep them.

Guard the cases oxc can prove:

| Call | Throws | Before | After |
| --- | --- | --- | --- |
| `String.raw()`, `Symbol.keyFor()`, `Symbol.keyFor(42)` | `TypeError` (missing/invalid required arg) | dropped | kept |
| `URL.canParse()` | `TypeError` (required first arg) | dropped | kept |
| `String.fromCodePoint(-1 / 1.5 / 0x110000)` | `RangeError` (code point outside `[0, 0x10FFFF]`) | dropped | kept |
| `Math.abs(10n)`, `Date.UTC(10n)`, `String.fromCharCode(10n)`, `isNaN(10n)`, … | `TypeError` (`ToNumber(BigInt)`) | dropped | kept |

Safe optimizations are preserved — `URL.canParse("x")`, `String.fromCodePoint(65)`, `String.fromCharCode(65)`, `Math.abs(1)` still drop. `String.raw` / `Symbol.keyFor` are removed from the pure-method list (never provably safe); the rest get argument guards.

### A Symbol argument is left droppable, on purpose

A Symbol argument also throws (`ToString` / `ToNumber` on a Symbol), but oxc has no `ValueType::Symbol` and `to_primitive` never resolves a pure expression to `Symbol`, so a Symbol throw is **never provable**. Guarding it would mean keeping every `Math.abs(x)` / `parseInt(x)` on an undetermined argument — a large minification regression. esbuild makes the same trade-off (it drops `Math.abs(Symbol.iterator)` too), so only the provable cases are guarded.

oxc already guards exactly this for the `Number` / `Symbol` / `BigInt` / `Error` constructors; this brings the method-call path in line. The assumption (including the non-provable-Symbol carve-out) is documented in `crates/oxc_minifier/docs/ASSUMPTIONS.md`.

Covered by `test_throwing_global_calls` in `crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs`; cross-checked against terser and esbuild; `minsize` unchanged.

---

Stacked on #23916.
2026-07-02 02:02:14 +00:00
Dunqing 60e716087d feat(minifier): drop side-effect-free IIFEs whose result is unused (#23967)
## Summary

- `CallExpression::may_have_side_effects` now looks through an IIFE — a call whose callee is a function/arrow literal: it is side-effect-free when its arguments, parameters, and every body statement are. Previously every such call was assumed effectful, so the minifier kept dead wrappers like `var unused = (function () { function test() {} return test })()`. Closes #23777.
- This is a plain improvement to side-effect detection in `oxc_ecmascript` — no DCE awareness there. The "preserve IIFE structure in DCE-only mode" policy (match Rollup / esbuild `--tree-shaking=true`, see `preserve_iife_in_dce_mode`) lives in the minifier, in one helper (`has_side_effects_or_preserved_iife`) shared by every unused-removal site that can face an IIFE call, so only full minification actually drops it.
- Conservative — skips `async`/generator callees (different invocation semantics) and any non-identifier parameter (defaults run user code, destructuring reads properties / drives the iterator protocol).
- Covered by `remove_unused_expression.rs::test_remove_side_effect_free_iife` (full-mode positive + negative) and `dead_code_elimination.rs::preserve_iife_in_dce_mode` (DCE-mode preservation: bare, unused-binding, sequence, and `for`-init positions). `minsize` unchanged, `allocs` slightly down.

## Example

```js
// input
var unused = (function () {
  function test() {}
  return test
})()
```

```js
// full minify, before — kept as a bare expression statement
(function () {
  function test() {}
  return test;
})();

// full minify, after
// (empty)
```

In dce-only / tree-shaking mode the IIFE structure is preserved (matches Rollup / esbuild `--tree-shaking=true`).
2026-07-01 03:36:07 +00:00
Dunqing dcc9a73b12 fix(minifier): treat Object.isExtensible/isFrozen/isSealed as pure (#23916)
## What

`Object.isExtensible`, `Object.isFrozen`, and `Object.isSealed` were grouped with the throwing introspection methods (`Object.keys`, `getOwnPropertyDescriptor`, …) in `CallExpression::may_have_side_effects`, so a call with a missing / `null` / `undefined` argument was conservatively kept.

Unlike those methods, the three `is*` methods never `ToObject` their target — per [spec](https://tc39.es/ecma262/#sec-object.isextensible) a non-object receiver returns a primitive (`false` / `true`) instead of throwing, so the call is side-effect-free and an unused one can be dropped.

```js
// in
Object.isExtensible();
Object.isFrozen(x);
Object.isSealed();
Object.keys();

// out
Object.isFrozen(x), Object.keys();
```

`isExtensible()` / `isSealed()` drop (pure); `isFrozen(x)` stays — an undetermined target could be a `Proxy`, whose `[[IsExtensible]]` / `[[OwnPropertyKeys]]` traps are observable — and `keys()` stays (it throws on the `undefined` receiver).

Covered by new cases in `crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs`; `minsize` unchanged.

Closes #23779
2026-06-29 13:51:39 +00:00
overlookmotel 8de5122230 feat(ecmascript)!: switch to new AstBuilder (#23834)
Part of #23043. Migrate `oxc_ecmascript` over to new `AstBuilder`.

This is a breaking change for users of `oxc_ecmascript` crate. `ConstantEvaluationCtx` no longer has an `ast` method to get an `AstBuilder`. Instead, any type that implements `ConstantEvaluationCtx` must also implement `oxc_allocator::GetAllocator` and `oxc_ast::builder::GetAstBuilder` - the latter proving a `builder` method which returns a `&AstBuilder`.

I'll aim to fix this to remove the breaking change in a follow-on PR (before next release).
2026-06-26 18:17:30 +00:00
overlookmotel f65d8e5291 refactor(all): shorten redundant std::vec::Vec (#23748)
Pure refactor.

#23748 made all usages of arena-allocated `Vec` be named `ArenaVec`. This makes plain `Vec` unambiguously refer to heap-allocated `Vec` from standard library.

Shorten `std::vec::Vec` to just `Vec` in various places.
2026-06-24 01:21:41 +00:00
overlookmotel d1f18cda21 refactor(all): import arena Box and Vec as ArenaBox / ArenaVec (#23747)
Pure refactor.

Use `ArenaBox` and `ArenaVec` everywhere for the arena-allocated types from `oxc_allocator`, instead of plain `Box` and `Vec`.

This disambiguates when `Vec` is `oxc_allocator::Vec` and when it's `std::vec::Vec`, making code clearer to follow.

Previously it was easy to confuse the two, and such confusion led to UB in `Traverse` (fixed in #23745).

Ditto other arena types - `ArenaHashMap`, `ArenaHashSet`, `ArenaStringBuilder`.

The only exceptions are:

1. Inside `oxc_allocator` crate itself.
2. AST type definitions.
3. Generated code (migrated separately in #23749).
2026-06-24 01:21:41 +00:00
Dunqing 31bfd9bc75 fix(minifier): keep Object introspection calls on a possible Proxy (#23483)
## Summary

Closes #17157.

`Object.keys`, `getOwnPropertyDescriptor(s)`, `getOwnPropertyNames`, `getOwnPropertySymbols`, `getPrototypeOf`, `hasOwn`, `isExtensible`, `isFrozen`, and `isSealed` introspect their target through internal methods a `Proxy` can trap (`[[OwnPropertyKeys]]`, `[[GetOwnProperty]]`, …), so they have observable side effects when the target is a Proxy. They were treated as unconditionally pure and dropped when unused — so `Object.getOwnPropertyDescriptor(proxy, "foo")` / `Object.keys(proxy)` were silently removed, skipping the trap.

They are now treated as side-effect-free only when the target is provably not a Proxy (a determined, non-null value type — i.e. a literal), unless the bundler opts out of property-read side effects. A `null`/`undefined`/missing target is also kept (it throws a `TypeError`).

- `Object.create(proto)` is pure only with an object/`null` prototype and no `properties` argument (read via `[[OwnPropertyKeys]]`/`[[Get]]`).
- `Object.values`/`entries` are no longer treated as pure at all — they invoke `[[Get]]` and run getters even on a plain object.
- `Object.is` stays unconditionally pure.

## Credit

Adapted from #21056 by @gthb. Their `proxy_sensitive_arg_index` approach is preserved; this version adds corrections for the getter-invoking methods (`values`/`entries`, and `create` with properties) and the throw-on-`null`/`undefined` cases that the original treated as droppable.

Verified by `cargo test -p oxc_minifier` / `-p oxc_ecmascript`; `minsize` unchanged.

cloese https://github.com/oxc-project/oxc/pull/21056
2026-06-17 08:07:58 +00:00
Dunqing 53509a85df feat(minifier): treeshake pure typed arrays and Set/Map array literals (#23469)
## Summary

Mark side-effect-free `new` constructions pure so they are dropped when unused (and annotated for downstream tree-shaking):

- **TypedArrays** with a non-negative numeric-literal length (`new Int8Array(8)`, `new Uint8Array(1024)`). The buffer allocation runs no user code; a valid length is pure and a too-large length only throws a maximum-length `RangeError`, which the minifier is allowed to drop (see `docs/ASSUMPTIONS.md`). `new Int8Array(-1)` (negative-length `RangeError`), `new Int8Array(0n)` (BigInt `TypeError`), object, and non-literal arguments are kept.
- **`new Set([...])` / `new Map([[..], ..])`** with array-literal arguments; element side effects are preserved when the construction is dropped. `WeakSet`/`WeakMap` are excluded (their keys must be objects, so `new WeakSet([1])` throws), and `Map` entries must be array literals (`new Map([1])` throws).

This matches esbuild's per-constructor handling — the only other minifier that keeps every throwing form; Rollup, Terser (`unsafe`), and SWC drop some of these incorrectly. Cross-verified against the ES spec, esbuild, Terser, Rollup, and SWC.

Minified-size impact: react.development.js −0.04 kB; size-neutral elsewhere (real code uses dynamic buffer lengths, not numeric literals).
2026-06-16 07:46:28 +00:00
Alexander Lichter 0f26de6dd2 fix(ecmascript): resolve identifier value type via tracked constants (#22234)
`value_type` for `Expression::Identifier` returned `Undetermined` for
any non-global identifier, **even when the binding had zero writes and a
known constant initializer**. This causes code like `slot?.()` on a `let
slot;` with all writers tree-shaken away not being folded to `void 0`,
leaving dead code in the output.

This PR changes that by checking
`GlobalContext::get_constant_value_for_reference_id` for non-global
identifier references and mapping the resulting `ConstantValue` to a
`ValueType` via a new private helper.

Once the identifier resolves to a tracked nullish value, the existing
nullish branch in `fold_chain_expr` collapses the chain to `void 0`. The
same lookup also works for the opposite case (#21923).

The lookup is gated by making sure there are no write references, so
anything that is being written anywhere keeps its `Undetermined` type.

Closes rolldown/rolldown#9281
Related (but does not resolve) #21923

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-05-18 16:59:34 +08:00
Dunqing d7a359a0cc fix(ecmascript): treat update expressions as unconditionally side-effectful (#21456)
## Summary

`obj.prop++` performs an implicit `GetValue` + `ToNumeric` + `PutValue`. The `ToNumeric` coercion alone can invoke `valueOf`/`Symbol.toPrimitive` on the old value, and the read/write may trigger getters, setters, or Proxy traps.

Previously `UpdateExpression::may_have_side_effects` only checked `property_write_side_effects()` and treated the update as free when disabled — letting the minifier drop `counter.value++` even when `counter` is externally observable.

## Fix

Return `true` unconditionally for `UpdateExpression`. Terser, esbuild, Rollup, and SWC all hardcode `++`/`--` as unconditionally side-effectful — this matches their behavior, and mirrors how compound assignments like `a.b += 1` are already handled one impl above (`AssignmentExpression`).

## Impact

Under rolldown's tree-shake config (`propertyWriteSideEffects: false`), these are now correctly preserved instead of being dropped:

```js
counter.value++;
(class { static { ++counter.count; } });
class A { [counter.another++] = 123; }
```

## Reported

rolldown/rolldown#9094 ([comment](https://github.com/rolldown/rolldown/pull/9094#discussion_r3083594834))
2026-04-15 14:12:57 +00:00
sapphi-red 2338e28f59 fix(ecmascript): treat this as potentially having side effects (#21297)
fixes https://github.com/oxc-project/oxc/issues/21296 by treating `this` as potentially having side effects.
We can reduce the case that is bailed out by checking whether `this` is written inside `constructor` or not in the future.
2026-04-11 14:25:36 +00:00
overlookmotel c9cd809e50 refactor(str)!: rename Atom to Str (#20835)
Pure refactor.

Rename `Atom` to `Str`. The name "Atom" is a leftover from when we used to intern strings (years ago), and is misleading now.

Just call it `Str`. This follows the naming convention we have for arena-allocated versions of other types e.g. `Box`, `Vec`, `HashMap` - which we name the same as the native equivalents but with a lifetime (`Box<'a>` not `ArenaBox<'a>`, so `Str<'a>` not `ArenaStr<'a>`).

Rename all variables that contained "atom", and methods which relate to `Str`s to match the new type name.

Rename `as_atom` methods to `as_arena_str`. The longer name is required here to disambiguate from `as_str` methods which return a `&str`.

Some variables called "atom" still exist in `oxc_regular_expression` crate - that's a different meaning of "atom" - the meaning defined in regexp spec.

The diff of this PR is large but it's 100% renaming. No substantive or unrelated changes whatsoever.
2026-03-30 02:40:33 +00:00
Dunqing 4ae3f3fbc9 fix(ecmascript): apply coercion-is-pure assumption to constructor side-effect detection (#20420)
## Summary

Address review feedback from #20383 and #20395. Apply the "Coercion Methods Are Pure" assumption (`ASSUMPTIONS.md`) — `.toString()`/`.valueOf()`/`[Symbol.toPrimitive]()` are assumed side-effect-free — while still checking for spec-mandated throws that are NOT covered by that assumption.

### Changes

**`new String/Number(arg)`** — `ToString(Symbol)` / `ToNumber(Symbol)` throws TypeError. Check via `new_expr_first_arg_may_be_symbol`. BigInt is fine for both (`ToString(BigInt)` works, Number converts BigInt→Number).

**`new Date/ArrayBuffer(arg)`** — `ToNumber(Symbol)` AND `ToNumber(BigInt)` throw TypeError. Check via `new_expr_first_arg_may_be_symbol_or_bigint`.

**Error constructors** — `new Error(Symbol())` / `Error(Symbol())` throw (`ToString(Symbol)`). Removed from `is_unconditionally_pure_constructor` and `is_pure_callable_constructor`. Added `is_error_constructor()` helper with Symbol check.

**TypedArray constructors** — use `value_type()` instead of `to_primitive()`: object args call `@@iterator` (NOT a coercion method), BigInt args cause `ToNumber` to throw. Only known-safe primitive value types (Number/String/Boolean/Null/Undefined) accepted.

**`String(x)` call** — unconditionally pure. `String()` as a function has special Symbol handling per spec (`String(Symbol())` → `"Symbol()"` without throwing). Added to `is_pure_callable_constructor`.

**`Number(x)` / `Symbol(x)` calls** — keep special handling (`ToNumeric(Symbol)` / `ToString(Symbol)` throw).

**`is_pure_collection_constructor`** — now verifies `undefined` is actually global (not shadowed). Removed invalid issue link (`XXXX`).

**Also fixes:** typo `"BitUint64Array"` → `"BigUint64Array"` in `is_known_global_constructor`.

### Cross-tool comparison

| Expression | ES Spec | esbuild | Terser | Rollup | SWC | Oxc (this PR) |
|---|---|---|---|---|---|---|
| `new String(x)` | impure (Symbol) | impure | impure | pure | impure | **impure** (Symbol check) |
| `new Number(x)` | impure (Symbol) | impure | impure | pure | impure | **impure** (Symbol check) |
| `new Date(x)` | impure (Symbol/BigInt) | impure | impure | pure | impure | **impure** (Symbol+BigInt) |
| `new Date(0n)` | impure (BigInt) | impure | impure | pure | impure | **impure** (BigInt check) |
| `new ArrayBuffer(x)` | impure (Symbol/BigInt) | impure | impure | pure | impure | **impure** (Symbol+BigInt) |
| `new Error(x)` | impure (Symbol) | impure | impure | pure | impure | **impure** (Symbol check) |
| `new Uint8Array(x)` | impure (@@iterator) | impure | impure | pure | impure | **impure** (value_type) |
| `new Uint8Array({})` | impure (@@iterator) | impure | impure | pure | impure | **impure** (value_type) |
| `String(x)` call | safe (Symbol handled) | impure | impure | pure | impure | **pure** (coercion assumption) |
| `Error(x)` call | impure (Symbol) | impure | impure | pure | impure | **impure** (Symbol check) |

Oxc sits between esbuild (most conservative) and Rollup (most aggressive): uses the coercion assumption to skip ToPrimitive checks on objects, but catches spec-mandated throws (`ToString(Symbol)`, `ToNumber(Symbol)`, `ToNumber(BigInt)`, `@@iterator` on objects).

## Test plan

- [x] All 473 `oxc_minifier` tests pass (39 ignored)
- [x] All 13 `oxc_ecmascript` tests pass
- [x] Updated test expectations with new cases (`new Date(0n)`, `new ArrayBuffer(0n)`, `new Error(x)`, `Error(x)`)
- [x] Cross-tool research verified against ES Spec, esbuild, Terser, Rollup, SWC
2026-03-17 15:44:46 +00:00
Dunqing 878eace78e refactor(ecmascript): consolidate side-effect known globals into separate module (#20401)
## Summary

- Extract all global lookup tables (~430 lines) into a new `known_globals.rs` module, keeping `expressions.rs` focused on `MayHaveSideEffects` trait implementations
- Extract `is_pure_math_method` to deduplicate 36 Math method names shared between read-safety (`is_known_global_property`) and call-safety (`is_pure_global_method_call`) checks
- Extract inline call-safe match into a named `is_pure_global_method_call` function with clear doc comments
- Rename `is_pure_call` → `is_pure_callable_constructor`, removing 4 dead entries (`String`, `Number`, `BigInt`, `Symbol`) already handled by earlier special-case logic
- Redefine `is_unconditionally_pure_constructor` in terms of `is_pure_callable_constructor` to keep error-type lists in sync
- **Fix**: use `is_typed_array_constructor` for the TypedArray `.of()` arm, adding previously missing `BigInt64Array` and `BigUint64Array`

## Test plan

- [x] Added tests for `BigInt64Array.of()` and `BigUint64Array.of()`
- [x] All existing `side_effects` tests pass
- [x] `cargo clippy` clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-17 15:44:46 +00:00
Dunqing efeba2802e fix(ecmascript): add argument validation for NewExpression side-effect detection (#20395)
## Summary

`is_pure_constructor` treated many global constructors as unconditionally side-effect-free regardless of arguments. Cross-tool research (ES spec, esbuild, Terser, Rollup, SWC) confirmed this was the most permissive of all bundlers/minifiers and introduced real bugs.

### Problem

These constructors perform observable operations on object arguments but were treated as pure:

| Constructor | Side effect | ES Spec operation |
|---|---|---|
| `new String(obj)` | Calls `toString()`/`valueOf()` | `ToString` -> `ToPrimitive` |
| `new Number(obj)` | Calls `valueOf()`/`toString()`, throws on Symbol | `ToNumeric` -> `ToPrimitive` |
| `new Date(obj)` | Calls `[Symbol.toPrimitive]`/`valueOf()`/`toString()` | `ToPrimitive` |
| `new TypedArray(obj)` | Calls `[Symbol.iterator]` or `.length` getter | `GetMethod(@@iterator)` |
| `new ArrayBuffer(obj)` | Calls `valueOf()`/`toString()` | `ToIndex` -> `ToNumber` |

### Cross-tool comparison

| Constructor | esbuild | Terser | Rollup | SWC | Oxc (before) |
|---|---|---|---|---|---|
| `new String(obj)` | impure | impure (`unsafe` only) | pure | pure | **pure** |
| `new Number(obj)` | impure | impure (`unsafe` only) | pure | pure | **pure** |
| `new Date(obj)` | impure | impure (`unsafe` only) | pure | impure | **pure** |
| `new TypedArray(obj)` | impure | impure | pure | impure | **pure** |

### Solution

- Split `is_pure_constructor` into `is_unconditionally_pure_constructor` (Object, Boolean, Error types) and `is_typed_array_constructor`
- Add per-constructor argument validation in `NewExpression::may_have_side_effects` using the same `ToPrimitive`-based pattern already used by `CallExpression`
- Three shared helpers: `new_expr_with_to_string_check`, `new_expr_with_to_number_check`, `new_expr_with_to_primitive_check`

### Also fixes

- Typo `"BitUint64Array"` -> `"BigUint64Array"` in `is_known_global_constructor`
- `is_pure_collection_constructor` now verifies `undefined` is actually global (not shadowed)

## Test plan

- Added 52 new test cases covering primitive args (safe), unknown/object args (side-effectful), and edge cases (`{toString(){}}`, `{valueOf(){}}`)
- All existing tests continue to pass
- Verified no regressions in Rolldown (1639/1639 tests pass with this change patched in)
2026-03-17 14:53:24 +00:00
Dunqing edb8677e26 fix(ecmascript): treat collection constructor with variable arg as side-effectful (#20383)
## Summary

`new Map(x)`, `new Set(x)`, `new WeakMap(x)`, `new WeakSet(x)` were incorrectly considered side-effect-free when the argument is a variable reference. These constructors iterate their argument via `Symbol.iterator`, which can execute arbitrary user code (custom iterators, proxies, etc.).

**Before:** `is_pure_constructor` unconditionally included `Map`/`Set`/`WeakMap`/`WeakSet`, so `new Map(variable)` was treated as pure.

**After:** Collection constructors are only pure with provably safe arguments:
- No arguments: `new Set()`
- `null` or `undefined`: `new Set(null)`
- Array literals: `new Set([1, 2, 3])`, `new Map([[k, v]])`
- Variable references or other expressions: **NOT pure**

This matches the behavior of **esbuild** and **Rollup**:
- esbuild: only marks pure with no args, null, undefined, or array literals ([test cases](https://github.com/evanw/esbuild/blob/main/internal/js_parser/js_parser_test.go))
- Rollup: uses `PURE_WITH_ARRAY` — only array literals are trusted ([knownGlobals.ts](https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts))

**Found via:** Rolldown's `tree-shake-iterable` test failing after delegating `NewExpression` side-effect analysis to Oxc.

## Test plan

- [x] Added tests for all cases: null, undefined, array literals, variable refs, Map with non-array entries
- [x] All existing `oxc_minifier` tests pass (473 passed)
- [x] Verified against Rolldown's `tree-shake-iterable` and `manual-pure-functions` test suites

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-16 08:49:52 +00:00
Boshen e62524d618 fix(minifier): treat object spread of getters as having side effects (#20380)
## Summary

- Object spread (`...{ get prop() {} }`) invokes getters at runtime, so
it has side effects
- The previous code only checked key/value AST nodes and missed the
getter invocation
- Setters are **not** invoked by spread, so they remain side-effect-free
- Fixes the Rolldown test `rollup@function@object-spread-side-effect`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-14 23:04:28 +08:00
Boshen 5c97b14bbc fix(minifier): recognize object spread of object literals as side-effect-free (#20299)
## Summary

- Add `ObjectExpression` handling in `ObjectPropertyKind::SpreadProperty`'s `may_have_side_effects` — spreading an object literal is side-effect-free when all its properties are
- Fix `remove_unused_object_expr` to check side effects for all-spread objects instead of unconditionally bailing

Closes rolldown/rolldown#8582
2026-03-13 04:56:43 +00:00
Dunqing ade14d4510 fix(ecmascript): enhance side-effect detection for classes, TypedArrays, computed members, and spread (#20213)
## Summary

Part of https://github.com/oxc-project/oxc/issues/19673

Several side-effect detection improvements:

- **Computed member expressions**: When `property_read_side_effects` is `None`, non-literal keys (e.g. `obj[identRef]`) now check the key expression and object for their own side effects instead of always returning `true`
- **ObjectExpression spread**: When `property_read_side_effects` is `None`, `{...expr}` delegates to the argument's own side effects
- **Class MethodDefinition**: Now checks for parameter decorators (TypeScript `@foo` on params)
- **Class AccessorProperty**: Now checks accessor property values for side effects
- **TypedArray constructors**: Added `Int8Array`, `Uint8Array`, `Uint8ClampedArray`, `Int16Array`, `Uint16Array`, `Int32Array`, `Uint32Array`, `Float32Array`, `Float64Array`, `BigInt64Array`, `BigUint64Array` to pure constructors (`DataView` excluded as it requires an `ArrayBuffer` argument)

## Test plan

- [x] Added tests for computed member non-literal keys with `property_read_side_effects`
- [x] Added tests for ObjectExpression spread with `property_read_side_effects`
- [x] Added tests for class method parameter decorators
- [x] Added tests for class accessor property values
- [x] Added tests for TypedArray constructors
- [x] All existing tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-12 09:22:57 +00:00
Dunqing e7163b6720 feat(ecmascript): add known-globals to side-effect-free property reads (#20212)
## Summary

close: #13067

Part of https://github.com/oxc-project/oxc/issues/19673

Depends on #20217.

Port known-globals lists from Rolldown to Oxc's side-effect detection:

- **Global identifiers** (Math, Array, console, DOM classes, etc.) are now considered side-effect-free to access via `is_known_global_identifier`, matching Rolldown's `GLOBAL_IDENT` set
- **Property reads** like `Math.PI`, `console.log`, `Object.keys` are considered side-effect-free via `is_known_global_property`
- **3-level chains** like `Object.prototype.hasOwnProperty` are supported via `is_known_global_property_deep`

## Test plan

- [x] Added tests for known global identifiers (Math, Array, Object, console, etc.)
- [x] Added tests for known global property reads (Math.PI, Object.keys, etc.)
- [x] Added tests for 3-level chains (Object.prototype.hasOwnProperty, etc.)
- [x] Existing minifier test expectations updated for improved side-effect detection
- [x] All existing tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-12 09:22:57 +00:00
Dunqing 139ab685f9 feat(ecmascript): add property_write_side_effects to MayHaveSideEffectsContext (#20217)
Part of https://github.com/oxc-project/oxc/issues/19673

## Summary

- Add `property_write_side_effects()` to `MayHaveSideEffectsContext` trait with default `true` (conservative)
- Implement `MayHaveSideEffects` for `AssignmentExpression` and `UpdateExpression` (previously fell through to `_ => true`)
- When `property_write_side_effects` is `false`, member expression writes are side-effect-free
- Compound assignments (`+=`) and updates (`++`) still respect `property_read_side_effects` for their implicit reads
- Reference: https://rolldown.rs/reference/InputOptions.treeshake#propertywritesideeffects

## Test plan

- [x] Added `test_property_write_side_effects_support` covering all combinations of write/read side-effect settings
- [x] Existing tests pass unchanged
2026-03-12 07:25:05 +00:00