diff --git a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp index 8dfeef3b1c..dd4563ed73 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.cpp @@ -31,7 +31,7 @@ void ErrorPrototype::initialize(GlobalObject& global_object) // 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); + define_native_accessor(vm.names.stack, stack_getter, stack_setter, attr); } // 20.5.3.4 Error.prototype.toString ( ), https://tc39.es/ecma262/#sec-error.prototype.tostring @@ -69,7 +69,7 @@ JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::to_string) return js_string(vm, String::formatted("{}: {}", name, message)); } -JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::stack) +JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::stack_getter) { auto* error = TRY(typed_this_value(global_object)); @@ -91,6 +91,27 @@ JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::stack) String::formatted("{}\n{}", header, error->stack_string())); } +// B.1.2 set Error.prototype.stack ( value ), https://tc39.es/proposal-error-stacks/#sec-set-error.prototype-stack +JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::stack_setter) +{ + // 1. Let E be the this value. + auto this_value = vm.this_value(global_object); + + // 2. If ! Type(E) is not Object, throw a TypeError exception. + 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(); + + // 3. Let numberOfArgs be the number of arguments passed to this function call. + // 4. If numberOfArgs is 0, throw a TypeError exception. + if (vm.argument_count() == 0) + return vm.throw_completion(global_object, ErrorType::BadArgCountOne, "set stack"); + + // 5. Return ? CreateDataPropertyOrThrow(E, "stack", value); + return TRY(this_object.create_data_property_or_throw(vm.names.stack, vm.argument(0))); +} + #define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \ PrototypeName::PrototypeName(GlobalObject& global_object) \ : PrototypeObject(*global_object.error_prototype()) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h index 007e21935f..5a7ce94e01 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorPrototype.h @@ -22,7 +22,8 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(to_string); - JS_DECLARE_NATIVE_FUNCTION(stack); + JS_DECLARE_NATIVE_FUNCTION(stack_getter); + JS_DECLARE_NATIVE_FUNCTION(stack_setter); }; #define DECLARE_NATIVE_ERROR_PROTOTYPE(ClassName, snake_name, PrototypeName, ConstructorName) \ diff --git a/Userland/Libraries/LibJS/Tests/builtins/Error/Error.prototype.stack-setter.js b/Userland/Libraries/LibJS/Tests/builtins/Error/Error.prototype.stack-setter.js new file mode 100644 index 0000000000..f987ce962b --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Error/Error.prototype.stack-setter.js @@ -0,0 +1,73 @@ +const stackDescriptor = Object.getOwnPropertyDescriptor(Error.prototype, "stack"); +const stackSetter = stackDescriptor.set; + +describe("normal behavior", () => { + test("basic functionality", () => { + "use strict"; + const error = new Error(); + expect(Object.hasOwn(error, "stack")).toBeFalse(); + expect(() => { + error.stack = "hello world"; + }).not.toThrowWithMessage(TypeError, "Cannot set property 'stack' of [object Error]"); + expect(Object.hasOwn(error, "stack")).toBeTrue(); + expect(error.stack).toBe("hello world"); + }); + + test("the name of the set function is 'set stack'", () => { + expect(stackSetter.name).toBe("set stack"); + }); + + test("works on any object", () => { + const nonError = {}; + expect(Object.hasOwn(nonError, "stack")).toBeFalse(); + stackSetter.call(nonError, "hello world"); + expect(Object.hasOwn(nonError, "stack")).toBeTrue(); + expect(nonError.stack).toBe("hello world"); + }); + + test("accepts any value", () => { + const nonError = { stack: 1 }; + stackSetter.call(nonError, undefined); + expect(nonError.stack).toBeUndefined(); + stackSetter.call(nonError, null); + expect(nonError.stack).toBeNull(); + stackSetter.call(nonError, NaN); + expect(nonError.stack).toBeNaN(); + stackSetter.call(nonError, 1n); + expect(nonError.stack).toBe(1n); + }); +}); + +describe("errors", () => { + test("this is not an object", () => { + expect(() => { + stackSetter.call(undefined); + }).toThrowWithMessage(TypeError, "undefined is not an object"); + + expect(() => { + stackSetter.call(null); + }).toThrowWithMessage(TypeError, "null is not an object"); + + expect(() => { + stackSetter.call(1); + }).toThrowWithMessage(TypeError, "1 is not an object"); + + expect(() => { + stackSetter.call("foo"); + }).toThrowWithMessage(TypeError, "foo is not an object"); + }); + + test("requires one argument", () => { + expect(() => { + stackSetter.call({}); + }).toThrowWithMessage(TypeError, "set stack() needs one argument"); + }); + + test("create_data_property_or_throw can throw", () => { + const revocable = Proxy.revocable([], {}); + revocable.revoke(); + expect(() => { + stackSetter.call(revocable.proxy, "foo"); + }).toThrowWithMessage(TypeError, "An operation was performed on a revoked Proxy object"); + }); +});