LibJS: Make Errors fully spec compliant

The previous handling of the name and message properties specifically
was breaking websites that created their own error types and relied on
the error prototype working correctly - not assuming an JS::Error this
object, that is.

The way it works now, and it is supposed to work, is:

- Error.prototype.name and Error.prototype.message just have initial
  string values and are no longer getters/setters
- When constructing an error with a message, we create a regular
  property on the newly created object, so a lookup of the message
  property will either get it from the object directly or go though the
  prototype chain
- Internal m_name/m_message properties are no longer needed and removed

This makes printing errors slightly more complicated, as we can no
longer rely on the (safe) internal properties, and cannot trust a
property lookup either - get_without_side_effects() is used to solve
this, it's not perfect but something we can revisit later.

I did some refactoring along the way, there was some really old stuff in
there - accessing vm.call_frame().arguments[0] is not something we (have
to) do anymore :^)

Fixes #6245.
This commit is contained in:
Linus Groh 2021-04-12 00:08:28 +02:00 committed by Andreas Kling
parent 6e9eb0a284
commit da177c6517
Notes: sideshowbarker 2024-07-18 20:29:39 +09:00
18 changed files with 157 additions and 180 deletions

View File

@ -106,16 +106,15 @@ ConsoleWidget::ConsoleWidget()
}
if (m_interpreter->exception()) {
output_html.append("Uncaught exception: ");
auto error = m_interpreter->exception()->value();
if (error.is_object() && is<Web::Bindings::DOMExceptionWrapper>(error.as_object())) {
auto& dom_exception_wrapper = static_cast<Web::Bindings::DOMExceptionWrapper&>(error.as_object());
error = JS::Error::create(m_interpreter->global_object(), dom_exception_wrapper.impl().name(), dom_exception_wrapper.impl().message());
}
output_html.append(JS::MarkupGenerator::html_from_value(error));
print_html(output_html.string_view());
auto* exception = m_interpreter->exception();
m_interpreter->vm().clear_exception();
output_html.append("Uncaught exception: ");
auto error = exception->value();
if (error.is_object())
output_html.append(JS::MarkupGenerator::html_from_error(error.as_object()));
else
output_html.append(JS::MarkupGenerator::html_from_value(error));
print_html(output_html.string_view());
return;
}

View File

@ -121,7 +121,7 @@ SheetGlobalObject::~SheetGlobalObject()
{
}
JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receiver) const
JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receiver, bool without_side_effects) const
{
if (name.is_string()) {
if (name.as_string() == "value") {
@ -137,7 +137,7 @@ JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receive
}
}
return GlobalObject::get(name, receiver);
return GlobalObject::get(name, receiver, without_side_effects);
}
bool SheetGlobalObject::put(const JS::PropertyName& name, JS::Value value, JS::Value receiver)

View File

@ -46,7 +46,7 @@ public:
virtual ~SheetGlobalObject() override;
virtual JS::Value get(const JS::PropertyName&, JS::Value receiver = {}) const override;
virtual JS::Value get(const JS::PropertyName&, JS::Value receiver = {}, bool without_side_effects = false) const override;
virtual bool put(const JS::PropertyName&, JS::Value value, JS::Value receiver = {}) override;
virtual void initialize_global_object() override;

View File

