boa_engine/builtins/proxy/mod.rs
1//! Boa's implementation of ECMAScript's global `Proxy` object.
2//!
3//! The `Proxy` object enables you to create a proxy for another object,
4//! which can intercept and redefine fundamental operations for that object.
5//!
6//! More information:
7//! - [ECMAScript reference][spec]
8//! - [MDN documentation][mdn]
9//!
10//! [spec]: https://tc39.es/ecma262/#sec-proxy-objects
11//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
12
13use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject, OrdinaryObject};
14use crate::JsExpect;
15use crate::object::internal_methods::InternalMethodCallContext;
16use crate::value::JsVariant;
17use crate::{
18 Context, JsArgs, JsResult, JsString, JsValue,
19 builtins::{BuiltInObject, array},
20 context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
21 error::JsNativeError,
22 js_string,
23 native_function::NativeFunction,
24 object::{
25 JsData, JsFunction, JsObject, JsPrototype,
26 internal_methods::{
27 CallValue, InternalMethodPropertyContext, InternalObjectMethods,
28 ORDINARY_INTERNAL_METHODS, is_compatible_property_descriptor,
29 },
30 shape::slot::SlotAttributes,
31 },
32 property::{PropertyDescriptor, PropertyKey},
33 realm::Realm,
34 string::StaticJsStrings,
35 value::Type,
36};
37use boa_gc::{Finalize, GcRefCell, Trace};
38use rustc_hash::FxHashSet;
39/// Javascript `Proxy` object.
40#[derive(Debug, Clone, Trace, Finalize)]
41pub struct Proxy {
42 // (target, handler)
43 data: Option<(JsObject, JsObject)>,
44}
45
46impl JsData for Proxy {
47 fn internal_methods(&self) -> &'static InternalObjectMethods {
48 static BASIC: InternalObjectMethods = InternalObjectMethods {
49 __get_prototype_of__: proxy_exotic_get_prototype_of,
50 __set_prototype_of__: proxy_exotic_set_prototype_of,
51 __is_extensible__: proxy_exotic_is_extensible,
52 __prevent_extensions__: proxy_exotic_prevent_extensions,
53 __get_own_property__: proxy_exotic_get_own_property,
54 __define_own_property__: proxy_exotic_define_own_property,
55 __has_property__: proxy_exotic_has_property,
56 __try_get__: proxy_exotic_try_get,
57 __get__: proxy_exotic_get,
58 __set__: proxy_exotic_set,
59 __delete__: proxy_exotic_delete,
60 __own_property_keys__: proxy_exotic_own_property_keys,
61 ..ORDINARY_INTERNAL_METHODS
62 };
63
64 static CALLABLE: InternalObjectMethods = InternalObjectMethods {
65 __call__: proxy_exotic_call,
66 ..BASIC
67 };
68
69 static CONSTRUCTOR: InternalObjectMethods = InternalObjectMethods {
70 __call__: proxy_exotic_call,
71 __construct__: proxy_exotic_construct,
72 ..BASIC
73 };
74
75 let Some(data) = &self.data else {
76 return &BASIC;
77 };
78
79 if data.0.is_constructor() {
80 &CONSTRUCTOR
81 } else if data.0.is_callable() {
82 &CALLABLE
83 } else {
84 &BASIC
85 }
86 }
87}
88
89impl IntrinsicObject for Proxy {
90 fn init(realm: &Realm) {
91 BuiltInBuilder::from_standard_constructor::<Self>(realm)
92 .static_method(Self::revocable, js_string!("revocable"), 2)
93 .build_without_prototype();
94 }
95
96 fn get(intrinsics: &Intrinsics) -> JsObject {
97 Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
98 }
99}
100
101impl BuiltInObject for Proxy {
102 const NAME: JsString = StaticJsStrings::PROXY;
103}
104
105impl BuiltInConstructor for Proxy {
106 const CONSTRUCTOR_ARGUMENTS: usize = 2;
107 const PROTOTYPE_STORAGE_SLOTS: usize = 0;
108 const CONSTRUCTOR_STORAGE_SLOTS: usize = 1;
109
110 const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
111 StandardConstructors::proxy;
112
113 /// `28.2.1.1 Proxy ( target, handler )`
114 ///
115 /// More information:
116 /// - [ECMAScript reference][spec]
117 ///
118 /// [spec]: https://tc39.es/ecma262/#sec-proxy-target-handler
119 fn constructor(
120 new_target: &JsValue,
121 args: &[JsValue],
122 context: &mut Context,
123 ) -> JsResult<JsValue> {
124 // 1. If NewTarget is undefined, throw a TypeError exception.
125 if new_target.is_undefined() {
126 return Err(JsNativeError::typ()
127 .with_message("Proxy constructor called on undefined new target")
128 .into());
129 }
130
131 // 2. Return ? ProxyCreate(target, handler).
132 Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context).map(JsValue::from)
133 }
134}
135
136impl Proxy {
137 pub(crate) fn new(target: JsObject, handler: JsObject) -> Self {
138 Self {
139 data: Some((target, handler)),
140 }
141 }
142
143 /// This is an internal method only built for usage in the proxy internal methods.
144 ///
145 /// It returns the (target, handler) of the proxy.
146 pub(crate) fn try_data(&self) -> JsResult<(JsObject, JsObject)> {
147 self.data.clone().ok_or_else(|| {
148 JsNativeError::typ()
149 .with_message("Proxy object has empty handler and target")
150 .into()
151 })
152 }
153
154 // `10.5.14 ProxyCreate ( target, handler )`
155 //
156 // More information:
157 // - [ECMAScript reference][spec]
158 //
159 // [spec]: https://tc39.es/ecma262/#sec-proxycreate
160 pub(crate) fn create(
161 target: &JsValue,
162 handler: &JsValue,
163 context: &mut Context,
164 ) -> JsResult<JsObject> {
165 // 1. If Type(target) is not Object, throw a TypeError exception.
166 let target = target.as_object().ok_or_else(|| {
167 JsNativeError::typ().with_message("Proxy constructor called with non-object target")
168 })?;
169
170 // 2. If Type(handler) is not Object, throw a TypeError exception.
171 let handler = handler.as_object().ok_or_else(|| {
172 JsNativeError::typ().with_message("Proxy constructor called with non-object handler")
173 })?;
174
175 // 3. Let P be ! MakeBasicObject(« [[ProxyHandler]], [[ProxyTarget]] »).
176 // 4. Set P's essential internal methods, except for [[Call]] and [[Construct]], to the definitions specified in 10.5.
177 // 5. If IsCallable(target) is true, then
178 // a. Set P.[[Call]] as specified in 10.5.12.
179 // b. If IsConstructor(target) is true, then
180 // i. Set P.[[Construct]] as specified in 10.5.13.
181 // 6. Set P.[[ProxyTarget]] to target.
182 // 7. Set P.[[ProxyHandler]] to handler.
183 let p = JsObject::from_proto_and_data_with_shared_shape(
184 context.root_shape(),
185 context.intrinsics().constructors().object().prototype(),
186 Self::new(target.clone(), handler.clone()),
187 )
188 .upcast();
189
190 // 8. Return P.
191 Ok(p)
192 }
193
194 pub(crate) fn revoker(proxy: JsObject, context: &mut Context) -> JsFunction {
195 // 3. Let revoker be ! CreateBuiltinFunction(revokerClosure, 0, "", « [[RevocableProxy]] »).
196 // 4. Set revoker.[[RevocableProxy]] to p.
197
198 NativeFunction::from_copy_closure_with_captures(
199 |_, _, revocable_proxy, _| {
200 // a. Let F be the active function object.
201 // b. Let p be F.[[RevocableProxy]].
202 // d. Set F.[[RevocableProxy]] to null.
203 if let Some(p) = std::mem::take(&mut *revocable_proxy.borrow_mut()) {
204 // e. Assert: p is a Proxy object.
205 // f. Set p.[[ProxyTarget]] to null.
206 // g. Set p.[[ProxyHandler]] to null.
207 p.downcast_mut::<Proxy>()
208 .js_expect("[[RevocableProxy]] must be a proxy object")?
209 .data = None;
210 }
211
212 // c. If p is null, return undefined.
213 // h. Return undefined.
214 Ok(JsValue::undefined())
215 },
216 GcRefCell::new(Some(proxy)),
217 )
218 .to_js_function(context.realm())
219 }
220
221 /// `28.2.2.1 Proxy.revocable ( target, handler )`
222 ///
223 /// More information:
224 /// - [ECMAScript reference][spec]
225 ///
226 /// [spec]: https://tc39.es/ecma262/#sec-proxy.revocable
227 fn revocable(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
228 // 1. Let p be ? ProxyCreate(target, handler).
229 let p = Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context)?;
230
231 // Revoker creation steps on `Proxy::revoker`
232 let revoker = Self::revoker(p.clone(), context);
233
234 // 5. Let result be ! OrdinaryObjectCreate(%Object.prototype%).
235 let result = JsObject::with_object_proto(context.intrinsics());
236
237 // 6. Perform ! CreateDataPropertyOrThrow(result, "proxy", p).
238 result
239 .create_data_property_or_throw(js_string!("proxy"), p, context)
240 .js_expect("CreateDataPropertyOrThrow cannot fail here")?;
241
242 // 7. Perform ! CreateDataPropertyOrThrow(result, "revoke", revoker).
243 result
244 .create_data_property_or_throw(js_string!("revoke"), revoker, context)
245 .js_expect("CreateDataPropertyOrThrow cannot fail here")?;
246
247 // 8. Return result.
248 Ok(result.into())
249 }
250}
251
252/// `10.5.1 [[GetPrototypeOf]] ( )`
253///
254/// More information:
255/// - [ECMAScript reference][spec]
256///
257/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof
258pub(crate) fn proxy_exotic_get_prototype_of(
259 obj: &JsObject,
260 context: &mut Context,
261) -> JsResult<JsPrototype> {
262 // 1. Let handler be O.[[ProxyHandler]].
263 // 2. If handler is null, throw a TypeError exception.
264 // 3. Assert: Type(handler) is Object.
265 // 4. Let target be O.[[ProxyTarget]].
266 let (target, handler) = obj
267 .downcast_ref::<Proxy>()
268 .js_expect("Proxy object internal internal method called on non-proxy object")?
269 .try_data()?;
270
271 // 5. Let trap be ? GetMethod(handler, "getPrototypeOf").
272 let Some(trap) = handler.get_method(js_string!("getPrototypeOf"), context)? else {
273 // 6. If trap is undefined, then
274 // a. Return ? target.[[GetPrototypeOf]]().
275 return target.__get_prototype_of__(context);
276 };
277
278 // 7. Let handlerProto be ? Call(trap, handler, « target »).
279 let handler_proto = trap.call(&handler.into(), &[target.clone().into()], context)?;
280
281 // 8. If Type(handlerProto) is neither Object nor Null, throw a TypeError exception.
282 let handler_proto = match handler_proto.variant() {
283 JsVariant::Object(obj) => Some(obj.clone()),
284 JsVariant::Null => None,
285 _ => {
286 return Err(JsNativeError::typ()
287 .with_message("Proxy trap result is neither object nor null")
288 .into());
289 }
290 };
291
292 // 9. Let extensibleTarget be ? IsExtensible(target).
293 // 10. If extensibleTarget is true, return handlerProto.
294 if target.is_extensible(context)? {
295 return Ok(handler_proto);
296 }
297
298 // 11. Let targetProto be ? target.[[GetPrototypeOf]]().
299 let target_proto = target.__get_prototype_of__(context)?;
300
301 // 12. If SameValue(handlerProto, targetProto) is false, throw a TypeError exception.
302 if handler_proto != target_proto {
303 return Err(JsNativeError::typ()
304 .with_message("Proxy trap returned unexpected prototype")
305 .into());
306 }
307
308 // 13. Return handlerProto.
309 Ok(handler_proto)
310}
311
312/// `10.5.2 [[SetPrototypeOf]] ( V )`
313///
314/// More information:
315/// - [ECMAScript reference][spec]
316///
317/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-setprototypeof-v
318pub(crate) fn proxy_exotic_set_prototype_of(
319 obj: &JsObject,
320 val: JsPrototype,
321 context: &mut Context,
322) -> JsResult<bool> {
323 // 1. Let handler be O.[[ProxyHandler]].
324 // 2. If handler is null, throw a TypeError exception.
325 // 3. Assert: Type(handler) is Object.
326 // 4. Let target be O.[[ProxyTarget]].
327 let (target, handler) = obj
328 .downcast_ref::<Proxy>()
329 .js_expect("Proxy object internal internal method called on non-proxy object")?
330 .try_data()?;
331
332 // 5. Let trap be ? GetMethod(handler, "setPrototypeOf").
333 let Some(trap) = handler.get_method(js_string!("setPrototypeOf"), context)? else {
334 // 6. If trap is undefined, then
335 // a. Return ? target.[[SetPrototypeOf]](V).
336 return target.__set_prototype_of__(val, context);
337 };
338
339 // 7. Let booleanTrapResult be ! ToBoolean(? Call(trap, handler, « target, V »)).
340 // 8. If booleanTrapResult is false, return false.
341 if !trap
342 .call(
343 &handler.into(),
344 &[
345 target.clone().into(),
346 val.clone().map_or(JsValue::null(), Into::into),
347 ],
348 context,
349 )?
350 .to_boolean()
351 {
352 return Ok(false);
353 }
354
355 // 9. Let extensibleTarget be ? IsExtensible(target).
356 // 10. If extensibleTarget is true, return true.
357 if target.is_extensible(context)? {
358 return Ok(true);
359 }
360
361 // 11. Let targetProto be ? target.[[GetPrototypeOf]]().
362 let target_proto = target.__get_prototype_of__(context)?;
363
364 // 12. If SameValue(V, targetProto) is false, throw a TypeError exception.
365 if val != target_proto {
366 return Err(JsNativeError::typ()
367 .with_message("Proxy trap failed to set prototype")
368 .into());
369 }
370
371 // 13. Return true.
372 Ok(true)
373}
374
375/// `10.5.3 [[IsExtensible]] ( )`
376///
377/// More information:
378/// - [ECMAScript reference][spec]
379///
380/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-isextensible
381pub(crate) fn proxy_exotic_is_extensible(obj: &JsObject, context: &mut Context) -> JsResult<bool> {
382 // 1. Let handler be O.[[ProxyHandler]].
383 // 2. If handler is null, throw a TypeError exception.
384 // 3. Assert: Type(handler) is Object.
385 // 4. Let target be O.[[ProxyTarget]].
386 let (target, handler) = obj
387 .downcast_ref::<Proxy>()
388 .js_expect("Proxy object internal internal method called on non-proxy object")?
389 .try_data()?;
390
391 // 5. Let trap be ? GetMethod(handler, "isExtensible").
392 let Some(trap) = handler.get_method(js_string!("isExtensible"), context)? else {
393 // 6. If trap is undefined, then
394 // a. Return ? IsExtensible(target).
395 return target.is_extensible(context);
396 };
397
398 // 7. Let booleanTrapResult be ! ToBoolean(? Call(trap, handler, « target »)).
399 let boolean_trap_result = trap
400 .call(&handler.into(), &[target.clone().into()], context)?
401 .to_boolean();
402
403 // 8. Let targetResult be ? IsExtensible(target).
404 let target_result = target.is_extensible(context)?;
405
406 // 9. If SameValue(booleanTrapResult, targetResult) is false, throw a TypeError exception.
407 if boolean_trap_result != target_result {
408 return Err(JsNativeError::typ()
409 .with_message("Proxy trap returned unexpected extensible value")
410 .into());
411 }
412
413 // 10. Return booleanTrapResult.
414 Ok(boolean_trap_result)
415}
416
417/// `10.5.4 [[PreventExtensions]] ( )`
418///
419/// More information:
420/// - [ECMAScript reference][spec]
421///
422/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-preventextensions
423pub(crate) fn proxy_exotic_prevent_extensions(
424 obj: &JsObject,
425 context: &mut Context,
426) -> JsResult<bool> {
427 // 1. Let handler be O.[[ProxyHandler]].
428 // 2. If handler is null, throw a TypeError exception.
429 // 3. Assert: Type(handler) is Object.
430 // 4. Let target be O.[[ProxyTarget]].
431 let (target, handler) = obj
432 .downcast_ref::<Proxy>()
433 .js_expect("Proxy object internal internal method called on non-proxy object")?
434 .try_data()?;
435
436 // 5. Let trap be ? GetMethod(handler, "preventExtensions").
437 let Some(trap) = handler.get_method(js_string!("preventExtensions"), context)? else {
438 // 6. If trap is undefined, then
439 // a. Return ? target.[[PreventExtensions]]().
440 return target.__prevent_extensions__(context);
441 };
442
443 // 7. Let booleanTrapResult be ! ToBoolean(? Call(trap, handler, « target »)).
444 let boolean_trap_result = trap
445 .call(&handler.into(), &[target.clone().into()], context)?
446 .to_boolean();
447
448 // 8. If booleanTrapResult is true, then
449 if boolean_trap_result && target.is_extensible(context)? {
450 // a. Let extensibleTarget be ? IsExtensible(target).
451 // b. If extensibleTarget is true, throw a TypeError exception.
452 return Err(JsNativeError::typ()
453 .with_message("Proxy trap failed to set extensible")
454 .into());
455 }
456
457 // 9. Return booleanTrapResult.
458 Ok(boolean_trap_result)
459}
460
461/// `10.5.5 [[GetOwnProperty]] ( P )`
462///
463/// More information:
464/// - [ECMAScript reference][spec]
465///
466/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getownproperty-p
467pub(crate) fn proxy_exotic_get_own_property(
468 obj: &JsObject,
469 key: &PropertyKey,
470 context: &mut InternalMethodPropertyContext<'_>,
471) -> JsResult<Option<PropertyDescriptor>> {
472 context.slot().attributes |= SlotAttributes::NOT_CACHEABLE;
473
474 // 1. Let handler be O.[[ProxyHandler]].
475 // 2. If handler is null, throw a TypeError exception.
476 // 3. Assert: Type(handler) is Object.
477 // 4. Let target be O.[[ProxyTarget]].
478 let (target, handler) = obj
479 .downcast_ref::<Proxy>()
480 .js_expect("Proxy object internal internal method called on non-proxy object")?
481 .try_data()?;
482
483 // 5. Let trap be ? GetMethod(handler, "getOwnPropertyDescriptor").
484 let Some(trap) = handler.get_method(js_string!("getOwnPropertyDescriptor"), context)? else {
485 // 6. If trap is undefined, then
486 // a. Return ? target.[[GetOwnProperty]](P).
487 return target.__get_own_property__(key, context);
488 };
489
490 // 7. Let trapResultObj be ? Call(trap, handler, « target, P »).
491 let trap_result_obj = trap.call(
492 &handler.into(),
493 &[target.clone().into(), key.clone().into()],
494 context,
495 )?;
496
497 // 8. If Type(trapResultObj) is neither Object nor Undefined, throw a TypeError exception.
498 if !trap_result_obj.is_object() && !trap_result_obj.is_undefined() {
499 return Err(JsNativeError::typ()
500 .with_message("Proxy trap result is neither object nor undefined")
501 .into());
502 }
503
504 // 9. Let targetDesc be ? target.[[GetOwnProperty]](P).
505 let target_desc = target.__get_own_property__(key, context)?;
506
507 // 10. If trapResultObj is undefined, then
508 if trap_result_obj.is_undefined() {
509 if let Some(desc) = target_desc {
510 // b. If targetDesc.[[Configurable]] is false, throw a TypeError exception.
511 if !desc.expect_configurable() {
512 return Err(JsNativeError::typ()
513 .with_message(
514 "Proxy trap result is undefined and target result is not configurable",
515 )
516 .into());
517 }
518
519 // c. Let extensibleTarget be ? IsExtensible(target).
520 // d. If extensibleTarget is false, throw a TypeError exception.
521 if !target.is_extensible(context)? {
522 return Err(JsNativeError::typ()
523 .with_message("Proxy trap result is undefined and target is not extensible")
524 .into());
525 }
526 // e. Return undefined.
527 return Ok(None);
528 }
529
530 // a. If targetDesc is undefined, return undefined.
531 return Ok(None);
532 }
533
534 // 11. Let extensibleTarget be ? IsExtensible(target).
535 let extensible_target = target.is_extensible(context)?;
536
537 // 12. Let resultDesc be ? ToPropertyDescriptor(trapResultObj).
538 let result_desc = trap_result_obj.to_property_descriptor(context)?;
539
540 // 13. Call CompletePropertyDescriptor(resultDesc).
541 let result_desc = result_desc.complete_property_descriptor();
542
543 // 14. Let valid be IsCompatiblePropertyDescriptor(extensibleTarget, resultDesc, targetDesc).
544 // 15. If valid is false, throw a TypeError exception.
545 if !is_compatible_property_descriptor(
546 extensible_target,
547 result_desc.clone(),
548 target_desc.clone(),
549 ) {
550 return Err(JsNativeError::typ()
551 .with_message("Proxy trap returned unexpected property")
552 .into());
553 }
554
555 // 16. If resultDesc.[[Configurable]] is false, then
556 if !result_desc.expect_configurable() {
557 // a. If targetDesc is undefined or targetDesc.[[Configurable]] is true, then
558 match &target_desc {
559 Some(desc) if !desc.expect_configurable() => {
560 // b. If resultDesc has a [[Writable]] field and resultDesc.[[Writable]] is false, then
561 if result_desc.writable() == Some(false) {
562 // i. If targetDesc.[[Writable]] is true, throw a TypeError exception.
563 if desc.expect_writable() {
564 return
565 Err(JsNativeError::typ().with_message("Proxy trap result is writable and not configurable while target result is not configurable").into())
566 ;
567 }
568 }
569 }
570 // i. Throw a TypeError exception.
571 _ => {
572 return Err(JsNativeError::typ()
573 .with_message(
574 "Proxy trap result is not configurable and target result is undefined",
575 )
576 .into());
577 }
578 }
579 }
580
581 // 17. Return resultDesc.
582 Ok(Some(result_desc))
583}
584
585/// `10.5.6 [[DefineOwnProperty]] ( P, Desc )`
586///
587/// More information:
588/// - [ECMAScript reference][spec]
589///
590/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc
591pub(crate) fn proxy_exotic_define_own_property(
592 obj: &JsObject,
593 key: &PropertyKey,
594 desc: PropertyDescriptor,
595 context: &mut InternalMethodPropertyContext<'_>,
596) -> JsResult<bool> {
597 context.slot().attributes |= SlotAttributes::NOT_CACHEABLE;
598
599 // 1. Let handler be O.[[ProxyHandler]].
600 // 2. If handler is null, throw a TypeError exception.
601 // 3. Assert: Type(handler) is Object.
602 // 4. Let target be O.[[ProxyTarget]].
603 let (target, handler) = obj
604 .downcast_ref::<Proxy>()
605 .js_expect("Proxy object internal internal method called on non-proxy object")?
606 .try_data()?;
607
608 // 5. Let trap be ? GetMethod(handler, "defineProperty").
609 let Some(trap) = handler.get_method(js_string!("defineProperty"), context)? else {
610 // 6. If trap is undefined, then
611 // a. Return ? target.[[DefineOwnProperty]](P, Desc).
612 return target.__define_own_property__(key, desc, context);
613 };
614
615 // 7. Let descObj be FromPropertyDescriptor(Desc).
616 let desc_obj = OrdinaryObject::from_property_descriptor(Some(desc.clone()), context)?;
617
618 // 8. Let booleanTrapResult be ! ToBoolean(? Call(trap, handler, « target, P, descObj »)).
619 // 9. If booleanTrapResult is false, return false.
620 if !trap
621 .call(
622 &handler.into(),
623 &[target.clone().into(), key.clone().into(), desc_obj],
624 context,
625 )?
626 .to_boolean()
627 {
628 return Ok(false);
629 }
630
631 // 10. Let targetDesc be ? target.[[GetOwnProperty]](P).
632 let target_desc = target.__get_own_property__(key, context)?;
633
634 // 11. Let extensibleTarget be ? IsExtensible(target).
635 let extensible_target = target.is_extensible(context)?;
636
637 // 12. If Desc has a [[Configurable]] field and if Desc.[[Configurable]] is false, then
638 let setting_config_false = matches!(desc.configurable(), Some(false));
639
640 match target_desc {
641 // 14. If targetDesc is undefined, then
642 None => {
643 // a. If extensibleTarget is false, throw a TypeError exception.
644 if !extensible_target {
645 return Err(JsNativeError::typ()
646 .with_message("Proxy trap failed to set property")
647 .into());
648 }
649
650 // b. If settingConfigFalse is true, throw a TypeError exception.
651 if setting_config_false {
652 return Err(JsNativeError::typ()
653 .with_message("Proxy trap failed to set property")
654 .into());
655 }
656 }
657 // 15. Else,
658 Some(target_desc) => {
659 // a. If IsCompatiblePropertyDescriptor(extensibleTarget, Desc, targetDesc) is false, throw a TypeError exception.
660 if !is_compatible_property_descriptor(
661 extensible_target,
662 desc.clone(),
663 Some(target_desc.clone()),
664 ) {
665 return Err(JsNativeError::typ()
666 .with_message("Proxy trap set property to unexpected value")
667 .into());
668 }
669
670 // b. If settingConfigFalse is true and targetDesc.[[Configurable]] is true, throw a TypeError exception.
671 if setting_config_false && target_desc.expect_configurable() {
672 return Err(JsNativeError::typ()
673 .with_message("Proxy trap set property with unexpected configurable field")
674 .into());
675 }
676
677 // c. If IsDataDescriptor(targetDesc) is true, targetDesc.[[Configurable]] is false, and targetDesc.[[Writable]] is true, then
678 if target_desc.is_data_descriptor()
679 && !target_desc.expect_configurable()
680 && target_desc.expect_writable()
681 {
682 // i. If Desc has a [[Writable]] field and Desc.[[Writable]] is false, throw a TypeError exception.
683 if let Some(writable) = desc.writable()
684 && !writable
685 {
686 return Err(JsNativeError::typ()
687 .with_message("Proxy trap set property with unexpected writable field")
688 .into());
689 }
690 }
691 }
692 }
693
694 // 16. Return true.
695 Ok(true)
696}
697
698/// `10.5.7 [[HasProperty]] ( P )`
699///
700/// More information:
701/// - [ECMAScript reference][spec]
702///
703/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-hasproperty-p
704pub(crate) fn proxy_exotic_has_property(
705 obj: &JsObject,
706 key: &PropertyKey,
707 context: &mut InternalMethodPropertyContext<'_>,
708) -> JsResult<bool> {
709 context.slot().attributes |= SlotAttributes::NOT_CACHEABLE;
710
711 // 1. Let handler be O.[[ProxyHandler]].
712 // 2. If handler is null, throw a TypeError exception.
713 // 3. Assert: Type(handler) is Object.
714 // 4. Let target be O.[[ProxyTarget]].
715 let (target, handler) = obj
716 .downcast_ref::<Proxy>()
717 .js_expect("Proxy object internal internal method called on non-proxy object")?
718 .try_data()?;
719
720 // 5. Let trap be ? GetMethod(handler, "has").
721 let Some(trap) = handler.get_method(js_string!("has"), context)? else {
722 // 6. If trap is undefined, then
723 // a. Return ? target.[[HasProperty]](P).
724 return target.has_property(key.clone(), context);
725 };
726
727 // 7. Let booleanTrapResult be ! ToBoolean(? Call(trap, handler, « target, P »)).
728 let boolean_trap_result = trap
729 .call(
730 &handler.into(),
731 &[target.clone().into(), key.clone().into()],
732 context,
733 )?
734 .to_boolean();
735
736 // 8. If booleanTrapResult is false, then
737 if !boolean_trap_result {
738 // a. Let targetDesc be ? target.[[GetOwnProperty]](P).
739 let target_desc = target.__get_own_property__(key, context)?;
740
741 // b. If targetDesc is not undefined, then
742 if let Some(target_desc) = target_desc {
743 // i. If targetDesc.[[Configurable]] is false, throw a TypeError exception.
744 if !target_desc.expect_configurable() {
745 return Err(JsNativeError::typ()
746 .with_message("Proxy trap returned unexpected property")
747 .into());
748 }
749
750 // ii. Let extensibleTarget be ? IsExtensible(target).
751 // iii. If extensibleTarget is false, throw a TypeError exception.
752 if !target.is_extensible(context)? {
753 return Err(JsNativeError::typ()
754 .with_message("Proxy trap returned unexpected property")
755 .into());
756 }
757 }
758 }
759
760 // 9. Return booleanTrapResult.
761 Ok(boolean_trap_result)
762}
763
764/// Internal optimization method for `Proxy` exotic objects.
765///
766/// This method combines the internal methods `[[HasProperty]]` and `[[Get]]`.
767///
768/// More information:
769/// - [ECMAScript reference HasProperty][spec0]
770/// - [ECMAScript reference Get][spec1]
771///
772/// [spec0]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-hasproperty-p
773/// [spec1]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver
774pub(crate) fn proxy_exotic_try_get(
775 obj: &JsObject,
776 key: &PropertyKey,
777 receiver: JsValue,
778 context: &mut InternalMethodPropertyContext<'_>,
779) -> JsResult<Option<JsValue>> {
780 // Note: For now, this just calls the normal methods. Could be optimized further.
781 if proxy_exotic_has_property(obj, key, context)? {
782 Ok(Some(proxy_exotic_get(obj, key, receiver, context)?))
783 } else {
784 Ok(None)
785 }
786}
787
788/// `10.5.8 [[Get]] ( P, Receiver )`
789///
790/// More information:
791/// - [ECMAScript reference][spec]
792///
793/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver
794pub(crate) fn proxy_exotic_get(
795 obj: &JsObject,
796 key: &PropertyKey,
797 receiver: JsValue,
798 context: &mut InternalMethodPropertyContext<'_>,
799) -> JsResult<JsValue> {
800 // Proxy object can't be cached.
801 context.slot().attributes |= SlotAttributes::NOT_CACHEABLE;
802
803 // 1. Let handler be O.[[ProxyHandler]].
804 // 2. If handler is null, throw a TypeError exception.
805 // 3. Assert: Type(handler) is Object.
806 // 4. Let target be O.[[ProxyTarget]].
807 let (target, handler) = obj
808 .downcast_ref::<Proxy>()
809 .js_expect("Proxy object internal internal method called on non-proxy object")?
810 .try_data()?;
811
812 // 5. Let trap be ? GetMethod(handler, "get").
813 let Some(trap) = handler.get_method(js_string!("get"), context)? else {
814 // 6. If trap is undefined, then
815 // a. Return ? target.[[Get]](P, Receiver).
816 return target.__get__(key, receiver, context);
817 };
818
819 // 7. Let trapResult be ? Call(trap, handler, « target, P, Receiver »).
820 let trap_result = trap.call(
821 &handler.into(),
822 &[target.clone().into(), key.clone().into(), receiver],
823 context,
824 )?;
825
826 // 8. Let targetDesc be ? target.[[GetOwnProperty]](P).
827 let target_desc = target.__get_own_property__(key, context)?;
828
829 // 9. If targetDesc is not undefined and targetDesc.[[Configurable]] is false, then
830 if let Some(target_desc) = target_desc
831 && !target_desc.expect_configurable()
832 {
833 // a. If IsDataDescriptor(targetDesc) is true and targetDesc.[[Writable]] is false, then
834 if target_desc.is_data_descriptor() && !target_desc.expect_writable() {
835 // i. If SameValue(trapResult, targetDesc.[[Value]]) is false, throw a TypeError exception.
836 if !JsValue::same_value(&trap_result, target_desc.expect_value()) {
837 return Err(JsNativeError::typ()
838 .with_message("Proxy trap returned unexpected data descriptor")
839 .into());
840 }
841 }
842
843 // b. If IsAccessorDescriptor(targetDesc) is true and targetDesc.[[Get]] is undefined, then
844 if target_desc.is_accessor_descriptor() && target_desc.expect_get().is_undefined() {
845 // i. If trapResult is not undefined, throw a TypeError exception.
846 if !trap_result.is_undefined() {
847 return Err(JsNativeError::typ()
848 .with_message("Proxy trap returned unexpected accessor descriptor")
849 .into());
850 }
851 }
852 }
853
854 // 10. Return trapResult.
855 Ok(trap_result)
856}
857
858/// `10.5.9 [[Set]] ( P, V, Receiver )`
859///
860/// More information:
861/// - [ECMAScript reference][spec]
862///
863/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver
864pub(crate) fn proxy_exotic_set(
865 obj: &JsObject,
866 key: PropertyKey,
867 value: JsValue,
868 receiver: JsValue,
869 context: &mut InternalMethodPropertyContext<'_>,
870) -> JsResult<bool> {
871 context.slot().attributes |= SlotAttributes::NOT_CACHEABLE;
872
873 // 1. Let handler be O.[[ProxyHandler]].
874 // 2. If handler is null, throw a TypeError exception.
875 // 3. Assert: Type(handler) is Object.
876 // 4. Let target be O.[[ProxyTarget]].
877 let (target, handler) = obj
878 .downcast_ref::<Proxy>()
879 .js_expect("Proxy object internal internal method called on non-proxy object")?
880 .try_data()?;
881
882 // 5. Let trap be ? GetMethod(handler, "set").
883 let Some(trap) = handler.get_method(js_string!("set"), context)? else {
884 // 6. If trap is undefined, then
885 // a. Return ? target.[[Set]](P, V, Receiver).
886 return target.__set__(key, value, receiver, context);
887 };
888
889 // 7. Let booleanTrapResult be ! ToBoolean(? Call(trap, handler, « target, P, V, Receiver »)).
890 // 8. If booleanTrapResult is false, return false.
891 if !trap
892 .call(
893 &handler.into(),
894 &[
895 target.clone().into(),
896 key.clone().into(),
897 value.clone(),
898 receiver,
899 ],
900 context,
901 )?
902 .to_boolean()
903 {
904 return Ok(false);
905 }
906
907 // 9. Let targetDesc be ? target.[[GetOwnProperty]](P).
908 let target_desc = target.__get_own_property__(&key, context)?;
909
910 // 10. If targetDesc is not undefined and targetDesc.[[Configurable]] is false, then
911 if let Some(target_desc) = target_desc
912 && !target_desc.expect_configurable()
913 {
914 // a. If IsDataDescriptor(targetDesc) is true and targetDesc.[[Writable]] is false, then
915 if target_desc.is_data_descriptor() && !target_desc.expect_writable() {
916 // i. If SameValue(V, targetDesc.[[Value]]) is false, throw a TypeError exception.
917 if !JsValue::same_value(&value, target_desc.expect_value()) {
918 return Err(JsNativeError::typ()
919 .with_message("Proxy trap set unexpected data descriptor")
920 .into());
921 }
922 }
923
924 // b. If IsAccessorDescriptor(targetDesc) is true, then
925 if target_desc.is_accessor_descriptor() {
926 // i. If targetDesc.[[Set]] is undefined, throw a TypeError exception.
927 match target_desc.set().map(JsValue::is_undefined) {
928 None | Some(true) => {
929 return Err(JsNativeError::typ()
930 .with_message("Proxy trap set unexpected accessor descriptor")
931 .into());
932 }
933 _ => {}
934 }
935 }
936 }
937
938 // 11. Return true.
939 Ok(true)
940}
941
942/// `10.5.10 [[Delete]] ( P )`
943///
944/// More information:
945/// - [ECMAScript reference][spec]
946///
947/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-delete-p
948pub(crate) fn proxy_exotic_delete(
949 obj: &JsObject,
950 key: &PropertyKey,
951 context: &mut InternalMethodPropertyContext<'_>,
952) -> JsResult<bool> {
953 // 1. Let handler be O.[[ProxyHandler]].
954 // 2. If handler is null, throw a TypeError exception.
955 // 3. Assert: Type(handler) is Object.
956 // 4. Let target be O.[[ProxyTarget]].
957 let (target, handler) = obj
958 .downcast_ref::<Proxy>()
959 .js_expect("Proxy object internal internal method called on non-proxy object")?
960 .try_data()?;
961
962 // 5. Let trap be ? GetMethod(handler, "deleteProperty").
963 let Some(trap) = handler.get_method(js_string!("deleteProperty"), context)? else {
964 // 6. If trap is undefined, then
965 // a. Return ? target.[[Delete]](P).
966 return target.__delete__(key, context);
967 };
968
969 // 7. Let booleanTrapResult be ! ToBoolean(? Call(trap, handler, « target, P »)).
970 // 8. If booleanTrapResult is false, return false.
971 if !trap
972 .call(
973 &handler.into(),
974 &[target.clone().into(), key.clone().into()],
975 context,
976 )?
977 .to_boolean()
978 {
979 return Ok(false);
980 }
981
982 // 9. Let targetDesc be ? target.[[GetOwnProperty]](P).
983 match target.__get_own_property__(key, context)? {
984 // 10. If targetDesc is undefined, return true.
985 None => return Ok(true),
986 // 11. If targetDesc.[[Configurable]] is false, throw a TypeError exception.
987 Some(target_desc) => {
988 if !target_desc.expect_configurable() {
989 return Err(JsNativeError::typ()
990 .with_message("Proxy trap failed to delete property")
991 .into());
992 }
993 }
994 }
995
996 // 12. Let extensibleTarget be ? IsExtensible(target).
997 // 13. If extensibleTarget is false, throw a TypeError exception.
998 if !target.is_extensible(context)? {
999 return Err(JsNativeError::typ()
1000 .with_message("Proxy trap failed to delete property")
1001 .into());
1002 }
1003
1004 // 14. Return true.
1005 Ok(true)
1006}
1007
1008/// `10.5.11 [[OwnPropertyKeys]] ( )`
1009///
1010/// More information:
1011/// - [ECMAScript reference][spec]
1012///
1013/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-ownpropertykeys
1014pub(crate) fn proxy_exotic_own_property_keys(
1015 obj: &JsObject,
1016 context: &mut Context,
1017) -> JsResult<Vec<PropertyKey>> {
1018 // 1. Let handler be O.[[ProxyHandler]].
1019 // 2. If handler is null, throw a TypeError exception.
1020 // 3. Assert: Type(handler) is Object.
1021 // 4. Let target be O.[[ProxyTarget]].
1022 let (target, handler) = obj
1023 .downcast_ref::<Proxy>()
1024 .js_expect("Proxy object internal internal method called on non-proxy object")?
1025 .try_data()?;
1026
1027 // 5. Let trap be ? GetMethod(handler, "ownKeys").
1028 let Some(trap) = handler.get_method(js_string!("ownKeys"), context)? else {
1029 // 6. If trap is undefined, then
1030 // a. Return ? target.[[OwnPropertyKeys]]().
1031 return target.__own_property_keys__(context);
1032 };
1033
1034 // 7. Let trapResultArray be ? Call(trap, handler, « target »).
1035 let trap_result_array = trap.call(&handler.into(), &[target.clone().into()], context)?;
1036
1037 // 8. Let trapResult be ? CreateListFromArrayLike(trapResultArray, « String, Symbol »).
1038 let trap_result_raw =
1039 trap_result_array.create_list_from_array_like(&[Type::String, Type::Symbol], context)?;
1040
1041 // 9. If trapResult contains any duplicate entries, throw a TypeError exception.
1042 let mut unchecked_result_keys: FxHashSet<PropertyKey> = FxHashSet::default();
1043 let mut trap_result = Vec::new();
1044 for value in &trap_result_raw {
1045 match value.variant() {
1046 JsVariant::String(s) => {
1047 if !unchecked_result_keys.insert(s.clone().into()) {
1048 return Err(JsNativeError::typ()
1049 .with_message("Proxy trap result contains duplicate string property keys")
1050 .into());
1051 }
1052 trap_result.push(s.clone().into());
1053 }
1054 JsVariant::Symbol(s) => {
1055 if !unchecked_result_keys.insert(s.clone().into()) {
1056 return Err(JsNativeError::typ()
1057 .with_message("Proxy trap result contains duplicate symbol property keys")
1058 .into());
1059 }
1060 trap_result.push(s.clone().into());
1061 }
1062 _ => {}
1063 }
1064 }
1065
1066 // 10. Let extensibleTarget be ? IsExtensible(target).
1067 let extensible_target = target.is_extensible(context)?;
1068
1069 // 11. Let targetKeys be ? target.[[OwnPropertyKeys]]().
1070 // 12. Assert: targetKeys is a List of property keys.
1071 // 13. Assert: targetKeys contains no duplicate entries.
1072 let target_keys = target.__own_property_keys__(context)?;
1073
1074 // 14. Let targetConfigurableKeys be a new empty List.
1075 // 15. Let targetNonconfigurableKeys be a new empty List.
1076 let mut target_configurable_keys = Vec::new();
1077 let mut target_nonconfigurable_keys = Vec::new();
1078
1079 // 16. For each element key of targetKeys, do
1080 for key in target_keys {
1081 // a. Let desc be ? target.[[GetOwnProperty]](key).
1082 match target.__get_own_property__(&key, &mut context.into())? {
1083 // b. If desc is not undefined and desc.[[Configurable]] is false, then
1084 Some(desc) if !desc.expect_configurable() => {
1085 // i. Append key as an element of targetNonconfigurableKeys.
1086 target_nonconfigurable_keys.push(key);
1087 }
1088 // c. Else,
1089 _ => {
1090 // i. Append key as an element of targetConfigurableKeys.
1091 target_configurable_keys.push(key);
1092 }
1093 }
1094 }
1095
1096 // 17. If extensibleTarget is true and targetNonconfigurableKeys is empty, then
1097 if extensible_target && target_nonconfigurable_keys.is_empty() {
1098 // a. Return trapResult.
1099 return Ok(trap_result);
1100 }
1101
1102 // 18. Let uncheckedResultKeys be a List whose elements are the elements of trapResult.
1103 // 19. For each element key of targetNonconfigurableKeys, do
1104 for key in target_nonconfigurable_keys {
1105 // a. If key is not an element of uncheckedResultKeys, throw a TypeError exception.
1106 // b. Remove key from uncheckedResultKeys.
1107 if !unchecked_result_keys.remove(&key) {
1108 return Err(JsNativeError::typ()
1109 .with_message("Proxy trap failed to return all non-configurable property keys")
1110 .into());
1111 }
1112 }
1113
1114 // 20. If extensibleTarget is true, return trapResult.
1115 if extensible_target {
1116 return Ok(trap_result);
1117 }
1118
1119 // 21. For each element key of targetConfigurableKeys, do
1120 for key in target_configurable_keys {
1121 // a. If key is not an element of uncheckedResultKeys, throw a TypeError exception.
1122 // b. Remove key from uncheckedResultKeys.
1123 if !unchecked_result_keys.remove(&key) {
1124 return Err(JsNativeError::typ()
1125 .with_message("Proxy trap failed to return all configurable property keys")
1126 .into());
1127 }
1128 }
1129
1130 // 22. If uncheckedResultKeys is not empty, throw a TypeError exception.
1131 if !unchecked_result_keys.is_empty() {
1132 return Err(JsNativeError::typ()
1133 .with_message("Proxy trap failed to return all property keys")
1134 .into());
1135 }
1136
1137 // 23. Return trapResult.
1138 Ok(trap_result)
1139}
1140
1141/// `10.5.12 [[Call]] ( thisArgument, argumentsList )`
1142///
1143/// More information:
1144/// - [ECMAScript reference][spec]
1145///
1146/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-call-thisargument-argumentslist
1147fn proxy_exotic_call(
1148 obj: &JsObject,
1149 argument_count: usize,
1150 context: &mut InternalMethodCallContext<'_>,
1151) -> JsResult<CallValue> {
1152 // 1. Let handler be O.[[ProxyHandler]].
1153 // 2. If handler is null, throw a TypeError exception.
1154 // 3. Assert: Type(handler) is Object.
1155 // 4. Let target be O.[[ProxyTarget]].
1156 let (target, handler) = obj
1157 .downcast_ref::<Proxy>()
1158 .js_expect("Proxy object internal internal method called on non-proxy object")?
1159 .try_data()?;
1160
1161 // 5. Let trap be ? GetMethod(handler, "apply").
1162 let Some(trap) = handler.get_method(js_string!("apply"), context)? else {
1163 // 6. If trap is undefined, then
1164 // a. Return ? Call(target, thisArgument, argumentsList).
1165 return Ok(target.__call__(argument_count));
1166 };
1167
1168 let args = context
1169 .vm
1170 .stack
1171 .calling_convention_pop_arguments(argument_count);
1172
1173 // 7. Let argArray be ! CreateArrayFromList(argumentsList).
1174 let arg_array = array::Array::create_array_from_list(args, context);
1175
1176 // 8. Return ? Call(trap, handler, « target, thisArgument, argArray »).
1177 let _func = context.vm.stack.pop();
1178 let this = context.vm.stack.pop();
1179
1180 context.vm.stack.push(handler); // This
1181 context.vm.stack.push(trap.clone()); // Function
1182
1183 context.vm.stack.push(target);
1184 context.vm.stack.push(this);
1185 context.vm.stack.push(arg_array);
1186 Ok(trap.__call__(3))
1187}
1188
1189/// `[[Construct]] ( argumentsList, newTarget )`
1190///
1191/// More information:
1192/// - [ECMAScript reference][spec]
1193///
1194/// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-construct-argumentslist-newtarget
1195fn proxy_exotic_construct(
1196 obj: &JsObject,
1197 argument_count: usize,
1198 context: &mut InternalMethodCallContext<'_>,
1199) -> JsResult<CallValue> {
1200 // 1. Let handler be O.[[ProxyHandler]].
1201 // 2. If handler is null, throw a TypeError exception.
1202 // 3. Assert: Type(handler) is Object.
1203 // 4. Let target be O.[[ProxyTarget]].
1204 let (target, handler) = obj
1205 .downcast_ref::<Proxy>()
1206 .js_expect("Proxy object internal internal method called on non-proxy object")?
1207 .try_data()?;
1208
1209 // 5. Assert: IsConstructor(target) is true.
1210 assert!(target.is_constructor());
1211
1212 // 6. Let trap be ? GetMethod(handler, "construct").
1213 let Some(trap) = handler.get_method(js_string!("construct"), context)? else {
1214 // 7. If trap is undefined, then
1215 // a. Return ? Construct(target, argumentsList, newTarget).
1216 return Ok(target.__construct__(argument_count));
1217 };
1218
1219 let new_target = context.vm.stack.pop();
1220 let args = context
1221 .vm
1222 .stack
1223 .calling_convention_pop_arguments(argument_count);
1224 let _func = context.vm.stack.pop();
1225 let _this = context.vm.stack.pop();
1226
1227 // 8. Let argArray be ! CreateArrayFromList(argumentsList).
1228 let arg_array = array::Array::create_array_from_list(args, context);
1229
1230 // 9. Let newObj be ? Call(trap, handler, « target, argArray, newTarget »).
1231 let new_obj = trap.call(
1232 &handler.into(),
1233 &[target.into(), arg_array.into(), new_target],
1234 context,
1235 )?;
1236
1237 // 10. If Type(newObj) is not Object, throw a TypeError exception.
1238 let new_obj = new_obj.as_object().ok_or_else(|| {
1239 JsNativeError::typ().with_message("Proxy trap constructor returned non-object value")
1240 })?;
1241
1242 // 11. Return newObj.
1243 context.vm.stack.push(new_obj);
1244 Ok(CallValue::Complete)
1245}
1246
1247#[cfg(test)]
1248mod tests;