ClassDefinitionEvaluation calls MakeConstructor with false for the writablePrototype argument, so class constructors get a non-writable, non-enumerable, non-configurable prototype property. We were using the ordinary function attributes instead, which also allowed static class elements named "prototype" to redefine the property. Add LibJS coverage for the constructor and prototype descriptors, and for static methods/accessors that attempt to define "prototype".
287 lines
13 KiB
C++
287 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2026, Andreas Kling <andreas@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/ScopeGuard.h>
|
|
#include <LibGC/ConservativeVector.h>
|
|
#include <LibJS/Bytecode/Executable.h>
|
|
#include <LibJS/Runtime/AbstractOperations.h>
|
|
#include <LibJS/Runtime/Accessor.h>
|
|
#include <LibJS/Runtime/ClassConstruction.h>
|
|
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
|
|
#include <LibJS/Runtime/Error.h>
|
|
#include <LibJS/Runtime/Object.h>
|
|
#include <LibJS/Runtime/PrivateEnvironment.h>
|
|
#include <LibJS/Runtime/SharedFunctionInstanceData.h>
|
|
#include <LibJS/Runtime/ValueInlines.h>
|
|
|
|
namespace JS {
|
|
|
|
static void update_function_name(Value value, ClassElementName const& name, Optional<StringView> const& prefix = {})
|
|
{
|
|
if (auto function = value.as_if<ECMAScriptFunctionObject>(); function && function->name().is_empty())
|
|
function->set_inferred_name(name, prefix);
|
|
}
|
|
|
|
static ThrowCompletionOr<ClassElementName> resolve_element_key(VM& vm, Bytecode::ClassElementDescriptor const& descriptor, Value property_key)
|
|
{
|
|
if (descriptor.is_private) {
|
|
auto private_environment = vm.running_execution_context().private_environment;
|
|
VERIFY(private_environment);
|
|
return ClassElementName { private_environment->resolve_private_identifier(*descriptor.private_identifier) };
|
|
}
|
|
|
|
VERIFY(!property_key.is_special_empty_value());
|
|
|
|
if (property_key.is_object())
|
|
property_key = TRY(property_key.to_primitive(vm, Value::PreferredType::String));
|
|
|
|
auto key = TRY(PropertyKey::from_value(vm, property_key));
|
|
return ClassElementName { key };
|
|
}
|
|
|
|
ThrowCompletionOr<ECMAScriptFunctionObject*> construct_class(
|
|
VM& vm,
|
|
Bytecode::ClassBlueprint const& blueprint,
|
|
Bytecode::Executable const& executable,
|
|
Environment* class_environment,
|
|
Environment* outer_environment,
|
|
Value super_class,
|
|
ReadonlySpan<Value> element_keys,
|
|
Optional<Utf16FlyString> const& binding_name,
|
|
Utf16FlyString const& class_name)
|
|
{
|
|
auto& realm = *vm.current_realm();
|
|
|
|
// We might not set the lexical environment but we always want to restore it eventually.
|
|
ArmedScopeGuard restore_environment = [&] {
|
|
vm.running_execution_context().lexical_environment = outer_environment;
|
|
};
|
|
|
|
vm.running_execution_context().lexical_environment = class_environment;
|
|
|
|
auto proto_parent = GC::Ptr { realm.intrinsics().object_prototype() };
|
|
auto constructor_parent = realm.intrinsics().function_prototype();
|
|
|
|
if (blueprint.has_super_class) {
|
|
if (super_class.is_null()) {
|
|
proto_parent = nullptr;
|
|
} else if (!super_class.is_constructor()) {
|
|
return vm.throw_completion<TypeError>(ErrorType::ClassExtendsValueNotAConstructorOrNull, super_class);
|
|
} else {
|
|
auto super_class_prototype = TRY(super_class.get(vm, vm.names.prototype));
|
|
if (!super_class_prototype.is_null() && !super_class_prototype.is_object())
|
|
return vm.throw_completion<TypeError>(ErrorType::ClassExtendsValueInvalidPrototype, super_class_prototype);
|
|
|
|
if (super_class_prototype.is_null())
|
|
proto_parent = nullptr;
|
|
else
|
|
proto_parent = super_class_prototype.as_object();
|
|
|
|
constructor_parent = super_class.as_object();
|
|
}
|
|
}
|
|
|
|
auto prototype = Object::create_prototype(realm, proto_parent);
|
|
|
|
// FIXME: Step 14.a is done in the parser. By using a synthetic super(...args) which does not call @@iterator of %Array.prototype%
|
|
auto constructor_shared_data = executable.shared_function_data[blueprint.constructor_shared_function_data_index];
|
|
auto class_constructor = ECMAScriptFunctionObject::create_from_function_data(
|
|
realm,
|
|
*constructor_shared_data,
|
|
vm.lexical_environment(),
|
|
vm.running_execution_context().private_environment);
|
|
|
|
class_constructor->set_name(class_name);
|
|
class_constructor->set_home_object(prototype);
|
|
class_constructor->set_is_class_constructor();
|
|
class_constructor->define_direct_property(vm.names.prototype, prototype, 0);
|
|
TRY(class_constructor->internal_set_prototype_of(constructor_parent));
|
|
|
|
if (blueprint.has_super_class)
|
|
class_constructor->set_constructor_kind(ConstructorKind::Derived);
|
|
|
|
prototype->define_direct_property(vm.names.constructor, class_constructor, Attribute::Writable | Attribute::Configurable);
|
|
|
|
using StaticElement = Variant<ClassFieldDefinition, GC::Ref<ECMAScriptFunctionObject>>;
|
|
|
|
GC::ConservativeVector<PrivateElement> static_private_methods;
|
|
GC::ConservativeVector<PrivateElement> instance_private_methods;
|
|
GC::ConservativeVector<ClassFieldDefinition> instance_fields;
|
|
GC::ConservativeVector<StaticElement> static_elements;
|
|
|
|
for (size_t element_index = 0; element_index < blueprint.elements.size(); ++element_index) {
|
|
auto const& descriptor = blueprint.elements[element_index];
|
|
auto& home_object = descriptor.is_static ? static_cast<Object&>(*class_constructor) : static_cast<Object&>(*prototype);
|
|
|
|
switch (descriptor.kind) {
|
|
case Bytecode::ClassElementDescriptor::Kind::Method:
|
|
case Bytecode::ClassElementDescriptor::Kind::Getter:
|
|
case Bytecode::ClassElementDescriptor::Kind::Setter: {
|
|
auto element_name = TRY(resolve_element_key(vm, descriptor, element_keys[element_index]));
|
|
|
|
auto shared_data = executable.shared_function_data[*descriptor.shared_function_data_index];
|
|
auto& method_function = *ECMAScriptFunctionObject::create_from_function_data(
|
|
realm,
|
|
*shared_data,
|
|
vm.lexical_environment(),
|
|
vm.running_execution_context().private_environment);
|
|
|
|
auto method_value = Value(&method_function);
|
|
method_function.make_method(home_object);
|
|
|
|
if (element_name.has<PropertyKey>()) {
|
|
auto& property_key = element_name.get<PropertyKey>();
|
|
switch (descriptor.kind) {
|
|
case Bytecode::ClassElementDescriptor::Kind::Method: {
|
|
update_function_name(method_value, element_name);
|
|
PropertyDescriptor property_descriptor { .value = method_value, .writable = true, .enumerable = false, .configurable = true };
|
|
TRY(home_object.define_property_or_throw(property_key, property_descriptor));
|
|
break;
|
|
}
|
|
case Bytecode::ClassElementDescriptor::Kind::Getter: {
|
|
update_function_name(method_value, element_name, "get"sv);
|
|
PropertyDescriptor property_descriptor { .get = &method_function, .enumerable = false, .configurable = true };
|
|
TRY(home_object.define_property_or_throw(property_key, property_descriptor));
|
|
break;
|
|
}
|
|
case Bytecode::ClassElementDescriptor::Kind::Setter: {
|
|
update_function_name(method_value, element_name, "set"sv);
|
|
PropertyDescriptor property_descriptor { .set = &method_function, .enumerable = false, .configurable = true };
|
|
TRY(home_object.define_property_or_throw(property_key, property_descriptor));
|
|
break;
|
|
}
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
} else {
|
|
auto& private_name = element_name.get<PrivateName>();
|
|
auto& container = descriptor.is_static ? static_private_methods : instance_private_methods;
|
|
|
|
PrivateElement private_element = [&] {
|
|
switch (descriptor.kind) {
|
|
case Bytecode::ClassElementDescriptor::Kind::Method:
|
|
update_function_name(method_value, element_name);
|
|
return PrivateElement { private_name, PrivateElement::Kind::Method, method_value };
|
|
case Bytecode::ClassElementDescriptor::Kind::Getter:
|
|
update_function_name(method_value, element_name, "get"sv);
|
|
return PrivateElement { private_name, PrivateElement::Kind::Accessor, Value(Accessor::create(vm, &method_function, nullptr)) };
|
|
case Bytecode::ClassElementDescriptor::Kind::Setter:
|
|
update_function_name(method_value, element_name, "set"sv);
|
|
return PrivateElement { private_name, PrivateElement::Kind::Accessor, Value(Accessor::create(vm, nullptr, &method_function)) };
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
}();
|
|
|
|
// Merge accessor pairs.
|
|
auto added_to_existing = false;
|
|
for (auto& existing : container) {
|
|
if (existing.key == private_element.key) {
|
|
VERIFY(existing.kind == PrivateElement::Kind::Accessor);
|
|
VERIFY(private_element.kind == PrivateElement::Kind::Accessor);
|
|
auto& accessor = private_element.value.as_accessor();
|
|
if (!accessor.getter())
|
|
existing.value.as_accessor().set_setter(accessor.setter());
|
|
else
|
|
existing.value.as_accessor().set_getter(accessor.getter());
|
|
added_to_existing = true;
|
|
}
|
|
}
|
|
|
|
if (!added_to_existing)
|
|
container.append(move(private_element));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Bytecode::ClassElementDescriptor::Kind::Field: {
|
|
auto element_name = TRY(resolve_element_key(vm, descriptor, element_keys[element_index]));
|
|
|
|
Variant<GC::Ref<ECMAScriptFunctionObject>, Value, Empty> initializer;
|
|
if (descriptor.has_initializer) {
|
|
if (descriptor.literal_value.has_value()) {
|
|
initializer = *descriptor.literal_value;
|
|
} else {
|
|
auto shared_data = executable.shared_function_data[*descriptor.shared_function_data_index];
|
|
|
|
// Set class_field_initializer_name at runtime for computed keys.
|
|
if (!descriptor.is_private && !shared_data->m_class_field_initializer_name.has<PropertyKey>()
|
|
&& !shared_data->m_class_field_initializer_name.has<PrivateName>()) {
|
|
shared_data->m_class_field_initializer_name = element_name.visit(
|
|
[](PropertyKey const& key) -> Variant<PropertyKey, PrivateName, Empty> { return key; },
|
|
[](PrivateName const& name) -> Variant<PropertyKey, PrivateName, Empty> { return name; });
|
|
}
|
|
|
|
auto function = ECMAScriptFunctionObject::create_from_function_data(
|
|
realm,
|
|
*shared_data,
|
|
vm.lexical_environment(),
|
|
vm.running_execution_context().private_environment);
|
|
function->make_method(home_object);
|
|
initializer = function;
|
|
}
|
|
}
|
|
|
|
ClassFieldDefinition field {
|
|
move(element_name),
|
|
move(initializer),
|
|
};
|
|
|
|
if (descriptor.is_static)
|
|
static_elements.append(move(field));
|
|
else
|
|
instance_fields.append(move(field));
|
|
break;
|
|
}
|
|
|
|
case Bytecode::ClassElementDescriptor::Kind::StaticInitializer: {
|
|
auto shared_data = executable.shared_function_data[*descriptor.shared_function_data_index];
|
|
auto body_function = ECMAScriptFunctionObject::create_from_function_data(
|
|
realm,
|
|
*shared_data,
|
|
vm.lexical_environment(),
|
|
vm.running_execution_context().private_environment);
|
|
body_function->make_method(home_object);
|
|
static_elements.append(GC::Ref { *body_function });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
vm.running_execution_context().lexical_environment = outer_environment;
|
|
restore_environment.disarm();
|
|
|
|
if (binding_name.has_value())
|
|
MUST(class_environment->initialize_binding(vm, binding_name.value(), class_constructor, Environment::InitializeBindingHint::Normal));
|
|
|
|
for (auto& field : instance_fields)
|
|
class_constructor->add_field(field);
|
|
|
|
for (auto& private_method : instance_private_methods)
|
|
class_constructor->add_private_method(private_method);
|
|
|
|
for (auto& method : static_private_methods)
|
|
TRY(class_constructor->private_method_or_accessor_add(move(method)));
|
|
|
|
for (auto& element : static_elements) {
|
|
TRY(element.visit(
|
|
[&](ClassFieldDefinition& field) -> ThrowCompletionOr<void> {
|
|
return TRY(class_constructor->define_field(field));
|
|
},
|
|
[&](GC::Ref<ECMAScriptFunctionObject> static_block_function) -> ThrowCompletionOr<void> {
|
|
// We discard any value returned here.
|
|
TRY(call(vm, *static_block_function, class_constructor));
|
|
return {};
|
|
}));
|
|
}
|
|
|
|
if (blueprint.source_code)
|
|
class_constructor->set_source_text_range(*blueprint.source_code, blueprint.source_text_offset, blueprint.source_text_length);
|
|
|
|
return { class_constructor };
|
|
}
|
|
|
|
}
|