## 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.
## 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
```
## 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.
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.
`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>
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>
## 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
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
```
## 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
simple refactor to extract out `SequenceExpression` side effects such that it now implements `MayHaveSideEffects` to allow us to call `SequenceExpression::may_have_side_effects`
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.
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.
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.
> **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.
Fixesrolldown/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.
> **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.
> **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.
## 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.
## 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>
## 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.
## 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`).
## 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
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).
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.
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).
## 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
## 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).
`value_type` for `Expression::Identifier` returned `Undetermined` for
any non-global identifier, **even when the binding had zero writes and a
known constant initializer**. This causes code like `slot?.()` on a `let
slot;` with all writers tree-shaken away not being folded to `void 0`,
leaving dead code in the output.
This PR changes that by checking
`GlobalContext::get_constant_value_for_reference_id` for non-global
identifier references and mapping the resulting `ConstantValue` to a
`ValueType` via a new private helper.
Once the identifier resolves to a tracked nullish value, the existing
nullish branch in `fold_chain_expr` collapses the chain to `void 0`. The
same lookup also works for the opposite case (#21923).
The lookup is gated by making sure there are no write references, so
anything that is being written anywhere keeps its `Undetermined` type.
Closesrolldown/rolldown#9281
Related (but does not resolve) #21923
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
## 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))
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.
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.
## 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)
## 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)
## 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>
## 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
Closesrolldown/rolldown#8582
## 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)
## 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)
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