mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 02:47:34 +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:
parent
8c3942d90c
commit
89c82abf1f
6 changed files with 100 additions and 0 deletions
|
@ -436,6 +436,7 @@ namespace JS {
|
|||
P(source) \
|
||||
P(splice) \
|
||||
P(sqrt) \
|
||||
P(stack) \
|
||||
P(startOfDay) \
|
||||
P(startsWith) \
|
||||
P(status) \
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibJS/AST.h>
|
||||
#include <LibJS/Runtime/Completion.h>
|
||||
#include <LibJS/Runtime/Error.h>
|
||||
#include <LibJS/Runtime/ExecutionContext.h>
|
||||
#include <LibJS/Runtime/GlobalObject.h>
|
||||
#include <LibJS/SourceRange.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
|
@ -28,6 +31,7 @@ Error* Error::create(GlobalObject& global_object, String const& message)
|
|||
Error::Error(Object& prototype)
|
||||
: Object(prototype)
|
||||
{
|
||||
populate_stack();
|
||||
}
|
||||
|
||||
// 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 {};
|
||||
}
|
||||
|
||||
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) \
|
||||
ClassName* ClassName::create(GlobalObject& global_object) \
|
||||
{ \
|
||||
|
|
|
@ -23,7 +23,13 @@ public:
|
|||
explicit Error(Object& prototype);
|
||||
virtual ~Error() override = default;
|
||||
|
||||
String const& stack_string() const { return m_stack_string; }
|
||||
|
||||
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
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <LibJS/AST.h>
|
||||
#include <LibJS/Runtime/Error.h>
|
||||
#include <LibJS/Runtime/ErrorPrototype.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.message, js_string(vm, ""), 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
|
||||
|
@ -54,6 +59,34 @@ JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::to_string)
|
|||
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) \
|
||||
PrototypeName::PrototypeName(GlobalObject& global_object) \
|
||||
: Object(*global_object.error_prototype()) \
|
||||
|
|
|
@ -20,6 +20,7 @@ public:
|
|||
|
||||
private:
|
||||
JS_DECLARE_NATIVE_FUNCTION(to_string);
|
||||
JS_DECLARE_NATIVE_FUNCTION(stack);
|
||||
};
|
||||
|
||||
#define DECLARE_NATIVE_ERROR_PROTOTYPE(ClassName, snake_name, PrototypeName, ConstructorName) \
|
||||
|
|
28
Userland/Libraries/LibJS/Tests/error-stack.js
Normal file
28
Userland/Libraries/LibJS/Tests/error-stack.js
Normal 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();
|
||||
}
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue