fix(ecmascript): add argument validation for NewExpression side-effect detection#20395
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Merging this PR will not alter performance
Comparing Footnotes
|
305e281 to
ada6427
Compare
Merge activity
|
…e-effect detection Address review feedback from #20383 and #20395. The "Coercion Methods Are Pure" assumption (ASSUMPTIONS.md) means toString/valueOf/Symbol.toPrimitive are assumed side-effect-free, so per-constructor ToPrimitive validation is unnecessary for String, Number, Date, ArrayBuffer, and Error types. Changes: - String, Number, Date, ArrayBuffer, Error constructors now unconditionally pure (coercion assumed pure) - String(), Number(), Symbol() call expressions simplified to pure - TypedArray uses value_type() check: object args call @@iterator (NOT covered by coercion assumption), BigInt args cause ToNumber to throw - Removed 3 unnecessary helpers (new_expr_with_to_{string,number,primitive}_check) - Removed invalid issue link (XXXX) - Fixed is_pure_callable_constructor doc comment - BigInt() retains special-case validation (throws on invalid values) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
74c9ddd to
edb8677
Compare
ada6427 to
5ebee41
Compare
5ebee41 to
b03fc1d
Compare
…e-effect detection Address review feedback from #20383 and #20395. The "Coercion Methods Are Pure" assumption (ASSUMPTIONS.md) means toString/valueOf/Symbol.toPrimitive are assumed side-effect-free, so per-constructor ToPrimitive validation is unnecessary for String, Number, Date, ArrayBuffer, and Error types. Changes: - String, Number, Date, ArrayBuffer, Error constructors now unconditionally pure (coercion assumed pure) - String(), Number(), Symbol() call expressions simplified to pure - TypedArray uses value_type() check: object args call @@iterator (NOT covered by coercion assumption), BigInt args cause ToNumber to throw - Removed 3 unnecessary helpers (new_expr_with_to_{string,number,primitive}_check) - Removed invalid issue link (XXXX) - Fixed is_pure_callable_constructor doc comment - BigInt() retains special-case validation (throws on invalid values) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e-effect detection Address review feedback from #20383 and #20395. The "Coercion Methods Are Pure" assumption (ASSUMPTIONS.md) means toString/valueOf/Symbol.toPrimitive are assumed side-effect-free, so per-constructor ToPrimitive validation is unnecessary for String, Number, Date, ArrayBuffer, and Error types. Changes: - String, Number, Date, ArrayBuffer, Error constructors now unconditionally pure (coercion assumed pure) - String(), Number(), Symbol() call expressions simplified to pure - TypedArray uses value_type() check: object args call @@iterator (NOT covered by coercion assumption), BigInt args cause ToNumber to throw - Removed 3 unnecessary helpers (new_expr_with_to_{string,number,primitive}_check) - Removed invalid issue link (XXXX) - Fixed is_pure_callable_constructor doc comment - BigInt() retains special-case validation (throws on invalid values) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
b03fc1d to
7c0f44d
Compare
…t 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)
7c0f44d to
efeba28
Compare
…e-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
### 🚀 Features - 7215d9e transformer: Support lowering `accessor` with legacy decorators (#20348) (Dunqing) ### 🐛 Bug Fixes - 3bbd0cd transformer: Emit `Object` instead of `void 0` for untyped getter/setter `design:type` metadata (#20488) (Dunqing) - 4ae3f3f ecmascript: Apply coercion-is-pure assumption to constructor side-effect detection (#20420) (Dunqing) - 11f9695 transformer: Legacy decorator on computed property key leaves variable unassigned (#20430) (bab) - efeba28 ecmascript: Add argument validation for NewExpression side-effect detection (#20395) (Dunqing) Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>

Summary
is_pure_constructortreated 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:
new String(obj)toString()/valueOf()ToString->ToPrimitivenew Number(obj)valueOf()/toString(), throws on SymbolToNumeric->ToPrimitivenew Date(obj)[Symbol.toPrimitive]/valueOf()/toString()ToPrimitivenew TypedArray(obj)[Symbol.iterator]or.lengthgetterGetMethod(@@iterator)new ArrayBuffer(obj)valueOf()/toString()ToIndex->ToNumberCross-tool comparison
new String(obj)unsafeonly)new Number(obj)unsafeonly)new Date(obj)unsafeonly)new TypedArray(obj)Solution
is_pure_constructorintois_unconditionally_pure_constructor(Object, Boolean, Error types) andis_typed_array_constructorNewExpression::may_have_side_effectsusing the sameToPrimitive-based pattern already used byCallExpressionnew_expr_with_to_string_check,new_expr_with_to_number_check,new_expr_with_to_primitive_checkAlso fixes
"BitUint64Array"->"BigUint64Array"inis_known_global_constructoris_pure_collection_constructornow verifiesundefinedis actually global (not shadowed)Test plan
{toString(){}},{valueOf(){}})