Переглянути джерело

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.
Hendiadyoin1 3 роки тому
батько
коміт
89c82abf1f

+ 1 - 0
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)                                \

+ 31 - 0
Userland/Libraries/LibJS/Runtime/Error.cpp

@@ -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)                                                    \
     {                                                                                                            \

+ 6 - 0
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<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

+ 33 - 0
Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp

@@ -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())                                       \

+ 1 - 0
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) \

+ 28 - 0
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();
+    }
+});