@ -53,6 +53,14 @@ String MarkupGenerator::html_from_value(Value value)
return output_html.to_string();
}
String MarkupGenerator::html_from_error(Object& object)
{
StringBuilder output_html;
HashTable<Object*> seen_objects;
error_to_html(object, output_html, seen_objects);
return output_html.to_string();
}
void MarkupGenerator::value_to_html(Value value, StringBuilder& output_html, HashTable<Object*> seen_objects)
{
if (value.is_empty()) {
@ -156,10 +164,16 @@ void MarkupGenerator::date_to_html(const Object& date, StringBuilder& html_outpu
void MarkupGenerator::error_to_html(const Object& object, StringBuilder& html_output, HashTable<Object*>&)
{
auto& error = static_cast<const Error&>(object);
html_output.append(wrap_string_in_style(String::formatted("[{}]", error.name()), StyleType::Invalid));
if (!error.message().is_empty()) {
html_output.appendff(": {}", escape_html_entities(error.message()));
auto name = object.get_without_side_effects("name").value_or(JS::js_undefined());
auto message = object.get_without_side_effects("message").value_or(JS::js_undefined());
if (name.is_accessor() || name.is_native_property() || message.is_accessor() || message.is_native_property()) {
html_output.append(wrap_string_in_style(JS::Value(&object).to_string_without_side_effects(), StyleType::Invalid));
} else {
auto name_string = name.to_string_without_side_effects();
auto message_string = message.to_string_without_side_effects();
html_output.append(wrap_string_in_style(String::formatted("[{}]", name_string), StyleType::Invalid));
if (!message_string.is_empty())
html_output.appendff(": {}", escape_html_entities(message_string));
}
}

View File

@ -36,6 +36,7 @@ class MarkupGenerator {
public:
static String html_from_source(const StringView&);
static String html_from_value(Value);
static String html_from_error(Object&);
private:
enum class StyleType {

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Linus Groh <mail@linusgroh.de>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -29,32 +30,34 @@
namespace JS {
Error* Error::create(GlobalObject& global_object, const FlyString& name, const String& message)
Error* Error::create(GlobalObject& global_object, const String& message)
{
return global_object.heap().allocate<Error>(global_object, name, message, *global_object.error_prototype());
auto& vm = global_object.vm();
auto* error = global_object.heap().allocate<Error>(global_object, *global_object.error_prototype());
if (!message.is_null())
error->define_property(vm.names.message, js_string(vm, message));
return error;
}
Error::Error(const FlyString& name, const String& message, Object& prototype)
Error::Error(Object& prototype)
: Object(prototype)
, m_name(name)
, m_message(message)
{
}
Error::~Error()
{
}
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
ClassName* ClassName::create(GlobalObject& global_object, const String& message) \
{ \
return global_object.heap().allocate<ClassName>(global_object, message, *global_object.snake_name##_prototype()); \
} \
ClassName::ClassName(const String& message, Object& prototype) \
: Error(vm().names.ClassName, message, prototype) \
{ \
} \
ClassName::~ClassName() { }
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
ClassName* ClassName::create(GlobalObject& global_object, const String& message) \
{ \
auto& vm = global_object.vm(); \
auto* error = global_object.heap().allocate<ClassName>(global_object, *global_object.snake_name##_prototype()); \
if (!message.is_null()) \
error->define_property(vm.names.message, js_string(vm, message)); \
return error; \
} \
\
ClassName::ClassName(Object& prototype) \
: Error(prototype) \
{ \
}
JS_ENUMERATE_ERROR_SUBCLASSES
#undef __JS_ENUMERATE

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Linus Groh <mail@linusgroh.de>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -35,19 +36,10 @@ class Error : public Object {
JS_OBJECT(Error, Object);
public:
static Error* create(GlobalObject&, const FlyString& name, const String& message);
static Error* create(GlobalObject&, const String& message = {});
Error(const FlyString& name, const String& message, Object& prototype);
virtual ~Error() override;
const FlyString& name() const { return m_name; }
const String& message() const { return m_message; }
void set_name(const FlyString& name) { m_name = name; }
private:
FlyString m_name;
String m_message;
explicit Error(Object& prototype);
virtual ~Error() override = default;
};
#define DECLARE_ERROR_SUBCLASS(ClassName, snake_name, PrototypeName, ConstructorName) \
@ -55,10 +47,10 @@ private:
JS_OBJECT(ClassName, Error); \
\
public: \
static ClassName* create(GlobalObject&, const String& message); \
static ClassName* create(GlobalObject&, const String& message = {}); \
\
ClassName(const String& message, Object& prototype); \
virtual ~ClassName() override; \
explicit ClassName(Object& prototype); \
virtual ~ClassName() override = default; \
};
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, Linus Groh <mail@linusgroh.de>
* Copyright (c) 2020-2021, Linus Groh <mail@linusgroh.de>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -43,10 +43,6 @@ void ErrorConstructor::initialize(GlobalObject& global_object)
define_property(vm.names.length, Value(1), Attribute::Configurable);
}
ErrorConstructor::~ErrorConstructor()
{
}
Value ErrorConstructor::call()
{
return construct(*this);
@ -55,41 +51,46 @@ Value ErrorConstructor::call()
Value ErrorConstructor::construct(Function&)
{
auto& vm = this->vm();
String message = "";
if (!vm.call_frame().arguments.is_empty() && !vm.call_frame().arguments[0].is_undefined()) {
message = vm.call_frame().arguments[0].to_string(global_object());
String message;
if (!vm.argument(0).is_undefined()) {
message = vm.argument(0).to_string(global_object());
if (vm.exception())
return {};
}
return Error::create(global_object(), vm.names.Error, message);
return Error::create(global_object(), message);
}
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
ConstructorName::ConstructorName(GlobalObject& global_object) \
: NativeFunction(*global_object.function_prototype()) \
{ \
} \
void ConstructorName::initialize(GlobalObject& global_object) \
{ \
auto& vm = this->vm(); \
NativeFunction::initialize(global_object); \
define_property(vm.names.prototype, global_object.snake_name##_prototype(), 0); \
define_property(vm.names.length, Value(1), Attribute::Configurable); \
} \
ConstructorName::~ConstructorName() { } \
Value ConstructorName::call() \
{ \
return construct(*this); \
} \
Value ConstructorName::construct(Function&) \
{ \
String message = ""; \
if (!vm().call_frame().arguments.is_empty() && !vm().call_frame().arguments[0].is_undefined()) { \
message = vm().call_frame().arguments[0].to_string(global_object()); \
if (vm().exception()) \
return {}; \
} \
return ClassName::create(global_object(), message); \
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
ConstructorName::ConstructorName(GlobalObject& global_object) \
: NativeFunction(*global_object.function_prototype()) \
{ \
} \
\
void ConstructorName::initialize(GlobalObject& global_object) \
{ \
auto& vm = this->vm(); \
NativeFunction::initialize(global_object); \
define_property(vm.names.prototype, global_object.snake_name##_prototype(), 0); \
define_property(vm.names.length, Value(1), Attribute::Configurable); \
} \
\
ConstructorName::~ConstructorName() { } \
\
Value ConstructorName::call() \
{ \
return construct(*this); \
} \
\
Value ConstructorName::construct(Function&) \
{ \
auto& vm = this->vm(); \
String message = ""; \
if (!vm.argument(0).is_undefined()) { \
message = vm.argument(0).to_string(global_object()); \
if (vm.exception()) \
return {}; \
} \
return ClassName::create(global_object(), message); \
}
JS_ENUMERATE_ERROR_SUBCLASSES

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, Linus Groh <mail@linusgroh.de>
* Copyright (c) 2020-2021, Linus Groh <mail@linusgroh.de>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -37,7 +37,7 @@ class ErrorConstructor final : public NativeFunction {
public:
explicit ErrorConstructor(GlobalObject&);
virtual void initialize(GlobalObject&) override;
virtual ~ErrorConstructor() override;
virtual ~ErrorConstructor() override = default;
virtual Value call() override;
virtual Value construct(Function& new_target) override;

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Linus Groh <mail@linusgroh.de>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -25,7 +26,6 @@
*/
#include <AK/Function.h>
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/ErrorPrototype.h>
#include <LibJS/Runtime/GlobalObject.h>
@ -44,85 +44,44 @@ void ErrorPrototype::initialize(GlobalObject& global_object)
auto& vm = this->vm();
Object::initialize(global_object);
u8 attr = Attribute::Writable | Attribute::Configurable;
define_native_property(vm.names.name, name_getter, name_setter, attr);
define_native_property(vm.names.message, message_getter, {}, attr);
define_property(vm.names.name, js_string(vm, "Error"), attr);
define_property(vm.names.message, js_string(vm, ""), attr);
define_native_function(vm.names.toString, to_string, 0, attr);
}
ErrorPrototype::~ErrorPrototype()
{
}
JS_DEFINE_NATIVE_GETTER(ErrorPrototype::name_getter)
{
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return {};
if (!is<Error>(this_object)) {
vm.throw_exception<TypeError>(global_object, ErrorType::NotAn, "Error");
return {};
}
return js_string(vm, static_cast<const Error*>(this_object)->name());
}
JS_DEFINE_NATIVE_SETTER(ErrorPrototype::name_setter)
{
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return;
if (!is<Error>(this_object)) {
vm.throw_exception<TypeError>(global_object, ErrorType::NotAn, "Error");
return;
}
auto name = value.to_string(global_object);
if (vm.exception())
return;
static_cast<Error*>(this_object)->set_name(name);
}
JS_DEFINE_NATIVE_GETTER(ErrorPrototype::message_getter)
{
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return {};
if (!is<Error>(this_object)) {
vm.throw_exception<TypeError>(global_object, ErrorType::NotAn, "Error");
return {};
}
return js_string(vm, static_cast<const Error*>(this_object)->message());
}
// 20.5.3.4 Error.prototype.toString, https://tc39.es/ecma262/#sec-error.prototype.tostring
JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::to_string)
{
if (!vm.this_value(global_object).is_object()) {
vm.throw_exception<TypeError>(global_object, ErrorType::NotAnObject, vm.this_value(global_object).to_string_without_side_effects());
auto this_value = vm.this_value(global_object);
if (!this_value.is_object()) {
vm.throw_exception<TypeError>(global_object, ErrorType::NotAnObject, this_value.to_string_without_side_effects());
return {};
}
auto& this_object = vm.this_value(global_object).as_object();
auto& this_object = this_value.as_object();
String name = "Error";
auto name_property = this_object.get(vm.names.name);
auto name_property = this_object.get(vm.names.name).value_or(js_undefined());
if (vm.exception())
return {};
if (!name_property.is_empty() && !name_property.is_undefined()) {
if (!name_property.is_undefined()) {
name = name_property.to_string(global_object);
if (vm.exception())
return {};
}
String message = "";
auto message_property = this_object.get(vm.names.message);
auto message_property = this_object.get(vm.names.message).value_or(js_undefined());
if (vm.exception())
return {};
if (!message_property.is_empty() && !message_property.is_undefined()) {
if (!message_property.is_undefined()) {
message = message_property.to_string(global_object);
if (vm.exception())
return {};
}
if (name.length() == 0)
if (name.is_empty())
return js_string(vm, message);
if (message.length() == 0)
if (message.is_empty())
return js_string(vm, name);
return js_string(vm, String::formatted("{}: {}", name, message));
}
@ -131,8 +90,7 @@ JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::to_string)
PrototypeName::PrototypeName(GlobalObject& global_object) \
: Object(*global_object.error_prototype()) \
{ \
} \
PrototypeName::~PrototypeName() { }
}
JS_ENUMERATE_ERROR_SUBCLASSES
#undef __JS_ENUMERATE

View File

@ -36,15 +36,10 @@ class ErrorPrototype final : public Object {
public:
explicit ErrorPrototype(GlobalObject&);
virtual void initialize(GlobalObject&) override;
virtual ~ErrorPrototype() override;
virtual ~ErrorPrototype() override = default;
private:
JS_DECLARE_NATIVE_FUNCTION(to_string);
JS_DECLARE_NATIVE_GETTER(name_getter);
JS_DECLARE_NATIVE_SETTER(name_setter);
JS_DECLARE_NATIVE_GETTER(message_getter);
};
#define DECLARE_ERROR_SUBCLASS_PROTOTYPE(ClassName, snake_name, PrototypeName, ConstructorName) \
@ -54,7 +49,7 @@ private:
public: \
explicit PrototypeName(GlobalObject&); \
virtual void initialize(GlobalObject&) override { } \
virtual ~PrototypeName() override; \
virtual ~PrototypeName() override = default; \
};
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \

View File

@ -294,9 +294,16 @@ void VM::throw_exception(Exception* exception)
{
if (should_log_exceptions()) {
auto value = exception->value();
if (value.is_object() && is<Error>(value.as_object())) {
auto& error = static_cast<Error&>(value.as_object());
dbgln("Throwing JavaScript exception: [{}] {}", error.name(), error.message());
if (value.is_object()) {
auto& object = value.as_object();
auto name = object.get_without_side_effects(names.name).value_or(js_undefined());
auto message = object.get_without_side_effects(names.message).value_or(js_undefined());
if (name.is_accessor() || name.is_native_property() || message.is_accessor() || message.is_native_property()) {
// The result is not going to be useful, let's just print the value. This affects DOMExceptions, for example.
dbgln("Throwing JavaScript exception: {}", value);
} else {
dbgln("Throwing JavaScript exception: [{}] {}", name, message);
}
} else {
dbgln("Throwing JavaScript exception: {}", value);
}

View File

@ -124,7 +124,7 @@ public:
// Ensure we got some stack space left, so the next function call doesn't kill us.
// This value is merely a guess and might need tweaking at a later point.
if (m_stack_info.size_free() < 16 * KiB)
throw_exception<Error>(global_object, "RuntimeError", "Call stack size limit exceeded");
throw_exception<Error>(global_object, "Call stack size limit exceeded");
else
m_call_stack.append(&call_frame);
}

View File

@ -7,7 +7,7 @@ test("infinite recursion", () => {
infiniteRecursion();
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e.name).toBe("RuntimeError");
expect(e.name).toBe("Error");
expect(e.message).toBe("Call stack size limit exceeded");
}

View File

@ -31,12 +31,12 @@
namespace Web::Bindings {
JS::Value CSSStyleDeclarationWrapper::get(const JS::PropertyName& name, JS::Value receiver) const
JS::Value CSSStyleDeclarationWrapper::get(const JS::PropertyName& name, JS::Value receiver, bool without_side_effects) const
{
// FIXME: These should actually use camelCase versions of the property names!
auto property_id = CSS::property_id_from_string(name.to_string());
if (property_id == CSS::PropertyID::Invalid)
return Base::get(name, receiver);
return Base::get(name, receiver, without_side_effects);
for (auto& property : impl().properties()) {
if (property.property_id == property_id)
return js_string(vm(), property.value->to_string());

View File

@ -761,7 +761,7 @@ public:
if (interface.extended_attributes.contains("CustomGet")) {
generator.append(R"~~~(
virtual JS::Value get(const JS::PropertyName&, JS::Value receiver = {}) const override;
virtual JS::Value get(const JS::PropertyName&, JS::Value receiver = {}, bool without_side_effects = false) const override;
)~~~");
}
if (interface.extended_attributes.contains("CustomPut")) {

View File

@ -54,16 +54,15 @@ void WebContentConsoleClient::handle_input(const String& js_source)
}
if (m_interpreter->exception()) {
output_html.append("Uncaught exception: ");
auto error = m_interpreter->exception()->value();
if (error.is_object() && is<Web::Bindings::DOMExceptionWrapper>(error.as_object())) {
auto& dom_exception_wrapper = static_cast<Web::Bindings::DOMExceptionWrapper&>(error.as_object());
error = JS::Error::create(m_interpreter->global_object(), dom_exception_wrapper.impl().name(), dom_exception_wrapper.impl().message());
}
output_html.append(JS::MarkupGenerator::html_from_value(error));
print_html(output_html.string_view());
auto* exception = m_interpreter->exception();
m_interpreter->vm().clear_exception();
output_html.append("Uncaught exception: ");
auto error = exception->value();
if (error.is_object())
output_html.append(JS::MarkupGenerator::html_from_error(error.as_object()));
else
output_html.append(JS::MarkupGenerator::html_from_value(error));
print_html(output_html.string_view());
return;
}

View File

@ -239,18 +239,25 @@ static void print_function(const JS::Object& object, HashTable<JS::Object*>&)
out(" {}", static_cast<const JS::NativeFunction&>(object).name());
}
static void print_date(const JS::Object& date, HashTable<JS::Object*>&)
static void print_date(const JS::Object& object, HashTable<JS::Object*>&)
{
print_type("Date");
out(" \033[34;1m{}\033[0m", static_cast<const JS::Date&>(date).string());
out(" \033[34;1m{}\033[0m", static_cast<const JS::Date&>(object).string());
}
static void print_error(const JS::Object& object, HashTable<JS::Object*>&)
static void print_error(const JS::Object& object, HashTable<JS::Object*>& seen_objects)
{
auto& error = static_cast<const JS::Error&>(object);
print_type(error.name());
if (!error.message().is_empty())
out(" \033[31;1m{}\033[0m", error.message());
auto name = object.get_without_side_effects(vm->names.name).value_or(JS::js_undefined());
auto message = object.get_without_side_effects(vm->names.message).value_or(JS::js_undefined());
if (name.is_accessor() || name.is_native_property() || message.is_accessor() || message.is_native_property()) {
print_value(&object, seen_objects);
} else {
auto name_string = name.to_string_without_side_effects();
auto message_string = message.to_string_without_side_effects();
print_type(name_string);
if (!message_string.is_empty())
out(" \033[31;1m{}\033[0m", message_string);
}
}
static void print_regexp_object(const JS::Object& object, HashTable<JS::Object*>&)
@ -498,9 +505,11 @@ static bool parse_and_run(JS::Interpreter& interpreter, const StringView& source
}
auto handle_exception = [&] {
auto* exception = vm->exception();
vm->clear_exception();
out("Uncaught exception: ");
print(vm->exception()->value());
auto trace = vm->exception()->trace();
print(exception->value());
auto& trace = exception->trace();
if (trace.size() > 1) {
unsigned repetitions = 0;
for (size_t i = 0; i < trace.size(); ++i) {
@ -522,7 +531,6 @@ static bool parse_and_run(JS::Interpreter& interpreter, const StringView& source
repetitions = 0;
}
}
vm->clear_exception();
};
if (vm->exception()) {
@ -532,8 +540,8 @@ static bool parse_and_run(JS::Interpreter& interpreter, const StringView& source
if (s_print_last_result)
print(vm->last_value());
if (vm->exception()) {
return false;
handle_exception();
return false;
}
return true;
}
@ -738,7 +746,7 @@ int main(int argc, char** argv)
OwnPtr<JS::Interpreter> interpreter;
interrupt_interpreter = [&] {
auto error = JS::Error::create(interpreter->global_object(), "Error", "Received SIGINT");
auto error = JS::Error::create(interpreter->global_object(), "Received SIGINT");
vm->throw_exception(interpreter->global_object(), error);
};