From 9af07c7803aaf08c55414dca315bbbfb9a23754e Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Tue, 6 Apr 2021 22:06:11 +0200 Subject: [PATCH] LibJS: Implement Object.freeze() and Object.seal() --- .../LibJS/Runtime/CommonPropertyNames.h | 2 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 2 + Userland/Libraries/LibJS/Runtime/Object.cpp | 48 ++++++++++++++++++ Userland/Libraries/LibJS/Runtime/Object.h | 7 +++ .../LibJS/Runtime/ObjectConstructor.cpp | 43 ++++++++++++++-- .../LibJS/Runtime/ObjectConstructor.h | 3 ++ .../Tests/builtins/Object/Object.freeze.js | 50 +++++++++++++++++++ .../Tests/builtins/Object/Object.seal.js | 50 +++++++++++++++++++ 8 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Object/Object.freeze.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Object/Object.seal.js diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 21ad39f0b4..e7f685ff7c 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -118,6 +118,7 @@ namespace JS { P(flat) \ P(floor) \ P(forEach) \ + P(freeze) \ P(from) \ P(fromCharCode) \ P(fround) \ @@ -206,6 +207,7 @@ namespace JS { P(resolve) \ P(reverse) \ P(round) \ + P(seal) \ P(set) \ P(setFullYear) \ P(setHours) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 6437d9d479..fc91c2f917 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -74,6 +74,8 @@ M(NonExtensibleDefine, "Cannot define property {} on non-extensible object") \ M(NumberIncompatibleThis, "Number.prototype.{} method called with incompatible this target") \ M(ObjectDefinePropertyReturnedFalse, "Object's [[DefineProperty]] method returned false") \ + M(ObjectFreezeFailed, "Could not freeze object") \ + M(ObjectSealFailed, "Could not seal object") \ M(ObjectSetPrototypeOfReturnedFalse, "Object's [[SetPrototypeOf]] method returned false") \ M(ObjectSetPrototypeOfTwoArgs, "Object.setPrototypeOf requires at least two arguments") \ M(ObjectPreventExtensionsReturnedFalse, "Object's [[PreventExtensions]] method returned false") \ diff --git a/Userland/Libraries/LibJS/Runtime/Object.cpp b/Userland/Libraries/LibJS/Runtime/Object.cpp index 5bc2f27322..4bcf0bf33e 100644 --- a/Userland/Libraries/LibJS/Runtime/Object.cpp +++ b/Userland/Libraries/LibJS/Runtime/Object.cpp @@ -160,6 +160,54 @@ bool Object::prevent_extensions() return true; } +// 7.3.15 SetIntegrityLevel, https://tc39.es/ecma262/#sec-setintegritylevel +bool Object::set_integrity_level(IntegrityLevel level) +{ + // FIXME: This feels clunky and should get nicer abstractions. + auto update_property = [this](auto& key, auto attributes) { + auto property_name = PropertyName::from_value(global_object(), key); + auto metadata = shape().lookup(property_name.to_string_or_symbol()); + VERIFY(metadata.has_value()); + auto value = get_direct(metadata->offset); + define_property(property_name, value, metadata->attributes.bits() & attributes); + }; + + auto& vm = this->vm(); + auto status = prevent_extensions(); + if (vm.exception()) + return false; + if (!status) + return false; + auto keys = get_own_properties(PropertyKind::Key); + if (vm.exception()) + return false; + switch (level) { + case IntegrityLevel::Sealed: + for (auto& key : keys) { + update_property(key, ~Attribute::Configurable); + if (vm.exception()) + return {}; + } + break; + case IntegrityLevel::Frozen: + for (auto& key : keys) { + auto property_name = PropertyName::from_value(global_object(), key); + auto property_descriptor = get_own_property_descriptor(property_name); + VERIFY(property_descriptor.has_value()); + u8 attributes = property_descriptor->is_accessor_descriptor() + ? ~Attribute::Configurable + : ~Attribute::Configurable & ~Attribute::Writable; + update_property(key, attributes); + if (vm.exception()) + return {}; + } + break; + default: + VERIFY_NOT_REACHED(); + } + return true; +} + Value Object::get_own_property(const PropertyName& property_name, Value receiver) const { VERIFY(property_name.is_valid()); diff --git a/Userland/Libraries/LibJS/Runtime/Object.h b/Userland/Libraries/LibJS/Runtime/Object.h index e72e0cc705..6cb4455cd4 100644 --- a/Userland/Libraries/LibJS/Runtime/Object.h +++ b/Userland/Libraries/LibJS/Runtime/Object.h @@ -84,6 +84,11 @@ public: DefineProperty, }; + enum class IntegrityLevel { + Sealed, + Frozen, + }; + Shape& shape() { return *m_shape; } const Shape& shape() const { return *m_shape; } @@ -129,6 +134,8 @@ public: virtual bool is_extensible() const { return m_is_extensible; } virtual bool prevent_extensions(); + bool set_integrity_level(IntegrityLevel); + virtual Value value_of() const { return Value(const_cast(this)); } virtual Value ordinary_to_primitive(Value::PreferredType preferred_type) const; diff --git a/Userland/Libraries/LibJS/Runtime/ObjectConstructor.cpp b/Userland/Libraries/LibJS/Runtime/ObjectConstructor.cpp index e9ab856b11..735a5ef4b2 100644 --- a/Userland/Libraries/LibJS/Runtime/ObjectConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/ObjectConstructor.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling + * Copyright (c) 2020-2021, Linus Groh * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -55,6 +56,8 @@ void ObjectConstructor::initialize(GlobalObject& global_object) define_native_function(vm.names.setPrototypeOf, set_prototype_of, 2, attr); define_native_function(vm.names.isExtensible, is_extensible, 1, attr); define_native_function(vm.names.preventExtensions, prevent_extensions, 1, attr); + define_native_function(vm.names.freeze, freeze, 1, attr); + define_native_function(vm.names.seal, seal, 1, attr); define_native_function(vm.names.keys, keys, 1, attr); define_native_function(vm.names.values, values, 1, attr); define_native_function(vm.names.entries, entries, 1, attr); @@ -146,9 +149,43 @@ JS_DEFINE_NATIVE_FUNCTION(ObjectConstructor::prevent_extensions) auto argument = vm.argument(0); if (!argument.is_object()) return argument; - if (!argument.as_object().prevent_extensions()) { - if (!vm.exception()) - vm.throw_exception(global_object, ErrorType::ObjectPreventExtensionsReturnedFalse); + auto status = argument.as_object().prevent_extensions(); + if (vm.exception()) + return {}; + if (!status) { + vm.throw_exception(global_object, ErrorType::ObjectPreventExtensionsReturnedFalse); + return {}; + } + return argument; +} + +// 20.1.2.6 Object.freeze, https://tc39.es/ecma262/#sec-object.freeze +JS_DEFINE_NATIVE_FUNCTION(ObjectConstructor::freeze) +{ + auto argument = vm.argument(0); + if (!argument.is_object()) + return argument; + auto status = argument.as_object().set_integrity_level(Object::IntegrityLevel::Frozen); + if (vm.exception()) + return {}; + if (!status) { + vm.throw_exception(global_object, ErrorType::ObjectFreezeFailed); + return {}; + } + return argument; +} + +// 20.1.2.20 Object.seal, https://tc39.es/ecma262/#sec-object.seal +JS_DEFINE_NATIVE_FUNCTION(ObjectConstructor::seal) +{ + auto argument = vm.argument(0); + if (!argument.is_object()) + return argument; + auto status = argument.as_object().set_integrity_level(Object::IntegrityLevel::Sealed); + if (vm.exception()) + return {}; + if (!status) { + vm.throw_exception(global_object, ErrorType::ObjectSealFailed); return {}; } return argument; diff --git a/Userland/Libraries/LibJS/Runtime/ObjectConstructor.h b/Userland/Libraries/LibJS/Runtime/ObjectConstructor.h index 717539651c..df7ff1f83c 100644 --- a/Userland/Libraries/LibJS/Runtime/ObjectConstructor.h +++ b/Userland/Libraries/LibJS/Runtime/ObjectConstructor.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling + * Copyright (c) 2020-2021, Linus Groh * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -52,6 +53,8 @@ private: JS_DECLARE_NATIVE_FUNCTION(set_prototype_of); JS_DECLARE_NATIVE_FUNCTION(is_extensible); JS_DECLARE_NATIVE_FUNCTION(prevent_extensions); + JS_DECLARE_NATIVE_FUNCTION(seal); + JS_DECLARE_NATIVE_FUNCTION(freeze); JS_DECLARE_NATIVE_FUNCTION(keys); JS_DECLARE_NATIVE_FUNCTION(values); JS_DECLARE_NATIVE_FUNCTION(entries); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Object/Object.freeze.js b/Userland/Libraries/LibJS/Tests/builtins/Object/Object.freeze.js new file mode 100644 index 0000000000..0f37f1fa92 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Object/Object.freeze.js @@ -0,0 +1,50 @@ +test("length is 1", () => { + expect(Object.freeze).toHaveLength(1); +}); + +describe("normal behavior", () => { + test("returns given argument", () => { + const o = {}; + expect(Object.freeze(42)).toBe(42); + expect(Object.freeze("foobar")).toBe("foobar"); + expect(Object.freeze(o)).toBe(o); + }); + + test("prevents addition of new properties", () => { + const o = {}; + expect(o.foo).toBeUndefined(); + Object.freeze(o); + o.foo = "bar"; + expect(o.foo).toBeUndefined(); + }); + + test("prevents deletion of existing properties", () => { + const o = { foo: "bar" }; + expect(o.foo).toBe("bar"); + Object.freeze(o); + delete o.foo; + expect(o.foo).toBe("bar"); + }); + + test("prevents changing attributes of existing properties", () => { + const o = { foo: "bar" }; + Object.freeze(o); + // FIXME: These don't change anything and should not throw! + // expect(Object.defineProperty(o, "foo", {})).toBe(o); + // expect(Object.defineProperty(o, "foo", { configurable: false })).toBe(o); + expect(() => { + Object.defineProperty(o, "foo", { configurable: true }); + }).toThrowWithMessage( + TypeError, + "Cannot change attributes of non-configurable property 'foo'" + ); + }); + + test("prevents changing value of existing properties", () => { + const o = { foo: "bar" }; + expect(o.foo).toBe("bar"); + Object.freeze(o); + o.foo = "baz"; + expect(o.foo).toBe("bar"); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Object/Object.seal.js b/Userland/Libraries/LibJS/Tests/builtins/Object/Object.seal.js new file mode 100644 index 0000000000..ee7209c68f --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Object/Object.seal.js @@ -0,0 +1,50 @@ +test("length is 1", () => { + expect(Object.seal).toHaveLength(1); +}); + +describe("normal behavior", () => { + test("returns given argument", () => { + const o = {}; + expect(Object.seal(42)).toBe(42); + expect(Object.seal("foobar")).toBe("foobar"); + expect(Object.seal(o)).toBe(o); + }); + + test("prevents addition of new properties", () => { + const o = {}; + expect(o.foo).toBeUndefined(); + Object.seal(o); + o.foo = "bar"; + expect(o.foo).toBeUndefined(); + }); + + test("prevents deletion of existing properties", () => { + const o = { foo: "bar" }; + expect(o.foo).toBe("bar"); + Object.seal(o); + delete o.foo; + expect(o.foo).toBe("bar"); + }); + + test("prevents changing attributes of existing properties", () => { + const o = { foo: "bar" }; + Object.seal(o); + // FIXME: These don't change anything and should not throw! + // expect(Object.defineProperty(o, "foo", {})).toBe(o); + // expect(Object.defineProperty(o, "foo", { configurable: false })).toBe(o); + expect(() => { + Object.defineProperty(o, "foo", { configurable: true }); + }).toThrowWithMessage( + TypeError, + "Cannot change attributes of non-configurable property 'foo'" + ); + }); + + test("doesn't prevent changing value of existing properties", () => { + const o = { foo: "bar" }; + expect(o.foo).toBe("bar"); + Object.seal(o); + o.foo = "baz"; + expect(o.foo).toBe("baz"); + }); +});