From 89c82abf1f2641c467741cc98e29c957db666501 Mon Sep 17 00:00:00 2001 From: Hendiadyoin1 Date: Sun, 6 Feb 2022 17:00:28 +0100 Subject: [PATCH] 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. --- .../LibJS/Runtime/CommonPropertyNames.h | 1 + Userland/Libraries/LibJS/Runtime/Error.cpp | 31 +++++++++++++++++ Userland/Libraries/LibJS/Runtime/Error.h | 6 ++++ .../LibJS/Runtime/ErrorPrototype.cpp | 33 +++++++++++++++++++ .../Libraries/LibJS/Runtime/ErrorPrototype.h | 1 + Userland/Libraries/LibJS/Tests/error-stack.js | 28 ++++++++++++++++ 6 files changed, 100 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/error-stack.js diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 9f0bd564b4..02672074af 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -436,6 +436,7 @@ namespace JS { P(source) \ P(splice) \ P(sqrt) \ + P(stack) \ P(startOfDay) \ P(startsWith) \ P(status) \ diff --git a/Userland/Libraries/LibJS/Runtime/Error.cpp b/Userland/Libraries/LibJS/Runtime/Error.cpp index 378d6f572d..7c9d81c3e6 100644 --- a/Userland/Libraries/LibJS/Runtime/Error.cpp +++ b/Userland/Libraries/LibJS/Runtime/Error.cpp @@ -5,9 +5,12 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include +#include #include +#include 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 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() ? ""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) \ { \ diff --git a/Userland/Libraries/LibJS/Runtime/Error.h b/Userland/Libraries/LibJS/Runtime/Error.h index c2f47c86c3..279c3c905f 100644 --- a/Userland/Libraries/LibJS/Runtime/Error.h +++ b/Userland/Libraries/LibJS/Runtime/Error.h @@ -23,7 +23,13 @@ public: explicit Error(Object& prototype); virtual ~Error() override = default; + String const& stack_string() const { return m_stack_string; } + ThrowCompletionOr 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 diff --git a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp index d890f6fb8b..82cd7c3710 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -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(global_object, ErrorType::NotAnObject, this_value.to_string_without_side_effects()); + auto& this_object = this_value.as_object(); + + if (!is(this_object)) + return vm.throw_completion(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(this_object).stack_string())); +} + #define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \ PrototypeName::PrototypeName(GlobalObject& global_object) \ : Object(*global_object.error_prototype()) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h index 081a19666c..dff88e777e 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h @@ -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) \ diff --git a/Userland/Libraries/LibJS/Tests/error-stack.js b/Userland/Libraries/LibJS/Tests/error-stack.js new file mode 100644 index 0000000000..5b57e76dcd --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/error-stack.js @@ -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(); + } +});