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:
parent
1c3eef5317
commit
9af07c7803
8 changed files with 202 additions and 3 deletions
|
@ -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) \
|
||||||
|
|
|
@ -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") \
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue