mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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
This commit is contained in:
1 parent
837a395fba
commit
31bfd9bc75
5 files changed
+143
-17
No files matched your search
@@ -27,6 +27,10 @@ impl ValueType {
|
||||
self == Self::Null
|
||||
}
|
||||
|
||||
pub fn is_null_or_undefined(self) -> bool {
|
||||
matches!(self, Self::Null | Self::Undefined)
|
||||
}
|
||||
|
||||
pub fn is_string(self) -> bool {
|
||||
self == Self::String
|
||||
}
|
||||
|
||||
@@ -603,7 +603,71 @@ impl<'a> MayHaveSideEffects<'a> for CallExpression<'a> {
|
||||
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
|
||||
}
|
||||
|
||||
true
|
||||
if object.name != "Object" {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `Object` static methods that run no user code but whose purity depends on
|
||||
// their arguments.
|
||||
match name {
|
||||
// Introspection methods (`keys`, `getOwnPropertyDescriptor`, ...) are pure
|
||||
// except that a `Proxy` target makes them fire observable traps, and a
|
||||
// `null`/`undefined` target makes them throw. They introspect their first
|
||||
// argument via internal methods a Proxy can trap (`[[OwnPropertyKeys]]`,
|
||||
// `[[GetOwnProperty]]`, `[[GetPrototypeOf]]`, `[[IsExtensible]]`) but,
|
||||
// unlike `Object.values`/`entries`, never invoke `[[Get]]`, so they run no
|
||||
// getter on a plain object. Side-effect-free only when the target is
|
||||
// provably neither a Proxy nor nullish.
|
||||
"getOwnPropertyDescriptor"
|
||||
| "getOwnPropertyDescriptors"
|
||||
| "getOwnPropertyNames"
|
||||
| "getOwnPropertySymbols"
|
||||
| "getPrototypeOf"
|
||||
| "hasOwn"
|
||||
| "isExtensible"
|
||||
| "isFrozen"
|
||||
| "isSealed"
|
||||
| "keys" => {
|
||||
// An argument with its own side effects (e.g. inline `new Proxy(...)`) — keep.
|
||||
if self.arguments.iter().any(|e| e.may_have_side_effects(ctx)) {
|
||||
return true;
|
||||
}
|
||||
// Missing or spread target → `undefined` receiver → `ToObject` throws.
|
||||
let Some(arg) = self.arguments.first().and_then(Argument::as_expression) else {
|
||||
return true;
|
||||
};
|
||||
let value_type = arg.value_type(ctx);
|
||||
// `null`/`undefined` → `ToObject` throws a TypeError; keep the call.
|
||||
if value_type.is_null_or_undefined() {
|
||||
return true;
|
||||
}
|
||||
// With property reads assumed pure, the Proxy-trap concern is waived.
|
||||
if ctx.property_read_side_effects() == PropertyReadSideEffects::None {
|
||||
return false;
|
||||
}
|
||||
// Otherwise pure only when the target is a determined, non-Proxy value (a
|
||||
// literal object/array, or a primitive `ToObject` wraps without user code).
|
||||
// An undetermined value could be a Proxy whose trap is observable.
|
||||
value_type.is_undetermined()
|
||||
}
|
||||
// `Object.create(proto)` allocates an object with prototype `proto`; it runs
|
||||
// no user code and is pure when `proto` is provably an object or `null`
|
||||
// (otherwise it throws a TypeError) and there is no `properties` argument
|
||||
// (which would be read via `[[OwnPropertyKeys]]`/`[[Get]]`).
|
||||
"create" => {
|
||||
if self.arguments.iter().any(|e| e.may_have_side_effects(ctx)) {
|
||||
return true;
|
||||
}
|
||||
if self.arguments.len() == 1
|
||||
&& let Some(arg) = self.arguments[0].as_expression()
|
||||
&& matches!(arg.value_type(ctx), ValueType::Object | ValueType::Null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -400,8 +400,11 @@ pub(super) fn is_pure_global_method_call(object: &str, method: &str) -> bool {
|
||||
"Date" => matches!(method, "now" | "parse" | "UTC"),
|
||||
"Math" => is_pure_math_method(method),
|
||||
"Number" => matches!(method, "isFinite" | "isInteger" | "isNaN" | "isSafeInteger" | "parseFloat" | "parseInt"),
|
||||
"Object" => matches!(method, "create" | "getOwnPropertyDescriptor" | "getOwnPropertyDescriptors" | "getOwnPropertyNames"
|
||||
| "getOwnPropertySymbols" | "getPrototypeOf" | "hasOwn" | "is" | "isExtensible" | "isFrozen" | "isSealed" | "keys"),
|
||||
// Only `Object.is` is unconditionally pure. The introspection methods
|
||||
// (`keys`, `getOwnPropertyDescriptor`, ...) can trigger Proxy traps on their
|
||||
// target, and `Object.create` reads its `properties` argument; both are
|
||||
// handled in `CallExpression::may_have_side_effects`.
|
||||
"Object" => method == "is",
|
||||
"String" => matches!(method, "fromCharCode" | "fromCodePoint" | "raw"),
|
||||
"Symbol" => matches!(method, "for" | "keyFor"),
|
||||
"URL" => method == "canParse",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use oxc_ast::ast::*;
|
||||
|
||||
use crate::constant_evaluation::{DetermineValueType, ValueType};
|
||||
use crate::constant_evaluation::DetermineValueType;
|
||||
|
||||
use super::{MayHaveSideEffects, PropertyReadSideEffects, context::MayHaveSideEffectsContext};
|
||||
|
||||
@@ -108,8 +108,7 @@ impl<'a> MayHaveSideEffects<'a> for VariableDeclaration<'a> {
|
||||
if self.kind == VariableDeclarationKind::Using {
|
||||
return self.declarations.iter().any(|decl| {
|
||||
decl.init.as_ref().is_none_or(|init| {
|
||||
!matches!(init.value_type(ctx), ValueType::Undefined | ValueType::Null)
|
||||
|| init.may_have_side_effects(ctx)
|
||||
!init.value_type(ctx).is_null_or_undefined() || init.may_have_side_effects(ctx)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1177,18 +1177,20 @@ fn test_call_expressions() {
|
||||
test("Number.parseFloat()", false);
|
||||
test("Number.parseInt()", false);
|
||||
|
||||
test("Object.create()", false);
|
||||
test("Object.getOwnPropertyDescriptor()", false);
|
||||
test("Object.getOwnPropertyDescriptors()", false);
|
||||
test("Object.getOwnPropertyNames()", false);
|
||||
test("Object.getOwnPropertySymbols()", false);
|
||||
test("Object.getPrototypeOf()", false);
|
||||
test("Object.hasOwn()", false);
|
||||
// These throw on the `undefined` receiver (`ToObject(undefined)`) or are
|
||||
// conservatively kept; `Object.is()` is `Object.is(undefined, undefined)` -> true.
|
||||
test("Object.create()", true);
|
||||
test("Object.getOwnPropertyDescriptor()", true);
|
||||
test("Object.getOwnPropertyDescriptors()", true);
|
||||
test("Object.getOwnPropertyNames()", true);
|
||||
test("Object.getOwnPropertySymbols()", true);
|
||||
test("Object.getPrototypeOf()", true);
|
||||
test("Object.hasOwn()", true);
|
||||
test("Object.is()", false);
|
||||
test("Object.isExtensible()", false);
|
||||
test("Object.isFrozen()", false);
|
||||
test("Object.isSealed()", false);
|
||||
test("Object.keys()", false);
|
||||
test("Object.isExtensible()", true);
|
||||
test("Object.isFrozen()", true);
|
||||
test("Object.isSealed()", true);
|
||||
test("Object.keys()", true);
|
||||
|
||||
test("String.fromCharCode()", false);
|
||||
test("String.fromCodePoint()", false);
|
||||
@@ -1216,6 +1218,60 @@ fn test_call_expressions() {
|
||||
test_with_global_variables("Object.create()", &[], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_sensitive_object_methods() {
|
||||
// Pure with a determined, non-Proxy, non-null target (a literal cannot be a Proxy
|
||||
// and these methods never `[[Get]]`, so a literal getter is not invoked).
|
||||
test("Object.keys({})", false);
|
||||
test("Object.keys([])", false);
|
||||
test("Object.keys(42)", false);
|
||||
test("Object.keys('s')", false);
|
||||
test("Object.keys(true)", false);
|
||||
test("Object.keys(10n)", false);
|
||||
test("Object.keys({ get a() { f() } })", false);
|
||||
test("Object[\"keys\"]({})", false);
|
||||
test("Object.getOwnPropertyDescriptor({}, 'x')", false);
|
||||
test("Object.getOwnPropertyDescriptors({})", false);
|
||||
test("Object.getOwnPropertyNames({})", false);
|
||||
test("Object.getOwnPropertySymbols({})", false);
|
||||
test("Object.getPrototypeOf({})", false);
|
||||
test("Object.hasOwn({}, 'x')", false);
|
||||
test("Object.isExtensible({})", false);
|
||||
test("Object.isFrozen({})", false);
|
||||
test("Object.isSealed({})", false);
|
||||
|
||||
// Kept when the target could be a Proxy (undetermined) or would throw (null/undefined).
|
||||
test("Object.keys(x)", true);
|
||||
test("Object.keys(new Proxy({}, {}))", true);
|
||||
test("Object.keys(null)", true);
|
||||
test("Object.keys(undefined)", true);
|
||||
test("Object.keys(...x)", true);
|
||||
test("Object.getOwnPropertyDescriptor(x, 'x')", true);
|
||||
test("Object.getPrototypeOf(x)", true);
|
||||
test("Object.hasOwn(x, 'x')", true);
|
||||
test("Object[\"keys\"](x)", true);
|
||||
|
||||
// `Object.values`/`entries` invoke `[[Get]]` (run getters), so they are always kept.
|
||||
test("Object.values({})", true);
|
||||
test("Object.entries({})", true);
|
||||
test("Object.values(x)", true);
|
||||
test("Object.values({ get a() { f() } })", true);
|
||||
|
||||
// `Object.is` is unconditionally pure (never introspects its arguments).
|
||||
test("Object.is(1, 2)", false);
|
||||
test("Object.is(f(), 2)", true);
|
||||
|
||||
// `Object.create(proto)` is pure with an object/null prototype and no properties.
|
||||
test("Object.create({})", false);
|
||||
test("Object.create([])", false);
|
||||
test("Object.create(null)", false);
|
||||
test("Object.create(x)", true); // proto could be a non-object -> throws
|
||||
test("Object.create(42)", true); // primitive proto -> throws
|
||||
test("Object.create(undefined)", true); // throws
|
||||
test("Object.create({}, x)", true); // properties read via [[OwnPropertyKeys]]/[[Get]]
|
||||
test("Object.create({}, {})", true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_like_expressions() {
|
||||
test("foo()", true);
|
||||
|
||||
Reference in new issue
Block a user