ladybird/Libraries/LibJS/Runtime/ClassConstruction.cpp
Andreas Kling 991ba90d40 LibJS: Make class prototype property non-writable
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".
2026-05-22 01:56:57 +02:00

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 };
}
}