瀏覽代碼

LibJS: Implement Proxy.revocable()

Linus Groh 4 年之前
父節點
當前提交
9b35231453

+ 3 - 0
Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -202,6 +202,7 @@ namespace JS {
     P(preventExtensions)                     \
     P(propertyIsEnumerable)                  \
     P(prototype)                             \
+    P(proxy)                                 \
     P(push)                                  \
     P(race)                                  \
     P(random)                                \
@@ -212,6 +213,8 @@ namespace JS {
     P(repeat)                                \
     P(resolve)                               \
     P(reverse)                               \
+    P(revocable)                             \
+    P(revoke)                                \
     P(round)                                 \
     P(seal)                                  \
     P(set)                                   \

+ 42 - 10
Userland/Libraries/LibJS/Runtime/ProxyConstructor.cpp

@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org>
+ * Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -12,6 +13,20 @@
 
 namespace JS {
 
+static ProxyObject* proxy_create(GlobalObject& global_object, Value target, Value handler)
+{
+    auto& vm = global_object.vm();
+    if (!target.is_object()) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::ProxyConstructorBadType, "target", target.to_string_without_side_effects());
+        return {};
+    }
+    if (!handler.is_object()) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::ProxyConstructorBadType, "handler", handler.to_string_without_side_effects());
+        return {};
+    }
+    return ProxyObject::create(global_object, target.as_object(), handler.as_object());
+}
+
 ProxyConstructor::ProxyConstructor(GlobalObject& global_object)
     : NativeFunction(vm().names.Proxy, *global_object.function_prototype())
 {
@@ -22,6 +37,8 @@ void ProxyConstructor::initialize(GlobalObject& global_object)
     auto& vm = this->vm();
     NativeFunction::initialize(global_object);
     define_property(vm.names.length, Value(2), Attribute::Configurable);
+    u8 attr = Attribute::Writable | Attribute::Configurable;
+    define_native_function(vm.names.revocable, revocable, 2, attr);
 }
 
 ProxyConstructor::~ProxyConstructor()
@@ -38,18 +55,33 @@ Value ProxyConstructor::call()
 Value ProxyConstructor::construct(Function&)
 {
     auto& vm = this->vm();
-    auto target = vm.argument(0);
-    auto handler = vm.argument(1);
+    return proxy_create(global_object(), vm.argument(0), vm.argument(1));
+}
 
-    if (!target.is_object()) {
-        vm.throw_exception<TypeError>(global_object(), ErrorType::ProxyConstructorBadType, "target", target.to_string_without_side_effects());
-        return {};
-    }
-    if (!handler.is_object()) {
-        vm.throw_exception<TypeError>(global_object(), ErrorType::ProxyConstructorBadType, "handler", handler.to_string_without_side_effects());
+// 28.2.2.1 Proxy.revocable, https://tc39.es/ecma262/multipage/reflection.html#sec-proxy.revocable
+JS_DEFINE_NATIVE_FUNCTION(ProxyConstructor::revocable)
+{
+    auto* proxy = proxy_create(global_object, vm.argument(0), vm.argument(1));
+    if (vm.exception())
         return {};
-    }
-    return ProxyObject::create(global_object(), target.as_object(), handler.as_object());
+
+    // 28.2.2.1.1 Proxy Revocation Functions, https://tc39.es/ecma262/multipage/reflection.html#sec-proxy-revocation-functions
+    auto* revoker = NativeFunction::create(global_object, "", [proxy_handle = make_handle(proxy)](auto&, auto&) -> Value {
+        auto& proxy = const_cast<ProxyObject&>(*proxy_handle.cell());
+        if (proxy.is_revoked())
+            return js_undefined();
+        // NOTE: The spec wants us to unset [[ProxyTarget]] and [[ProxyHandler]],
+        // which is their way of revoking the Proxy - this might affect GC-ability,
+        // but AFAICT not doing that should be ok compatibility-wise.
+        proxy.revoke();
+        return js_undefined();
+    });
+    revoker->define_property(vm.names.length, Value(0));
+
+    auto* result = Object::create_empty(global_object);
+    result->define_property(vm.names.proxy, proxy);
+    result->define_property(vm.names.revoke, revoker);
+    return result;
 }
 
 }

+ 3 - 0
Userland/Libraries/LibJS/Runtime/ProxyConstructor.h

@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org>
+ * Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -23,6 +24,8 @@ public:
 
 private:
     virtual bool has_constructor() const override { return true; }
+
+    JS_DECLARE_NATIVE_FUNCTION(revocable);
 };
 
 }

+ 1 - 0
Userland/Libraries/LibJS/Runtime/ProxyObject.h

@@ -39,6 +39,7 @@ public:
     virtual bool put(const PropertyName& name, Value value, Value receiver) override;
     virtual bool delete_property(const PropertyName& name) override;
 
+    bool is_revoked() const { return m_is_revoked; }
     void revoke() { m_is_revoked = true; }
 
 private:

+ 72 - 0
Userland/Libraries/LibJS/Tests/builtins/Proxy/Proxy.revocable.js

@@ -0,0 +1,72 @@
+test("length is 2", () => {
+    expect(Proxy.revocable).toHaveLength(2);
+});
+
+describe("errors", () => {
+    test("constructor argument count", () => {
+        expect(() => {
+            Proxy.revocable();
+        }).toThrowWithMessage(
+            TypeError,
+            "Expected target argument of Proxy constructor to be object, got undefined"
+        );
+
+        expect(() => {
+            Proxy.revocable({});
+        }).toThrowWithMessage(
+            TypeError,
+            "Expected handler argument of Proxy constructor to be object, got undefined"
+        );
+    });
+
+    test("constructor requires objects", () => {
+        expect(() => {
+            Proxy.revocable(1, {});
+        }).toThrowWithMessage(
+            TypeError,
+            "Expected target argument of Proxy constructor to be object, got 1"
+        );
+
+        expect(() => {
+            Proxy.revocable({}, 1);
+        }).toThrowWithMessage(
+            TypeError,
+            "Expected handler argument of Proxy constructor to be object, got 1"
+        );
+    });
+});
+
+describe("normal behavior", () => {
+    test("returns object with 'proxy' and 'revoke' properties", () => {
+        const revocable = Proxy.revocable(
+            {},
+            {
+                get() {
+                    return 42;
+                },
+            }
+        );
+        expect(typeof revocable).toBe("object");
+        expect(Object.getPrototypeOf(revocable)).toBe(Object.prototype);
+        expect(revocable.hasOwnProperty("proxy")).toBeTrue();
+        expect(revocable.hasOwnProperty("revoke")).toBeTrue();
+        expect(typeof revocable.revoke).toBe("function");
+        // Can't `instanceof Proxy`, but this should do the trick :^)
+        expect(revocable.proxy.foo).toBe(42);
+    });
+
+    test("'revoke' function revokes Proxy", () => {
+        const revocable = Proxy.revocable({}, {});
+        expect(revocable.proxy.foo).toBeUndefined();
+        expect(revocable.revoke()).toBeUndefined();
+        expect(() => {
+            revocable.proxy.foo;
+        }).toThrowWithMessage(TypeError, "An operation was performed on a revoked Proxy object");
+    });
+
+    test("'revoke' called multiple times is a noop", () => {
+        const revocable = Proxy.revocable({}, {});
+        expect(revocable.revoke()).toBeUndefined();
+        expect(revocable.revoke()).toBeUndefined();
+    });
+});