From 734e37442db4419b58228e25702bfe176e52f5e3 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 15 Jul 2024 20:38:28 -0400 Subject: [PATCH] LibJS: Implement Iterator.prototype [ @@toStringTag ] according to spec The spec allows setting the prototype on non built-in Iterator objects. This is a normative change in the Iterator Helpers proposal. See: https://github.com/tc39/proposal-iterator-helpers/commit/30b3501 --- Userland/Libraries/LibJS/Runtime/Iterator.cpp | 36 +++++++++++++++++++ Userland/Libraries/LibJS/Runtime/Iterator.h | 1 + .../LibJS/Runtime/IteratorPrototype.cpp | 25 +++++++++++-- .../LibJS/Runtime/IteratorPrototype.h | 3 ++ .../Iterator.prototype.@@toStringTag.js | 34 ++++++++++++++++-- 5 files changed, 94 insertions(+), 5 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/Iterator.cpp b/Userland/Libraries/LibJS/Runtime/Iterator.cpp index bbf7a0a747a..9d2b2e8a793 100644 --- a/Userland/Libraries/LibJS/Runtime/Iterator.cpp +++ b/Userland/Libraries/LibJS/Runtime/Iterator.cpp @@ -353,6 +353,42 @@ ThrowCompletionOr> iterator_to_list(VM& vm, IteratorRecord& } } +// 2.1 SetterThatIgnoresPrototypeProperties ( this, home, p, v ), https://tc39.es/proposal-iterator-helpers/#sec-SetterThatIgnoresPrototypeProperties +ThrowCompletionOr setter_that_ignores_prototype_properties(VM& vm, Value this_, Object const& home, PropertyKey const& property, Value value) +{ + // 1. If this is not an Object, then + if (!this_.is_object()) { + // a. Throw a TypeError exception. + return vm.throw_completion(ErrorType::NotAnObject, this_); + } + + auto& this_object = this_.as_object(); + + // 2. If this is home, then + if (&this_object == &home) { + // a. NOTE: Throwing here emulates assignment to a non-writable data property on the home object in strict mode code. + // b. Throw a TypeError exception. + return vm.throw_completion(ErrorType::DescWriteNonWritable, this_); + } + + // 3. Let desc be ? this.[[GetOwnProperty]](p). + auto desc = TRY(this_object.internal_get_own_property(property)); + + // 4. If desc is undefined, then + if (!desc.has_value()) { + // a. Perform ? CreateDataPropertyOrThrow(this, p, v). + TRY(this_object.create_data_property_or_throw(property, value)); + } + // 5. Else, + else { + // a. Perform ? Set(this, p, v, true). + TRY(this_object.set(property, value, Object::ShouldThrowExceptions::Yes)); + } + + // 6. Return unused. + return {}; +} + // Non-standard Completion get_iterator_values(VM& vm, Value iterable, IteratorValueCallback callback) { diff --git a/Userland/Libraries/LibJS/Runtime/Iterator.h b/Userland/Libraries/LibJS/Runtime/Iterator.h index 04a165ac3ec..96266c838a3 100644 --- a/Userland/Libraries/LibJS/Runtime/Iterator.h +++ b/Userland/Libraries/LibJS/Runtime/Iterator.h @@ -83,6 +83,7 @@ Completion iterator_close(VM&, IteratorRecord const&, Completion); Completion async_iterator_close(VM&, IteratorRecord const&, Completion); NonnullGCPtr create_iterator_result_object(VM&, Value, bool done); ThrowCompletionOr> iterator_to_list(VM&, IteratorRecord&); +ThrowCompletionOr setter_that_ignores_prototype_properties(VM&, Value this_, Object const& home, PropertyKey const& property, Value value); using IteratorValueCallback = Function(Value)>; Completion get_iterator_values(VM&, Value iterable, IteratorValueCallback callback); diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp index 9f5ca879ce1..071b9070027 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp @@ -29,9 +29,6 @@ void IteratorPrototype::initialize(Realm& realm) auto& vm = this->vm(); Base::initialize(realm); - // 3.1.3.13 Iterator.prototype [ @@toStringTag ], https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype-@@tostringtag - define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Iterator"_string), Attribute::Configurable | Attribute::Writable); - u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(realm, vm.well_known_symbol_iterator(), symbol_iterator, 0, attr); define_native_function(realm, vm.names.map, map, 1, attr); @@ -45,6 +42,9 @@ void IteratorPrototype::initialize(Realm& realm) define_native_function(realm, vm.names.some, some, 1, attr); define_native_function(realm, vm.names.every, every, 1, attr); define_native_function(realm, vm.names.find, find, 1, attr); + + // 3.1.3.13 Iterator.prototype [ @@toStringTag ], https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype-@@tostringtag + define_native_accessor(realm, vm.well_known_symbol_to_string_tag(), to_string_tag_getter, to_string_tag_setter, Attribute::Configurable); } // 27.1.2.1 %IteratorPrototype% [ @@iterator ] ( ), https://tc39.es/ecma262/#sec-%iteratorprototype%-@@iterator @@ -749,4 +749,23 @@ JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::find) } } +// 3.1.3.13.1 get Iterator.prototype [ @@toStringTag ], https://tc39.es/proposal-iterator-helpers/#sec-get-iteratorprototype-@@tostringtag +JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::to_string_tag_getter) +{ + // 1. Return "Iterator". + return PrimitiveString::create(vm, vm.names.Iterator.as_string()); +} + +// 3.1.3.13.2 set Iterator.prototype [ @@toStringTag ], https://tc39.es/proposal-iterator-helpers/#sec-set-iteratorprototype-@@tostringtag +JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::to_string_tag_setter) +{ + auto& realm = *vm.current_realm(); + + // 1. Perform ? SetterThatIgnoresPrototypeProperties(this value, %Iterator.prototype%, %Symbol.toStringTag%, v). + TRY(setter_that_ignores_prototype_properties(vm, vm.this_value(), realm.intrinsics().iterator_prototype(), vm.well_known_symbol_to_string_tag(), vm.argument(0))); + + // 2. Return undefined. + return js_undefined(); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h index bef72b17f0b..b0db25d08c4 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h @@ -34,6 +34,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(some); JS_DECLARE_NATIVE_FUNCTION(every); JS_DECLARE_NATIVE_FUNCTION(find); + + JS_DECLARE_NATIVE_FUNCTION(to_string_tag_getter); + JS_DECLARE_NATIVE_FUNCTION(to_string_tag_setter); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.@@toStringTag.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.@@toStringTag.js index 802fd88d802..ba1a8f43520 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.@@toStringTag.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.@@toStringTag.js @@ -1,3 +1,33 @@ -test("basic functionality", () => { - expect(Iterator.prototype[Symbol.toStringTag]).toBe("Iterator"); +const sentinel = "whf :^)"; + +describe("errors", () => { + test("setter called on non-object", () => { + let { get, set } = Object.getOwnPropertyDescriptor(Iterator.prototype, Symbol.toStringTag); + + expect(() => { + set.call(undefined, sentinel); + }).toThrowWithMessage(TypeError, "undefined is not an object"); + }); + + test("cannot set the built-in Iterator's toStringTag", () => { + expect(() => { + Iterator.prototype[Symbol.toStringTag] = sentinel; + }).toThrowWithMessage( + TypeError, + "Cannot write to non-writable property '[object IteratorPrototype]'" + ); + }); +}); + +describe("correct behavior", () => { + test("basic functionality", () => { + expect(Iterator.prototype[Symbol.toStringTag]).toBe("Iterator"); + }); + + test("toStringTag setter", () => { + let Proto = Object.create(Iterator.prototype); + Proto[Symbol.toStringTag] = sentinel; + + expect(Proto[Symbol.toStringTag]).toBe(sentinel); + }); });