1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 02:27:43 +00:00

LibJS: Implement non standard error.stack attribute

All other browser already support this feature.
There is a Stage 1 proposal to standardize this, but it does not seem
to be active.
This commit is contained in:
Hendiadyoin1 2022-02-06 17:00:28 +01:00 committed by Linus Groh
parent 8c3942d90c
commit 89c82abf1f
6 changed files with 100 additions and 0 deletions

View file

@ -436,6 +436,7 @@ namespace JS {
P(source) \ P(source) \
P(splice) \ P(splice) \
P(sqrt) \ P(sqrt) \
P(stack) \
P(startOfDay) \ P(startOfDay) \
P(startsWith) \ P(startsWith) \
P(status) \ P(status) \

View file

@ -5,9 +5,12 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <LibJS/AST.h>
#include <LibJS/Runtime/Completion.h> #include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Error.h> #include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/ExecutionContext.h>
#include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/SourceRange.h>
namespace JS { namespace JS {
@ -28,6 +31,7 @@ Error* Error::create(GlobalObject& global_object, String const& message)
Error::Error(Object& prototype) Error::Error(Object& prototype)
: Object(prototype) : Object(prototype)
{ {
populate_stack();
} }
// 20.5.8.1 InstallErrorCause ( O, options ), https://tc39.es/ecma262/#sec-installerrorcause // 20.5.8.1 InstallErrorCause ( O, options ), https://tc39.es/ecma262/#sec-installerrorcause
@ -48,6 +52,33 @@ ThrowCompletionOr<void> Error::install_error_cause(Value options)
return {}; return {};
} }
void Error::populate_stack()
{
AK::StringBuilder stack_string_builder {};
// Note: We roughly follow V8's formatting
// Note: The error's name and message get prepended by ErrorPrototype::stack
// Note: We don't want to capture the global exectution context, so we omit the last frame
// FIXME: We generate a stack-frame for the Errors constructor, other engines do not
for (size_t i = vm().execution_context_stack().size() - 1; i > 0; --i) {
auto const* frame = vm().execution_context_stack()[i];
auto function_name = frame->function_name;
if (auto const* current_node = frame->current_node) {
auto const& source_range = current_node->source_range();
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>"sv : function_name.view());
}
}
m_stack_string = stack_string_builder.build();
}
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \ #define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
ClassName* ClassName::create(GlobalObject& global_object) \ ClassName* ClassName::create(GlobalObject& global_object) \
{ \ { \

View file

@ -23,7 +23,13 @@ public:
explicit Error(Object& prototype); explicit Error(Object& prototype);
virtual ~Error() override = default; virtual ~Error() override = default;
String const& stack_string() const { return m_stack_string; }
ThrowCompletionOr<void> install_error_cause(Value options); ThrowCompletionOr<void> install_error_cause(Value options);
private:
void populate_stack();
String m_stack_string {};
}; };
// NOTE: Making these inherit from Error is not required by the spec but // NOTE: Making these inherit from Error is not required by the spec but

View file

@ -6,6 +6,7 @@
*/ */
#include <AK/Function.h> #include <AK/Function.h>
#include <LibJS/AST.h>
#include <LibJS/Runtime/Error.h> #include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/ErrorPrototype.h> #include <LibJS/Runtime/ErrorPrototype.h>
#include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/GlobalObject.h>
@ -27,6 +28,10 @@ void ErrorPrototype::initialize(GlobalObject& global_object)
define_direct_property(vm.names.name, js_string(vm, "Error"), attr); define_direct_property(vm.names.name, js_string(vm, "Error"), attr);
define_direct_property(vm.names.message, js_string(vm, ""), attr); define_direct_property(vm.names.message, js_string(vm, ""), attr);
define_native_function(vm.names.toString, to_string, 0, attr); define_native_function(vm.names.toString, to_string, 0, attr);
// Non standard property "stack"
// Every other engine seems to have this in some way or another, and the spec
// proposal for this is only Stage 1
define_native_accessor(vm.names.stack, stack, nullptr, attr);
} }
// 20.5.3.4 Error.prototype.toString ( ), https://tc39.es/ecma262/#sec-error.prototype.tostring // 20.5.3.4 Error.prototype.toString ( ), https://tc39.es/ecma262/#sec-error.prototype.tostring
@ -54,6 +59,34 @@ JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::to_string)
return js_string(vm, String::formatted("{}: {}", name, message)); return js_string(vm, String::formatted("{}: {}", name, message));
} }
JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::stack)
{
auto this_value = vm.this_value(global_object);
if (!this_value.is_object())
return vm.throw_completion<TypeError>(global_object, ErrorType::NotAnObject, this_value.to_string_without_side_effects());
auto& this_object = this_value.as_object();
if (!is<Error>(this_object))
return vm.throw_completion<TypeError>(global_object, ErrorType::NotAnObjectOfType, "Error");
String name = "Error";
auto name_property = TRY(this_object.get(vm.names.name));
if (!name_property.is_undefined())
name = TRY(name_property.to_string(global_object));
String message = "";
auto message_property = TRY(this_object.get(vm.names.message));
if (!message_property.is_undefined())
message = TRY(message_property.to_string(global_object));
String header = name;
if (!message.is_empty())
header = String::formatted("{}: {}", name, message);
return js_string(vm,
String::formatted("{}\n{}", header, static_cast<Error&>(this_object).stack_string()));
}
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \ #define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
PrototypeName::PrototypeName(GlobalObject& global_object) \ PrototypeName::PrototypeName(GlobalObject& global_object) \
: Object(*global_object.error_prototype()) \ : Object(*global_object.error_prototype()) \

View file

@ -20,6 +20,7 @@ public:
private: private:
JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_string);
JS_DECLARE_NATIVE_FUNCTION(stack);
}; };
#define DECLARE_NATIVE_ERROR_PROTOTYPE(ClassName, snake_name, PrototypeName, ConstructorName) \ #define DECLARE_NATIVE_ERROR_PROTOTYPE(ClassName, snake_name, PrototypeName, ConstructorName) \

View file

@ -0,0 +1,28 @@
test("Anonymous function", function () {
let stackString = (() => {
return Error();
})().stack;
let [header, ...stackFrames] = stackString.split("\n");
expect(header).toBe("Error");
expect(!!stackFrames[0].match(/^ at Error \(.*\/error-stack\.js:3:\d+\)$/)).toBeTrue();
expect(!!stackFrames[1].match(/^ at .*\/error-stack\.js:3:\d+$/)).toBeTrue();
expect(!!stackFrames[2].match(/^ at .*\/error-stack\.js:2:\d+$/)).toBeTrue();
});
test("Named function with message", function () {
function f() {
throw Error("You Shalt Not Pass!");
}
try {
f();
} catch (e) {
let stackString = e.stack;
let [header, ...stack_frames] = stackString.split("\n");
expect(header).toBe("Error: You Shalt Not Pass!");
expect(!!stack_frames[0].match(/^ at Error \(.*\/error-stack\.js:15:\d+\)$/)).toBeTrue();
expect(!!stack_frames[1].match(/^ at f \(.*\/error-stack\.js:15:\d+\)$/)).toBeTrue();
expect(!!stack_frames[2].match(/^ at .*\/error-stack\.js:18:\d+$/)).toBeTrue();
}
});