1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 21:37:35 +00:00

LibJS: Implement spec-compliant OrdinaryToPrimitive

This renames Object::to_primitive() to Object::ordinary_to_primitive()
for two reasons:

- No confusion with Value::to_primitive()
- To match the spec's name

Also change existing uses of Object::to_primitive() to
Value::to_primitive() when the spec uses the latter (which will still
call Object::ordinary_to_primitive()). Object::to_string() has been
removed as it's not needed anymore (and nothing the spec uses).

This makes it possible to overwrite an object's toString and valueOf and
have them provide results for anything that uses to_primitive() - e.g.:

    const o = { toString: undefined, valueOf: () => 42 };
    Number(o) // 42, previously NaN
    ["foo", o].toString(); // "foo,42", previously "foo,[object Object]"
    ++o // 43, previously NaN

etc.
This commit is contained in:
Linus Groh 2020-11-03 19:52:21 +00:00 committed by Andreas Kling
parent e163db248d
commit fb89c324c5
4 changed files with 50 additions and 42 deletions

View file

@ -836,48 +836,30 @@ bool Object::has_own_property(const PropertyName& property_name) const
return shape().lookup(property_name.to_string_or_symbol()).has_value(); return shape().lookup(property_name.to_string_or_symbol()).has_value();
} }
Value Object::to_primitive(Value::PreferredType preferred_type) const Value Object::ordinary_to_primitive(Value::PreferredType preferred_type) const
{ {
Value result = js_undefined(); ASSERT(preferred_type == Value::PreferredType::String || preferred_type == Value::PreferredType::Number);
switch (preferred_type) {
case Value::PreferredType::Default:
case Value::PreferredType::Number: {
result = value_of();
if (result.is_object()) {
result = to_string();
}
break;
}
case Value::PreferredType::String: {
result = to_string();
if (result.is_object())
result = value_of();
break;
}
}
ASSERT(!result.is_object());
return result;
}
Value Object::to_string() const
{
auto& vm = this->vm(); auto& vm = this->vm();
auto to_string_property = get(vm.names.toString);
if (to_string_property.is_function()) { Vector<FlyString, 2> method_names;
auto& to_string_function = to_string_property.as_function(); if (preferred_type == Value::PreferredType::String)
auto to_string_result = vm.call(to_string_function, const_cast<Object*>(this)); method_names = { vm.names.toString, vm.names.valueOf };
if (to_string_result.is_object()) else
vm.throw_exception<TypeError>(global_object(), ErrorType::Convert, "object", "string"); method_names = { vm.names.valueOf, vm.names.toString };
for (auto& method_name : method_names) {
auto method = get(method_name);
if (vm.exception()) if (vm.exception())
return {}; return {};
auto* string = to_string_result.to_primitive_string(global_object()); if (method.is_function()) {
if (vm.exception()) auto result = vm.call(method.as_function(), const_cast<Object*>(this));
return {}; if (!result.is_object())
return string; return result;
}
} }
return js_string(vm, String::formatted("[object {}]", class_name())); vm.throw_exception<TypeError>(global_object(), ErrorType::Convert, "object", preferred_type == Value::PreferredType::String ? "string" : "number");
return {};
} }
Value Object::invoke(const StringOrSymbol& property_name, Optional<MarkedValueList> arguments) Value Object::invoke(const StringOrSymbol& property_name, Optional<MarkedValueList> arguments)

View file

@ -140,8 +140,7 @@ public:
virtual bool prevent_extensions(); virtual bool prevent_extensions();
virtual Value value_of() const { return Value(const_cast<Object*>(this)); } virtual Value value_of() const { return Value(const_cast<Object*>(this)); }
virtual Value to_primitive(Value::PreferredType preferred_type = Value::PreferredType::Default) const; virtual Value ordinary_to_primitive(Value::PreferredType preferred_type) const;
virtual Value to_string() const;
Value get_direct(size_t index) const { return m_storage[index]; } Value get_direct(size_t index) const { return m_storage[index]; }

View file

@ -168,7 +168,7 @@ String Value::to_string(GlobalObject& global_object) const
case Type::BigInt: case Type::BigInt:
return m_value.as_bigint->big_integer().to_base10(); return m_value.as_bigint->big_integer().to_base10();
case Type::Object: { case Type::Object: {
auto primitive_value = as_object().to_primitive(PreferredType::String); auto primitive_value = to_primitive(PreferredType::String);
if (global_object.vm().exception()) if (global_object.vm().exception())
return {}; return {};
return primitive_value.to_string(global_object); return primitive_value.to_string(global_object);
@ -205,8 +205,12 @@ bool Value::to_boolean() const
Value Value::to_primitive(PreferredType preferred_type) const Value Value::to_primitive(PreferredType preferred_type) const
{ {
if (is_object()) if (is_object()) {
return as_object().to_primitive(preferred_type); // FIXME: Also support @@toPrimitive
if (preferred_type == PreferredType::Default)
preferred_type = PreferredType::Number;
return as_object().ordinary_to_primitive(preferred_type);
}
return *this; return *this;
} }
@ -277,7 +281,7 @@ Value Value::to_number(GlobalObject& global_object) const
global_object.vm().throw_exception<TypeError>(global_object, ErrorType::Convert, "BigInt", "number"); global_object.vm().throw_exception<TypeError>(global_object, ErrorType::Convert, "BigInt", "number");
return {}; return {};
case Type::Object: { case Type::Object: {
auto primitive = m_value.as_object->to_primitive(PreferredType::Number); auto primitive = to_primitive(PreferredType::Number);
if (global_object.vm().exception()) if (global_object.vm().exception())
return {}; return {};
return primitive.to_number(global_object); return primitive.to_number(global_object);

View file

@ -0,0 +1,23 @@
test("object with custom toString", () => {
const o = { toString: () => "foo" };
expect(o + "bar").toBe("foobar");
expect([o, "bar"].toString()).toBe("foo,bar");
});
test("object with uncallable toString and custom valueOf", () => {
const o = { toString: undefined, valueOf: () => "foo" };
expect(o + "bar").toBe("foobar");
expect([o, "bar"].toString()).toBe("foo,bar");
});
test("object with custom valueOf", () => {
const o = { valueOf: () => 42 };
expect(Number(o)).toBe(42);
expect(o + 1).toBe(43);
});
test("object with uncallable valueOf and custom toString", () => {
const o = { valueOf: undefined, toString: () => "42" };
expect(Number(o)).toBe(42);
expect(o + 1).toBe("421");
});