1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 04:27:44 +00:00

LibJS: Implement Object.freeze() and Object.seal()

This commit is contained in:
Linus Groh 2021-04-06 22:06:11 +02:00 committed by Andreas Kling
parent 1c3eef5317
commit 9af07c7803
8 changed files with 202 additions and 3 deletions

View file

@ -118,6 +118,7 @@ namespace JS {
P(flat) \ P(flat) \
P(floor) \ P(floor) \
P(forEach) \ P(forEach) \
P(freeze) \
P(from) \ P(from) \
P(fromCharCode) \ P(fromCharCode) \
P(fround) \ P(fround) \
@ -206,6 +207,7 @@ namespace JS {
P(resolve) \ P(resolve) \
P(reverse) \ P(reverse) \
P(round) \ P(round) \
P(seal) \
P(set) \ P(set) \
P(setFullYear) \ P(setFullYear) \
P(setHours) \ P(setHours) \

View file

@ -74,6 +74,8 @@
M(NonExtensibleDefine, "Cannot define property {} on non-extensible object") \ M(NonExtensibleDefine, "Cannot define property {} on non-extensible object") \
M(NumberIncompatibleThis, "Number.prototype.{} method called with incompatible this target") \ M(NumberIncompatibleThis, "Number.prototype.{} method called with incompatible this target") \
M(ObjectDefinePropertyReturnedFalse, "Object's [[DefineProperty]] method returned false") \ 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(ObjectSetPrototypeOfReturnedFalse, "Object's [[SetPrototypeOf]] method returned false") \
M(ObjectSetPrototypeOfTwoArgs, "Object.setPrototypeOf requires at least two arguments") \ M(ObjectSetPrototypeOfTwoArgs, "Object.setPrototypeOf requires at least two arguments") \
M(ObjectPreventExtensionsReturnedFalse, "Object's [[PreventExtensions]] method returned false") \ M(ObjectPreventExtensionsReturnedFalse, "Object's [[PreventExtensions]] method returned false") \

View file

@ -160,6 +160,54 @@ bool Object::prevent_extensions()
return true; 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 Value Object::get_own_property(const PropertyName& property_name, Value receiver) const
{ {
VERIFY(property_name.is_valid()); VERIFY(property_name.is_valid());

View file

@ -84,6 +84,11 @@ public:
DefineProperty, DefineProperty,
}; };
enum class IntegrityLevel {
Sealed,
Frozen,
};
Shape& shape() { return *m_shape; } Shape& shape() { return *m_shape; }
const Shape& shape() const { 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 is_extensible() const { return m_is_extensible; }
virtual bool prevent_extensions(); virtual bool prevent_extensions();
bool set_integrity_level(IntegrityLevel);
virtual Value value_of() const { return Value(const_cast<Object*>(this)); } virtual Value value_of() const { return Value(const_cast<Object*>(this)); }
virtual Value ordinary_to_primitive(Value::PreferredType preferred_type) const; virtual Value ordinary_to_primitive(Value::PreferredType preferred_type) const;

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org> * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2020-2021, Linus Groh <mail@linusgroh.de>
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * 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.setPrototypeOf, set_prototype_of, 2, attr);
define_native_function(vm.names.isExtensible, is_extensible, 1, 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.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.keys, keys, 1, attr);
define_native_function(vm.names.values, values, 1, attr); define_native_function(vm.names.values, values, 1, attr);
define_native_function(vm.names.entries, entries, 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); auto argument = vm.argument(0);
if (!argument.is_object()) if (!argument.is_object())
return argument; return argument;
if (!argument.as_object().prevent_extensions()) { auto status = argument.as_object().prevent_extensions();
if (!vm.exception()) if (vm.exception())
vm.throw_exception<TypeError>(global_object, ErrorType::ObjectPreventExtensionsReturnedFalse); return {};
if (!status) {
vm.throw_exception<TypeError>(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<TypeError>(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<TypeError>(global_object, ErrorType::ObjectSealFailed);
return {}; return {};
} }
return argument; return argument;

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org> * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2020-2021, Linus Groh <mail@linusgroh.de>
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * 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(set_prototype_of);
JS_DECLARE_NATIVE_FUNCTION(is_extensible); JS_DECLARE_NATIVE_FUNCTION(is_extensible);
JS_DECLARE_NATIVE_FUNCTION(prevent_extensions); 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(keys);
JS_DECLARE_NATIVE_FUNCTION(values); JS_DECLARE_NATIVE_FUNCTION(values);
JS_DECLARE_NATIVE_FUNCTION(entries); JS_DECLARE_NATIVE_FUNCTION(entries);

View file

@ -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");
});
});

View file

@ -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");
});
});