LibJS+LibWeb: Make DOMException hold an [[ErrorData]] slot
Split JS::ErrorData out of JS::Error so that it can be used both by JS::Error and WebIDL::DOMException. This adds support for Error.isError to DOMException, also letting us report DOMException stack information to the console.
This commit is contained in:
parent
bdd9c98d44
commit
57130908b3
24 changed files with 429 additions and 152 deletions
|
|
@ -79,6 +79,7 @@ set(SOURCES
|
|||
Runtime/Environment.cpp
|
||||
Runtime/Error.cpp
|
||||
Runtime/ErrorConstructor.cpp
|
||||
Runtime/ErrorData.cpp
|
||||
Runtime/ErrorPrototype.cpp
|
||||
Runtime/ErrorTypes.cpp
|
||||
Runtime/ExecutionContext.cpp
|
||||
|
|
|
|||
|
|
@ -753,10 +753,10 @@ void Console::output_debug_message(LogLevel log_level, StringView output) const
|
|||
}
|
||||
}
|
||||
|
||||
void Console::report_exception(JS::Error const& exception, bool in_promise) const
|
||||
void Console::report_exception(String const& name, String const& message, JS::ErrorData const& error_data, bool in_promise) const
|
||||
{
|
||||
if (m_client)
|
||||
m_client->report_exception(exception, in_promise);
|
||||
m_client->report_exception(name, message, error_data, in_promise);
|
||||
}
|
||||
|
||||
ThrowCompletionOr<String> Console::value_vector_to_string(GC::RootVector<Value> const& values)
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ public:
|
|||
ThrowCompletionOr<Value> time_end();
|
||||
|
||||
void output_debug_message(LogLevel log_level, StringView output) const;
|
||||
void report_exception(JS::Error const&, bool) const;
|
||||
void report_exception(String const& name, String const& message, JS::ErrorData const&, bool) const;
|
||||
|
||||
private:
|
||||
explicit Console(Realm&);
|
||||
|
|
@ -126,7 +126,7 @@ public:
|
|||
virtual ThrowCompletionOr<Value> printer(Console::LogLevel log_level, PrinterArguments) = 0;
|
||||
|
||||
virtual void add_css_style_to_current_message(StringView) { }
|
||||
virtual void report_exception(JS::Error const&, bool) { }
|
||||
virtual void report_exception(String const&, String const&, JS::ErrorData const&, bool) { }
|
||||
|
||||
virtual void clear() = 0;
|
||||
virtual void end_group() = 0;
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ struct DisposableResource;
|
|||
class ECMAScriptFunctionObject;
|
||||
class Environment;
|
||||
class Error;
|
||||
class ErrorData;
|
||||
class ErrorType;
|
||||
struct ExecutionContext;
|
||||
struct ExportEntry;
|
||||
|
|
|
|||
|
|
@ -5,27 +5,14 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibJS/Runtime/Completion.h>
|
||||
#include <LibJS/Runtime/Error.h>
|
||||
#include <LibJS/Runtime/ExecutionContext.h>
|
||||
#include <LibJS/Runtime/FunctionObject.h>
|
||||
#include <LibJS/Runtime/GlobalObject.h>
|
||||
#include <LibJS/SourceRange.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(Error);
|
||||
|
||||
static SourceRange dummy_source_range { SourceCode::create({}, {}), {}, {} };
|
||||
|
||||
SourceRange const& TracebackFrame::source_range() const
|
||||
{
|
||||
if (!cached_source_range.has_value())
|
||||
return dummy_source_range;
|
||||
return *cached_source_range;
|
||||
}
|
||||
|
||||
GC::Ref<Error> Error::create(Realm& realm)
|
||||
{
|
||||
return realm.create<Error>(realm.intrinsics().error_prototype());
|
||||
|
|
@ -43,16 +30,21 @@ GC::Ref<Error> Error::create(Realm& realm, StringView message)
|
|||
return create(realm, Utf16String::from_utf8(message));
|
||||
}
|
||||
|
||||
Utf16String Error::stack_string(CompactTraceback compact) const
|
||||
{
|
||||
return ErrorData::stack_string(compact);
|
||||
}
|
||||
|
||||
Error::Error(Object& prototype)
|
||||
: Object(ConstructWithPrototypeTag::Tag, prototype)
|
||||
, ErrorData(prototype.vm())
|
||||
{
|
||||
populate_stack();
|
||||
}
|
||||
|
||||
void Error::visit_edges(Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_cached_string);
|
||||
ErrorData::visit_edges(visitor);
|
||||
}
|
||||
|
||||
// 20.5.8.1 InstallErrorCause ( O, options ), https://tc39.es/ecma262/#sec-installerrorcause
|
||||
|
|
@ -81,83 +73,6 @@ void Error::set_message(Utf16String message)
|
|||
define_direct_property(vm.names.message, PrimitiveString::create(vm, move(message)), attr);
|
||||
}
|
||||
|
||||
void Error::populate_stack()
|
||||
{
|
||||
auto stack_trace = vm().stack_trace();
|
||||
m_traceback.ensure_capacity(stack_trace.size());
|
||||
for (auto& element : stack_trace) {
|
||||
auto* context = element.execution_context;
|
||||
m_traceback.append({
|
||||
.function_name = context->function ? context->function->name_for_call_stack() : ""_utf16,
|
||||
.cached_source_range = move(element.source_range),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Utf16String Error::stack_string(CompactTraceback compact) const
|
||||
{
|
||||
if (m_traceback.is_empty())
|
||||
return {};
|
||||
|
||||
StringBuilder stack_string_builder(StringBuilder::Mode::UTF16);
|
||||
|
||||
// Note: We roughly follow V8's formatting
|
||||
auto append_frame = [&](TracebackFrame const& frame) {
|
||||
auto const& function_name = frame.function_name;
|
||||
auto const& source_range = frame.source_range();
|
||||
// Note: Since we don't know whether we have a valid SourceRange here we just check for some default values.
|
||||
if (!source_range.filename().is_empty() || source_range.start.offset != 0 || source_range.end.offset != 0) {
|
||||
|
||||
if (function_name.is_empty())
|
||||
stack_string_builder.appendff(" at {}:{}:{}\n", source_range.filename(), source_range.start.line, source_range.start.column);
|
||||
else
|
||||
stack_string_builder.appendff(" at {} ({}:{}:{})\n", function_name, source_range.filename(), source_range.start.line, source_range.start.column);
|
||||
} else {
|
||||
stack_string_builder.appendff(" at {}\n", function_name.is_empty() ? "<unknown>"_utf16 : function_name);
|
||||
}
|
||||
};
|
||||
|
||||
auto is_same_frame = [](TracebackFrame const& a, TracebackFrame const& b) {
|
||||
if (a.function_name.is_empty() && b.function_name.is_empty()) {
|
||||
auto const& source_range_a = a.source_range();
|
||||
auto const& source_range_b = b.source_range();
|
||||
return source_range_a.filename() == source_range_b.filename() && source_range_a.start.line == source_range_b.start.line;
|
||||
}
|
||||
return a.function_name == b.function_name;
|
||||
};
|
||||
|
||||
// Note: We don't want to capture the global execution context, so we omit the last frame
|
||||
// Note: The error's name and message get prepended by ErrorPrototype::stack
|
||||
// FIXME: We generate a stack-frame for the Errors constructor, other engines do not
|
||||
unsigned repetitions = 0;
|
||||
size_t used_frames = m_traceback.size() - 1;
|
||||
for (size_t i = 0; i < used_frames; ++i) {
|
||||
auto const& frame = m_traceback[i];
|
||||
if (compact == CompactTraceback::Yes && i + 1 < used_frames) {
|
||||
auto const& next_traceback_frame = m_traceback[i + 1];
|
||||
if (is_same_frame(frame, next_traceback_frame)) {
|
||||
repetitions++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (repetitions > 4) {
|
||||
// If more than 5 (1 + >4) consecutive function calls with the same name, print
|
||||
// the name only once and show the number of repetitions instead. This prevents
|
||||
// printing ridiculously large call stacks of recursive functions.
|
||||
append_frame(frame);
|
||||
stack_string_builder.appendff(" {} more calls\n", repetitions);
|
||||
} else {
|
||||
for (size_t j = 0; j < repetitions + 1; j++)
|
||||
append_frame(frame);
|
||||
}
|
||||
repetitions = 0;
|
||||
}
|
||||
for (size_t j = 0; j < repetitions; j++)
|
||||
append_frame(m_traceback[used_frames - 1]);
|
||||
|
||||
return stack_string_builder.to_utf16_string();
|
||||
}
|
||||
|
||||
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
|
||||
GC_DEFINE_ALLOCATOR(ClassName); \
|
||||
GC::Ref<ClassName> ClassName::create(Realm& realm) \
|
||||
|
|
|
|||
|
|
@ -12,24 +12,14 @@
|
|||
#include <AK/Utf16String.h>
|
||||
#include <LibJS/Export.h>
|
||||
#include <LibJS/Runtime/Completion.h>
|
||||
#include <LibJS/Runtime/ErrorData.h>
|
||||
#include <LibJS/Runtime/Object.h>
|
||||
#include <LibJS/SourceRange.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
struct JS_API TracebackFrame {
|
||||
Utf16String function_name;
|
||||
[[nodiscard]] SourceRange const& source_range() const;
|
||||
|
||||
Optional<SourceRange> cached_source_range;
|
||||
};
|
||||
|
||||
enum CompactTraceback {
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
|
||||
class JS_API Error : public Object {
|
||||
class JS_API Error
|
||||
: public Object
|
||||
, public ErrorData {
|
||||
JS_OBJECT(Error, Object);
|
||||
GC_DECLARE_ALLOCATOR(Error);
|
||||
|
||||
|
|
@ -46,11 +36,6 @@ public:
|
|||
|
||||
void set_message(Utf16String);
|
||||
|
||||
Vector<TracebackFrame, 32> const& traceback() const { return m_traceback; }
|
||||
|
||||
void set_cached_string(GC::Ref<PrimitiveString> string) { m_cached_string = string; }
|
||||
GC::Ptr<PrimitiveString> cached_string() const { return m_cached_string; }
|
||||
|
||||
protected:
|
||||
explicit Error(Object& prototype);
|
||||
|
||||
|
|
@ -58,11 +43,8 @@ protected:
|
|||
|
||||
private:
|
||||
virtual bool is_error_object() const final { return true; }
|
||||
|
||||
void populate_stack();
|
||||
Vector<TracebackFrame, 32> m_traceback;
|
||||
|
||||
GC::Ptr<PrimitiveString> m_cached_string;
|
||||
virtual ErrorData* error_data() final { return this; }
|
||||
virtual ErrorData const* error_data() const final { return this; }
|
||||
};
|
||||
|
||||
template<>
|
||||
|
|
|
|||
|
|
@ -127,9 +127,16 @@ JS_ENUMERATE_NATIVE_ERRORS
|
|||
JS_DEFINE_NATIVE_FUNCTION(ErrorConstructor::is_error)
|
||||
{
|
||||
// 1. If arg is not an Object, return false.
|
||||
auto object = vm.argument(0).as_if<Object>();
|
||||
if (!object)
|
||||
return false;
|
||||
|
||||
// 2. If arg does not have an [[ErrorData]] internal slot, return false.
|
||||
if (!object->has_error_data())
|
||||
return false;
|
||||
|
||||
// 3. Return true.
|
||||
return vm.argument(0).is<Error>();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
112
Libraries/LibJS/Runtime/ErrorData.cpp
Normal file
112
Libraries/LibJS/Runtime/ErrorData.cpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2025, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibJS/Runtime/ErrorData.h>
|
||||
#include <LibJS/Runtime/ExecutionContext.h>
|
||||
#include <LibJS/Runtime/FunctionObject.h>
|
||||
#include <LibJS/Runtime/PrimitiveString.h>
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
static SourceRange dummy_source_range { SourceCode::create({}, {}), {}, {} };
|
||||
|
||||
SourceRange const& TracebackFrame::source_range() const
|
||||
{
|
||||
if (!cached_source_range.has_value())
|
||||
return dummy_source_range;
|
||||
return *cached_source_range;
|
||||
}
|
||||
|
||||
ErrorData::ErrorData(VM& vm)
|
||||
{
|
||||
populate_stack(vm);
|
||||
}
|
||||
|
||||
void ErrorData::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
visitor.visit(m_cached_string);
|
||||
}
|
||||
|
||||
void ErrorData::populate_stack(VM& vm)
|
||||
{
|
||||
auto stack_trace = vm.stack_trace();
|
||||
m_traceback.ensure_capacity(stack_trace.size());
|
||||
for (auto& element : stack_trace) {
|
||||
auto* context = element.execution_context;
|
||||
m_traceback.append({
|
||||
.function_name = context->function ? context->function->name_for_call_stack() : ""_utf16,
|
||||
.cached_source_range = move(element.source_range),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Utf16String ErrorData::stack_string(CompactTraceback compact) const
|
||||
{
|
||||
if (m_traceback.is_empty())
|
||||
return {};
|
||||
|
||||
StringBuilder stack_string_builder(StringBuilder::Mode::UTF16);
|
||||
|
||||
// Note: We roughly follow V8's formatting
|
||||
auto append_frame = [&](TracebackFrame const& frame) {
|
||||
auto const& function_name = frame.function_name;
|
||||
auto const& source_range = frame.source_range();
|
||||
// Note: Since we don't know whether we have a valid SourceRange here we just check for some default values.
|
||||
if (!source_range.filename().is_empty() || source_range.start.offset != 0 || source_range.end.offset != 0) {
|
||||
|
||||
if (function_name.is_empty())
|
||||
stack_string_builder.appendff(" at {}:{}:{}\n", source_range.filename(), source_range.start.line, source_range.start.column);
|
||||
else
|
||||
stack_string_builder.appendff(" at {} ({}:{}:{})\n", function_name, source_range.filename(), source_range.start.line, source_range.start.column);
|
||||
} else {
|
||||
stack_string_builder.appendff(" at {}\n", function_name.is_empty() ? "<unknown>"_utf16 : function_name);
|
||||
}
|
||||
};
|
||||
|
||||
auto is_same_frame = [](TracebackFrame const& a, TracebackFrame const& b) {
|
||||
if (a.function_name.is_empty() && b.function_name.is_empty()) {
|
||||
auto const& source_range_a = a.source_range();
|
||||
auto const& source_range_b = b.source_range();
|
||||
return source_range_a.filename() == source_range_b.filename() && source_range_a.start.line == source_range_b.start.line;
|
||||
}
|
||||
return a.function_name == b.function_name;
|
||||
};
|
||||
|
||||
// Note: We don't want to capture the global execution context, so we omit the last frame
|
||||
// Note: The error's name and message get prepended by Error.prototype.stack
|
||||
unsigned repetitions = 0;
|
||||
size_t used_frames = m_traceback.size() - 1;
|
||||
for (size_t i = 0; i < used_frames; ++i) {
|
||||
auto const& frame = m_traceback[i];
|
||||
if (compact == CompactTraceback::Yes && i + 1 < used_frames) {
|
||||
auto const& next_traceback_frame = m_traceback[i + 1];
|
||||
if (is_same_frame(frame, next_traceback_frame)) {
|
||||
repetitions++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (repetitions > 4) {
|
||||
// If more than 5 (1 + >4) consecutive function calls with the same name, print
|
||||
// the name only once and show the number of repetitions instead. This prevents
|
||||
// printing ridiculously large call stacks of recursive functions.
|
||||
append_frame(frame);
|
||||
stack_string_builder.appendff(" {} more calls\n", repetitions);
|
||||
} else {
|
||||
for (size_t j = 0; j < repetitions + 1; j++)
|
||||
append_frame(frame);
|
||||
}
|
||||
repetitions = 0;
|
||||
}
|
||||
for (size_t j = 0; j < repetitions; j++)
|
||||
append_frame(m_traceback[used_frames - 1]);
|
||||
|
||||
return stack_string_builder.to_utf16_string();
|
||||
}
|
||||
|
||||
}
|
||||
53
Libraries/LibJS/Runtime/ErrorData.h
Normal file
53
Libraries/LibJS/Runtime/ErrorData.h
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <AK/Utf16String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGC/Ptr.h>
|
||||
#include <LibJS/Export.h>
|
||||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/Cell.h>
|
||||
#include <LibJS/SourceRange.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
struct JS_API TracebackFrame {
|
||||
Utf16String function_name;
|
||||
[[nodiscard]] SourceRange const& source_range() const;
|
||||
|
||||
Optional<SourceRange> cached_source_range;
|
||||
};
|
||||
|
||||
enum CompactTraceback {
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
|
||||
class JS_API ErrorData {
|
||||
public:
|
||||
explicit ErrorData(VM&);
|
||||
|
||||
[[nodiscard]] Utf16String stack_string(CompactTraceback compact = CompactTraceback::No) const;
|
||||
[[nodiscard]] Vector<TracebackFrame, 32> const& traceback() const { return m_traceback; }
|
||||
|
||||
void set_cached_string(GC::Ref<PrimitiveString> string) { m_cached_string = string; }
|
||||
[[nodiscard]] GC::Ptr<PrimitiveString> cached_string() const { return m_cached_string; }
|
||||
|
||||
protected:
|
||||
void visit_edges(Cell::Visitor&);
|
||||
|
||||
private:
|
||||
void populate_stack(VM&);
|
||||
|
||||
Vector<TracebackFrame, 32> m_traceback;
|
||||
GC::Ptr<PrimitiveString> m_cached_string;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -78,35 +78,35 @@ JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::stack_getter)
|
|||
auto this_object = TRY(PrototypeObject::this_object(vm));
|
||||
|
||||
// 3. If E does not have an [[ErrorData]] internal slot, return undefined.
|
||||
auto* error = as_if<Error>(*this_object);
|
||||
if (!error)
|
||||
auto* error_data = this_object->error_data();
|
||||
if (!error_data)
|
||||
return js_undefined();
|
||||
|
||||
// OPTIMIZATION: Avoid recomputing the stack string if we already have it cached.
|
||||
// At least one major engine does this as well, so it's not expected that changing
|
||||
// the name or message properties updates the stack string.
|
||||
if (error->cached_string())
|
||||
return error->cached_string();
|
||||
if (error_data->cached_string())
|
||||
return error_data->cached_string();
|
||||
|
||||
// 4. Return ? GetStackString(error).
|
||||
// NOTE: These steps are not implemented based on the proposal, but to roughly follow behavior of other browsers.
|
||||
|
||||
String name {};
|
||||
if (auto name_property = TRY(error->get(vm.names.name)); !name_property.is_undefined())
|
||||
if (auto name_property = TRY(this_object->get(vm.names.name)); !name_property.is_undefined())
|
||||
name = TRY(name_property.to_string(vm));
|
||||
else
|
||||
name = "Error"_string;
|
||||
|
||||
Utf16String message {};
|
||||
if (auto message_property = TRY(error->get(vm.names.message)); !message_property.is_undefined())
|
||||
if (auto message_property = TRY(this_object->get(vm.names.message)); !message_property.is_undefined())
|
||||
message = TRY(message_property.to_utf16_string(vm));
|
||||
|
||||
auto header = message.is_empty()
|
||||
? move(name)
|
||||
: MUST(String::formatted("{}: {}", name, message));
|
||||
|
||||
auto string = PrimitiveString::create(vm, Utf16String::formatted("{}\n{}", header, error->stack_string()));
|
||||
error->set_cached_string(string);
|
||||
auto string = PrimitiveString::create(vm, Utf16String::formatted("{}\n{}", header, error_data->stack_string()));
|
||||
error_data->set_cached_string(string);
|
||||
return string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,9 @@ public:
|
|||
virtual bool is_set_object() const { return false; }
|
||||
virtual bool is_map_object() const { return false; }
|
||||
virtual bool is_weak_map() const { return false; }
|
||||
virtual ErrorData* error_data() { return nullptr; }
|
||||
virtual ErrorData const* error_data() const { return nullptr; }
|
||||
bool has_error_data() const { return error_data(); }
|
||||
|
||||
virtual bool is_typed_array_base() const { return false; }
|
||||
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, Type) \
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ JS_DEFINE_NATIVE_FUNCTION(ObjectPrototype::to_string)
|
|||
else if (object->is_function())
|
||||
builtin_tag = "Function"sv;
|
||||
// 8. Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
|
||||
else if (is<Error>(*object))
|
||||
else if (object->has_error_data())
|
||||
builtin_tag = "Error"sv;
|
||||
// 9. Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
|
||||
else if (is<BooleanObject>(*object))
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ ErrorInformation extract_error_information(JS::VM& vm, JS::Value exception)
|
|||
// to the entire source document! Calculate that somehow.
|
||||
|
||||
// NB: If we got an Error object, then try and extract the information from the location the object was made.
|
||||
if (auto error = exception.as_if<JS::Error>()) {
|
||||
for (auto const& frame : error->traceback()) {
|
||||
if (auto object = exception.as_if<JS::Object>(); object && object->error_data()) {
|
||||
for (auto const& frame : object->error_data()->traceback()) {
|
||||
auto source_range = frame.source_range();
|
||||
if (source_range.start.line != 0 || source_range.start.column != 0) {
|
||||
attributes.filename = MUST(String::from_byte_string(source_range.filename()));
|
||||
|
|
|
|||
|
|
@ -35,21 +35,27 @@ void report_exception_to_console(JS::Value value, JS::Realm& realm, ErrorInPromi
|
|||
} else {
|
||||
dbgln("\033[31;1mUnhandled JavaScript exception{}:\033[0m [{}] {}", error_in_promise == ErrorInPromise::Yes ? " (in promise)" : "", name, message);
|
||||
}
|
||||
if (is<JS::Error>(object)) {
|
||||
// FIXME: We should be doing this for DOMException as well
|
||||
// https://webidl.spec.whatwg.org/#js-DOMException-specialness
|
||||
// "Additionally, if an implementation gives native Error objects special powers or nonstandard properties (such as a stack property), it should also expose those on DOMException objects."
|
||||
auto const& error_value = static_cast<JS::Error const&>(object);
|
||||
dbgln("{}", error_value.stack_string(JS::CompactTraceback::Yes));
|
||||
console.report_exception(error_value, error_in_promise == ErrorInPromise::Yes);
|
||||
|
||||
if (auto const* error_data = object.error_data()) {
|
||||
String exception_name;
|
||||
String exception_message;
|
||||
if (auto const* exception = as_if<WebIDL::DOMException>(object)) {
|
||||
exception_name = exception->name().to_string();
|
||||
exception_message = MUST(exception->message().view().to_utf8());
|
||||
} else {
|
||||
exception_name = name.to_string_without_side_effects();
|
||||
exception_message = message.to_string_without_side_effects();
|
||||
}
|
||||
dbgln("{}", error_data->stack_string(JS::CompactTraceback::Yes));
|
||||
console.report_exception(exception_name, exception_message, *error_data, error_in_promise == ErrorInPromise::Yes);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
dbgln("\033[31;1mUnhandled JavaScript exception{}:\033[0m {}", error_in_promise == ErrorInPromise::Yes ? " (in promise)" : "", value);
|
||||
}
|
||||
|
||||
console.report_exception(*JS::Error::create(realm, value.to_utf16_string_without_side_effects()), error_in_promise == ErrorInPromise::Yes);
|
||||
auto message = value.to_string_without_side_effects();
|
||||
auto error = JS::Error::create(realm, Utf16String::from_utf8(message));
|
||||
console.report_exception("Error"_string, message, *error, error_in_promise == ErrorInPromise::Yes);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#report-the-exception
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ GC::Ref<DOMException> DOMException::construct_impl(JS::Realm& realm, Utf16String
|
|||
|
||||
DOMException::DOMException(JS::Realm& realm, FlyString name, Utf16String const& message)
|
||||
: PlatformObject(realm)
|
||||
, ErrorData(realm.vm())
|
||||
, m_name(move(name))
|
||||
, m_message(message)
|
||||
{
|
||||
|
|
@ -37,6 +38,7 @@ DOMException::DOMException(JS::Realm& realm, FlyString name, Utf16String const&
|
|||
|
||||
DOMException::DOMException(JS::Realm& realm)
|
||||
: PlatformObject(realm)
|
||||
, ErrorData(realm.vm())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +50,12 @@ void DOMException::initialize(JS::Realm& realm)
|
|||
Base::initialize(realm);
|
||||
}
|
||||
|
||||
void DOMException::visit_edges(Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
ErrorData::visit_edges(visitor);
|
||||
}
|
||||
|
||||
WebIDL::ExceptionOr<void> DOMException::serialization_steps(HTML::TransferDataEncoder& serialized, bool, HTML::SerializationMemory&)
|
||||
{
|
||||
// 1. Set serialized.[[Name]] to value’s name.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <AK/Utf16FlyString.h>
|
||||
#include <AK/Utf16String.h>
|
||||
#include <LibJS/Runtime/ErrorData.h>
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
#include <LibWeb/Bindings/Serializable.h>
|
||||
#include <LibWeb/Export.h>
|
||||
|
|
@ -94,6 +95,7 @@ static u16 get_legacy_code_for_name(FlyString const& name)
|
|||
// https://webidl.spec.whatwg.org/#idl-DOMException
|
||||
class WEB_API DOMException
|
||||
: public Bindings::PlatformObject
|
||||
, public JS::ErrorData
|
||||
, public Bindings::Serializable {
|
||||
WEB_PLATFORM_OBJECT(DOMException, Bindings::PlatformObject);
|
||||
GC_DECLARE_ALLOCATOR(DOMException);
|
||||
|
|
@ -120,8 +122,12 @@ protected:
|
|||
explicit DOMException(JS::Realm&);
|
||||
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
virtual void visit_edges(Visitor&) override;
|
||||
|
||||
private:
|
||||
virtual ErrorData* error_data() final { return this; }
|
||||
virtual ErrorData const* error_data() const final { return this; }
|
||||
|
||||
FlyString m_name;
|
||||
Utf16FlyString m_message;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <AK/MemoryStream.h>
|
||||
#include <LibJS/Print.h>
|
||||
#include <LibJS/Runtime/BigInt.h>
|
||||
#include <LibJS/Runtime/ErrorData.h>
|
||||
#include <LibJS/Runtime/Realm.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
|
|
@ -99,17 +100,12 @@ void DevToolsConsoleClient::handle_result(JS::Value result)
|
|||
m_client->did_execute_js_console_input(serialize_js_value(m_realm, result));
|
||||
}
|
||||
|
||||
void DevToolsConsoleClient::report_exception(JS::Error const& exception, bool in_promise)
|
||||
void DevToolsConsoleClient::report_exception(String const& name, String const& message, JS::ErrorData const& error_data, bool in_promise)
|
||||
{
|
||||
auto& vm = exception.vm();
|
||||
|
||||
auto name = exception.get_without_side_effects(vm.names.name);
|
||||
auto message = exception.get_without_side_effects(vm.names.message);
|
||||
|
||||
Vector<WebView::StackFrame> trace;
|
||||
trace.ensure_capacity(exception.traceback().size());
|
||||
trace.ensure_capacity(error_data.traceback().size());
|
||||
|
||||
for (auto const& frame : exception.traceback()) {
|
||||
for (auto const& frame : error_data.traceback()) {
|
||||
auto const& source_range = frame.source_range();
|
||||
WebView::StackFrame stack_frame;
|
||||
|
||||
|
|
@ -129,8 +125,8 @@ void DevToolsConsoleClient::report_exception(JS::Error const& exception, bool in
|
|||
send_console_output({
|
||||
.timestamp = UnixDateTime::now(),
|
||||
.output = WebView::ConsoleError {
|
||||
.name = name.to_string_without_side_effects(),
|
||||
.message = message.to_string_without_side_effects(),
|
||||
.name = name,
|
||||
.message = message,
|
||||
.trace = move(trace),
|
||||
.inside_promise = in_promise,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ private:
|
|||
DevToolsConsoleClient(JS::Realm&, JS::Console&, PageClient&, ConsoleGlobalEnvironmentExtensions&);
|
||||
|
||||
virtual void handle_result(JS::Value) override;
|
||||
virtual void report_exception(JS::Error const&, bool) override;
|
||||
virtual void report_exception(String const& name, String const& message, JS::ErrorData const&, bool) override;
|
||||
virtual void end_group() override { }
|
||||
virtual void clear() override { }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 15 tests
|
||||
|
||||
15 Pass
|
||||
Pass Cannot construct without new
|
||||
Pass inherits from Error: prototype-side
|
||||
Pass does not inherit from Error: class-side
|
||||
Pass message property descriptor
|
||||
Pass message getter performs brand checks (i.e. is not [LegacyLenientThis])
|
||||
Pass name property descriptor
|
||||
Pass name getter performs brand checks (i.e. is not [LegacyLenientThis])
|
||||
Pass code property descriptor
|
||||
Pass code getter performs brand checks (i.e. is not [LegacyLenientThis])
|
||||
Pass code property is not affected by shadowing the name property
|
||||
Pass Object.prototype.toString behavior is like other interfaces
|
||||
Pass Inherits its toString() from Error.prototype
|
||||
Pass toString() behavior from Error.prototype applies as expected
|
||||
Pass DOMException.prototype.toString() applied to DOMException.prototype throws because of name/message brand checks
|
||||
Pass If the implementation has a stack property on normal errors, it also does on DOMExceptions
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 1 tests
|
||||
|
||||
1 Pass
|
||||
Pass DOMException-is-error
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
|
||||
<script>
|
||||
self.GLOBAL = {
|
||||
isWindow: function() { return true; },
|
||||
isWorker: function() { return false; },
|
||||
isShadowRealm: function() { return false; },
|
||||
};
|
||||
</script>
|
||||
<script src="../../../resources/testharness.js"></script>
|
||||
<script src="../../../resources/testharnessreport.js"></script>
|
||||
|
||||
<div id=log></div>
|
||||
<script src="../../../webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js"></script>
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// META: global=window,dedicatedworker,shadowrealm
|
||||
|
||||
"use strict";
|
||||
|
||||
test(() => {
|
||||
assert_throws_js(TypeError, () => DOMException());
|
||||
}, "Cannot construct without new");
|
||||
|
||||
test(() => {
|
||||
assert_equals(Object.getPrototypeOf(DOMException.prototype), Error.prototype);
|
||||
}, "inherits from Error: prototype-side");
|
||||
|
||||
test(() => {
|
||||
assert_equals(Object.getPrototypeOf(DOMException), Function.prototype);
|
||||
}, "does not inherit from Error: class-side");
|
||||
|
||||
test(() => {
|
||||
const e = new DOMException("message", "name");
|
||||
assert_false(e.hasOwnProperty("message"), "property is not own");
|
||||
|
||||
const propDesc = Object.getOwnPropertyDescriptor(DOMException.prototype, "message");
|
||||
assert_equals(typeof propDesc.get, "function", "property descriptor is a getter");
|
||||
assert_equals(propDesc.set, undefined, "property descriptor is not a setter");
|
||||
assert_true(propDesc.enumerable, "property descriptor enumerable");
|
||||
assert_true(propDesc.configurable, "property descriptor configurable");
|
||||
}, "message property descriptor");
|
||||
|
||||
test(() => {
|
||||
const getter = Object.getOwnPropertyDescriptor(DOMException.prototype, "message").get;
|
||||
|
||||
assert_throws_js(TypeError, () => getter.apply({}));
|
||||
}, "message getter performs brand checks (i.e. is not [LegacyLenientThis])");
|
||||
|
||||
test(() => {
|
||||
const e = new DOMException("message", "name");
|
||||
assert_false(e.hasOwnProperty("name"), "property is not own");
|
||||
|
||||
const propDesc = Object.getOwnPropertyDescriptor(DOMException.prototype, "name");
|
||||
assert_equals(typeof propDesc.get, "function", "property descriptor is a getter");
|
||||
assert_equals(propDesc.set, undefined, "property descriptor is not a setter");
|
||||
assert_true(propDesc.enumerable, "property descriptor enumerable");
|
||||
assert_true(propDesc.configurable, "property descriptor configurable");
|
||||
}, "name property descriptor");
|
||||
|
||||
test(() => {
|
||||
const getter = Object.getOwnPropertyDescriptor(DOMException.prototype, "name").get;
|
||||
|
||||
assert_throws_js(TypeError, () => getter.apply({}));
|
||||
}, "name getter performs brand checks (i.e. is not [LegacyLenientThis])");
|
||||
|
||||
test(() => {
|
||||
const e = new DOMException("message", "name");
|
||||
assert_false(e.hasOwnProperty("code"), "property is not own");
|
||||
|
||||
const propDesc = Object.getOwnPropertyDescriptor(DOMException.prototype, "code");
|
||||
assert_equals(typeof propDesc.get, "function", "property descriptor is a getter");
|
||||
assert_equals(propDesc.set, undefined, "property descriptor is not a setter");
|
||||
assert_true(propDesc.enumerable, "property descriptor enumerable");
|
||||
assert_true(propDesc.configurable, "property descriptor configurable");
|
||||
}, "code property descriptor");
|
||||
|
||||
test(() => {
|
||||
const getter = Object.getOwnPropertyDescriptor(DOMException.prototype, "code").get;
|
||||
|
||||
assert_throws_js(TypeError, () => getter.apply({}));
|
||||
}, "code getter performs brand checks (i.e. is not [LegacyLenientThis])");
|
||||
|
||||
test(() => {
|
||||
const e = new DOMException("message", "InvalidCharacterError");
|
||||
assert_equals(e.code, 5, "Initially the code is set to 5");
|
||||
|
||||
Object.defineProperty(e, "name", {
|
||||
value: "WrongDocumentError"
|
||||
});
|
||||
|
||||
assert_equals(e.code, 5, "The code is still set to 5");
|
||||
}, "code property is not affected by shadowing the name property");
|
||||
|
||||
test(() => {
|
||||
const e = new DOMException("message", "name");
|
||||
assert_equals(Object.prototype.toString.call(e), "[object DOMException]");
|
||||
}, "Object.prototype.toString behavior is like other interfaces");
|
||||
|
||||
test(() => {
|
||||
const e = new DOMException("message", "name");
|
||||
assert_false(e.hasOwnProperty("toString"), "toString must not exist on the instance");
|
||||
assert_false(DOMException.prototype.hasOwnProperty("toString"), "toString must not exist on DOMException.prototype");
|
||||
assert_equals(typeof e.toString, "function", "toString must still exist (via Error.prototype)");
|
||||
}, "Inherits its toString() from Error.prototype");
|
||||
|
||||
test(() => {
|
||||
const e = new DOMException("message", "name");
|
||||
assert_equals(e.toString(), "name: message",
|
||||
"The default Error.prototype.toString() behavior must work on supplied name and message");
|
||||
|
||||
Object.defineProperty(e, "name", { value: "new name" });
|
||||
Object.defineProperty(e, "message", { value: "new message" });
|
||||
assert_equals(e.toString(), "new name: new message",
|
||||
"The default Error.prototype.toString() behavior must work on shadowed names and messages");
|
||||
}, "toString() behavior from Error.prototype applies as expected");
|
||||
|
||||
test(() => {
|
||||
assert_throws_js(TypeError, () => DOMException.prototype.toString());
|
||||
}, "DOMException.prototype.toString() applied to DOMException.prototype throws because of name/message brand checks");
|
||||
|
||||
test(() => {
|
||||
let stackOnNormalErrors;
|
||||
try {
|
||||
throw new Error("normal error");
|
||||
} catch (e) {
|
||||
stackOnNormalErrors = e.stack;
|
||||
}
|
||||
|
||||
let stackOnDOMException;
|
||||
try {
|
||||
throw new DOMException("message", "name");
|
||||
} catch (e) {
|
||||
stackOnDOMException = e.stack;
|
||||
}
|
||||
|
||||
assert_equals(typeof stackOnDOMException, typeof stackOnNormalErrors, "The typeof values must match");
|
||||
}, "If the implementation has a stack property on normal errors, it also does on DOMExceptions");
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
|
||||
<script>
|
||||
self.GLOBAL = {
|
||||
isWindow: function() { return true; },
|
||||
isWorker: function() { return false; },
|
||||
isShadowRealm: function() { return false; },
|
||||
};
|
||||
</script>
|
||||
<script src="../../../resources/testharness.js"></script>
|
||||
<script src="../../../resources/testharnessreport.js"></script>
|
||||
|
||||
<div id=log></div>
|
||||
<script src="../../../webidl/ecmascript-binding/es-exceptions/DOMException-is-error.any.js"></script>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// META: global=window,dedicatedworker,shadowrealm
|
||||
|
||||
'use strict';
|
||||
|
||||
test(function() {
|
||||
// https://github.com/tc39/proposal-is-error/issues/9
|
||||
// https://github.com/whatwg/webidl/pull/1421
|
||||
assert_true(Error.isError(new DOMException()));
|
||||
});
|
||||
Loading…
Reference in a new issue