Skip to content

fix(minifier): preserve Object method calls when arg could be a Proxy#21056

Open
gthb wants to merge 9 commits into
oxc-project:mainfrom
gthb:fix-proxy-side-effect-blind-spots
Open

fix(minifier): preserve Object method calls when arg could be a Proxy#21056
gthb wants to merge 9 commits into
oxc-project:mainfrom
gthb:fix-proxy-side-effect-blind-spots

Conversation

@gthb

@gthb gthb commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

What

Preserve calls to Object methods (keys, values, entries, getOwnPropertyDescriptor, hasOwn, getPrototypeOf, etc.) when the target argument could be a Proxy, since Proxy handler traps are observable side effects.

Previously the minifier treated all these Object methods as unconditionally side-effect-free, removing calls like Object.getOwnPropertyDescriptor(proxy, "foo") even when the argument was a Proxy with a getOwnPropertyDescriptor trap.

Also handle Object.create(proto, props) where the second argument (propertiesObject) could be a Proxy, since ObjectDefineProperties reads [[OwnPropertyKeys]] and [[Get]] on it.

Closes #17157

How

Add a proxy_sensitive_arg_index(object, method) -> Option<usize> function in known_globals.rs that maps each proxy-sensitive method to the argument index that must not be a Proxy. This covers first-argument methods (keys, values, entries, getOwnPropertyDescriptor, hasOwn, getPrototypeOf, isExtensible, isFrozen, isSealed, etc.) and Object.create (second argument). Object.is stays in is_pure_global_method_call as unconditionally pure.

For the sensitive methods, check:

  1. whether evaluating the arguments themselves has side effects (catches inline new Proxy(...) since Proxy is not a pure constructor)
  2. whether PropertyReadSideEffects::None is set, in which case Proxy traps are irrelevant (matching Rollup's treeshake.propertyReadSideEffects: false)
  3. the relevant argument's value_type() — if Undetermined, the argument could be a Proxy. Spread arguments are conservatively treated as side-effectful.

gthb added 2 commits April 4, 2026 22:18
…Proxy

What
---

Preserve calls to Object methods (`keys`, `getOwnPropertyDescriptor`,
`hasOwn`, `getPrototypeOf`, etc.) when the target argument could be a
Proxy, since Proxy handler traps are observable side effects.

Previously the minifier treated all these Object methods as
unconditionally side-effect-free, removing calls like
`Object.getOwnPropertyDescriptor(proxy, "foo")` even when the argument
was a Proxy with a `getOwnPropertyDescriptor` trap.

Also handle `Object.create(proto, props)` where the second argument
(propertiesObject) could be a Proxy, since `ObjectDefineProperties`
reads `[[OwnPropertyKeys]]` and `[[Get]]` on it.

Closes oxc-project#17157

How
---

Split the Object methods previously listed in `is_pure_global_method_call`
into three categories:

- Unconditionally pure: `Object.is` (SameValue comparison, no object
  introspection).
- First-argument-sensitive: `getOwnPropertyDescriptor`, `keys`, `hasOwn`,
  `getPrototypeOf`, `isExtensible`, `isFrozen`, `isSealed`, and others
  that invoke Proxy-trappable internal methods (`[[GetOwnProperty]]`,
  `[[OwnPropertyKeys]]`, `[[GetPrototypeOf]]`, `[[IsExtensible]]`) on
  their first argument.
- Second-argument-sensitive: `Object.create`, where the optional
  `propertiesObject` is introspected via `[[OwnPropertyKeys]]` and
  `[[Get]]`.

For the sensitive methods, first check whether evaluating the arguments
themselves has side effects (this catches cases like `new Proxy(...)`
as an inline argument, since `Proxy` is not a pure constructor). Then
check the relevant argument's `value_type()`: if `Undetermined` (a
variable, call result, or other expression whose type is not statically
known), the argument could be a Proxy, so the call is treated as having
side effects. Primitive literals and object/array/function literals have
known types and cannot be Proxies.

This follows the existing pattern used for `instanceof` Proxy detection
in the same file.
@github-actions github-actions Bot added A-minifier Area - Minifier C-bug Category - Bug labels Apr 4, 2026
JustFly1984 added a commit to JustFly1984/oxc that referenced this pull request Apr 10, 2026
…Proxy

Cherry-picked from oxc-project#21056 (commit e0ae3be)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gthb added 5 commits April 13, 2026 12:42
Replace `is_proxy_sensitive_object_method()` + separate `Object.create`
special case with a single `proxy_sensitive_arg_index()` function that
returns which argument index must not be a Proxy. This eliminates code
duplication between first-arg and second-arg checking paths and
centralizes all proxy-sensitivity knowledge in one place.

No behavior change.
…nsitive methods

Object.keys(...x) was incorrectly treated as pure because
Argument::as_expression() returns None for SpreadElement, and the
subsequent is_some_and() defaulted to false. Use map_or(true, ...) so
spread elements are conservatively treated as having side effects,
matching the pattern used in the Number/Symbol constructor checks.
…methods

These Object methods internally perform property reads ([[OwnPropertyKeys]],
[[Get]], [[GetOwnProperty]]). When the context says property reads are
side-effect-free (Rollup's treeshake.propertyReadSideEffects: false),
these calls should be pure regardless of Proxy risk. This matches 6
other places in the same file that already check this setting.
…methods

These use EnumerableOwnProperties which calls [[OwnPropertyKeys]] and
[[Get]] on their first argument, same as Object.keys. Previously they
fell through to "always impure"; now Object.values({}) and
Object.entries({}) are correctly recognized as pure.
@codspeed-hq

codspeed-hq Bot commented Apr 13, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 57 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing gthb:fix-proxy-side-effect-blind-spots (ee03243) with main (78cf83f)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Dunqing added a commit that referenced this pull request Jun 16, 2026
`Object.keys`, `getOwnPropertyDescriptor(s)`, `getOwnPropertyNames`,
`getOwnPropertySymbols`, `getPrototypeOf`, `hasOwn`, `isExtensible`, `isFrozen`,
and `isSealed` introspect their target via 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 -- e.g. `Object.getOwnPropertyDescriptor(proxy, "foo")`
was silently removed, skipping the trap.

Treat them 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 kept too,
since 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, as they invoke `[[Get]]` and run getters even on a
plain object. `Object.is` stays unconditionally pure.

Adapted from #21056 by Gunnlaugur Thor Briem, with corrections for the
getter-invoking methods (`values`/`entries`, `create` with properties) and the
throw-on-`null`/`undefined` cases.

Closes #17157

Co-authored-by: Gunnlaugur Thor Briem <gunnlaugur@gmail.com>
graphite-app Bot pushed a commit that referenced this pull request Jun 17, 2026
…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 #21056
camc314 pushed a commit that referenced this pull request Jul 3, 2026
…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 #21056
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-minifier Area - Minifier C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

minifier: call expressions for Object.getOwnPropertyDescriptor is not preserved when combined with proxies

1 participant