boa/builtins/set/
mod.rs

1//! This module implements the global `Set` objest.
2//!
3//! The JavaScript `Set` class is a global object that is used in the construction of sets; which
4//! are high-level, collections of values.
5//!
6//! More information:
7//!  - [ECMAScript reference][spec]
8//!  - [MDN documentation][mdn]
9//!
10//! [spec]: https://tc39.es/ecma262/#sec-set-objects
11//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
12
13use crate::{
14    builtins::{iterable::get_iterator, BuiltIn},
15    context::StandardObjects,
16    object::{
17        internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder,
18        ObjectData,
19    },
20    property::{Attribute, PropertyNameKind},
21    symbol::WellKnownSymbols,
22    BoaProfiler, Context, JsResult, JsValue,
23};
24use ordered_set::OrderedSet;
25
26pub mod set_iterator;
27use set_iterator::SetIterator;
28
29use super::JsArgs;
30
31pub mod ordered_set;
32#[cfg(test)]
33mod tests;
34
35#[derive(Debug, Clone)]
36pub(crate) struct Set(OrderedSet<JsValue>);
37
38impl BuiltIn for Set {
39    const NAME: &'static str = "Set";
40
41    fn attribute() -> Attribute {
42        Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
43    }
44
45    fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) {
46        let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
47
48        let get_species = FunctionBuilder::native(context, Self::get_species)
49            .name("get [Symbol.species]")
50            .constructable(false)
51            .build();
52
53        let size_getter = FunctionBuilder::native(context, Self::size_getter)
54            .constructable(false)
55            .name("get size")
56            .build();
57
58        let iterator_symbol = WellKnownSymbols::iterator();
59
60        let to_string_tag = WellKnownSymbols::to_string_tag();
61
62        let values_function = FunctionBuilder::native(context, Self::values)
63            .name("values")
64            .length(0)
65            .constructable(false)
66            .build();
67
68        let set_object = ConstructorBuilder::with_standard_object(
69            context,
70            Self::constructor,
71            context.standard_objects().set_object().clone(),
72        )
73        .name(Self::NAME)
74        .length(Self::LENGTH)
75        .static_accessor(
76            WellKnownSymbols::species(),
77            Some(get_species),
78            None,
79            Attribute::CONFIGURABLE,
80        )
81        .method(Self::add, "add", 1)
82        .method(Self::clear, "clear", 0)
83        .method(Self::delete, "delete", 1)
84        .method(Self::entries, "entries", 0)
85        .method(Self::for_each, "forEach", 1)
86        .method(Self::has, "has", 1)
87        .property(
88            "keys",
89            values_function.clone(),
90            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
91        )
92        .accessor("size", Some(size_getter), None, Attribute::CONFIGURABLE)
93        .property(
94            "values",
95            values_function.clone(),
96            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
97        )
98        .property(
99            iterator_symbol,
100            values_function,
101            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
102        )
103        .property(
104            to_string_tag,
105            Self::NAME,
106            Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
107        )
108        .build();
109
110        (Self::NAME, set_object.into(), Self::attribute())
111    }
112}
113
114impl Set {
115    pub(crate) const LENGTH: usize = 0;
116
117    /// Create a new set
118    pub(crate) fn constructor(
119        new_target: &JsValue,
120        args: &[JsValue],
121        context: &mut Context,
122    ) -> JsResult<JsValue> {
123        // 1
124        if new_target.is_undefined() {
125            return context
126                .throw_type_error("calling a builtin Set constructor without new is forbidden");
127        }
128
129        // 2
130        let prototype =
131            get_prototype_from_constructor(new_target, StandardObjects::set_object, context)?;
132
133        let obj = context.construct_object();
134        obj.set_prototype_instance(prototype.into());
135
136        let set = JsValue::new(obj);
137        // 3
138        set.set_data(ObjectData::set(OrderedSet::default()));
139
140        let iterable = args.get_or_undefined(0);
141        // 4
142        if iterable.is_null_or_undefined() {
143            return Ok(set);
144        }
145
146        // 5
147        let adder = set.get_field("add", context)?;
148
149        // 6
150        if !adder.is_function() {
151            return context.throw_type_error("'add' of 'newTarget' is not a function");
152        }
153
154        // 7
155        let iterator_record = get_iterator(iterable, context)?;
156
157        // 8.a
158        let mut next = iterator_record.next(context)?;
159
160        // 8
161        while !next.done {
162            // c
163            let next_value = next.value;
164
165            // d, e
166            if let Err(status) = context.call(&adder, &set, &[next_value]) {
167                return iterator_record.close(Err(status), context);
168            }
169
170            next = iterator_record.next(context)?
171        }
172
173        // 8.b
174        Ok(set)
175    }
176
177    /// `get Set [ @@species ]`
178    ///
179    /// The Set[Symbol.species] accessor property returns the Set constructor.
180    ///
181    /// More information:
182    ///  - [ECMAScript reference][spec]
183    ///  - [MDN documentation][mdn]
184    ///
185    /// [spec]: https://tc39.es/ecma262/#sec-get-set-@@species
186    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/@@species
187    fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
188        // 1. Return the this value.
189        Ok(this.clone())
190    }
191
192    /// `Set.prototype.add( value )`
193    ///
194    /// This method adds an entry with value into the set. Returns the set object
195    ///
196    /// More information:
197    ///  - [ECMAScript reference][spec]
198    ///  - [MDN documentation][mdn]
199    ///
200    /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.add
201    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add
202    pub(crate) fn add(
203        this: &JsValue,
204        args: &[JsValue],
205        context: &mut Context,
206    ) -> JsResult<JsValue> {
207        let value = args.get_or_undefined(0);
208
209        if let Some(object) = this.as_object() {
210            if let Some(set) = object.borrow_mut().as_set_mut() {
211                set.add(if value.as_number().map(|n| n == -0f64).unwrap_or(false) {
212                    JsValue::Integer(0)
213                } else {
214                    value.clone()
215                });
216            } else {
217                return context.throw_type_error("'this' is not a Set");
218            }
219        } else {
220            return context.throw_type_error("'this' is not a Set");
221        };
222
223        Ok(this.clone())
224    }
225
226    /// `Set.prototype.clear( )`
227    ///
228    /// This method removes all entries from the set.
229    ///
230    /// More information:
231    ///  - [ECMAScript reference][spec]
232    ///  - [MDN documentation][mdn]
233    ///
234    /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.clear
235    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear
236    pub(crate) fn clear(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
237        if let Some(object) = this.as_object() {
238            if object.borrow().is_set() {
239                this.set_data(ObjectData::set(OrderedSet::new()));
240                Ok(JsValue::undefined())
241            } else {
242                context.throw_type_error("'this' is not a Set")
243            }
244        } else {
245            context.throw_type_error("'this' is not a Set")
246        }
247    }
248
249    /// `Set.prototype.delete( value )`
250    ///
251    /// This method removes the entry for the given value if it exists.
252    /// Returns true if there was an element, false otherwise.
253    ///
254    /// More information:
255    ///  - [ECMAScript reference][spec]
256    ///  - [MDN documentation][mdn]
257    ///
258    /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.delete
259    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete
260    pub(crate) fn delete(
261        this: &JsValue,
262        args: &[JsValue],
263        context: &mut Context,
264    ) -> JsResult<JsValue> {
265        let value = args.get_or_undefined(0);
266
267        let res = if let Some(object) = this.as_object() {
268            if let Some(set) = object.borrow_mut().as_set_mut() {
269                set.delete(value)
270            } else {
271                return context.throw_type_error("'this' is not a Set");
272            }
273        } else {
274            return context.throw_type_error("'this' is not a Set");
275        };
276
277        Ok(res.into())
278    }
279
280    /// `Set.prototype.entries( )`
281    ///
282    /// This method returns an iterator over the entries of the set
283    ///
284    /// More information:
285    ///  - [ECMAScript reference][spec]
286    ///  - [MDN documentation][mdn]
287    ///
288    /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.entries
289    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries
290    pub(crate) fn entries(
291        this: &JsValue,
292        _: &[JsValue],
293        context: &mut Context,
294    ) -> JsResult<JsValue> {
295        if let Some(object) = this.as_object() {
296            let object = object.borrow();
297            if !object.is_set() {
298                return context.throw_type_error(
299                    "Method Set.prototype.entries called on incompatible receiver",
300                );
301            }
302        } else {
303            return context
304                .throw_type_error("Method Set.prototype.entries called on incompatible receiver");
305        }
306
307        Ok(SetIterator::create_set_iterator(
308            this.clone(),
309            PropertyNameKind::KeyAndValue,
310            context,
311        ))
312    }
313
314    /// `Set.prototype.forEach( callbackFn [ , thisArg ] )`
315    ///
316    /// This method executes the provided callback function for each value in the set
317    ///
318    /// More information:
319    ///  - [ECMAScript reference][spec]
320    ///  - [MDN documentation][mdn]
321    ///
322    /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.foreach
323    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/foreach
324    pub(crate) fn for_each(
325        this: &JsValue,
326        args: &[JsValue],
327        context: &mut Context,
328    ) -> JsResult<JsValue> {
329        if args.is_empty() {
330            return Err(JsValue::new("Missing argument for Set.prototype.forEach"));
331        }
332
333        let callback_arg = &args[0];
334        let this_arg = args.get_or_undefined(1);
335        // TODO: if condition should also check that we are not in strict mode
336        let this_arg = if this_arg.is_undefined() {
337            JsValue::Object(context.global_object())
338        } else {
339            this_arg.clone()
340        };
341
342        let mut index = 0;
343
344        while index < Set::get_size(this, context)? {
345            let arguments = if let JsValue::Object(ref object) = this {
346                let object = object.borrow();
347                if let Some(set) = object.as_set_ref() {
348                    set.get_index(index)
349                        .map(|value| [value.clone(), value.clone(), this.clone()])
350                } else {
351                    return context.throw_type_error("'this' is not a Set");
352                }
353            } else {
354                return context.throw_type_error("'this' is not a Set");
355            };
356
357            if let Some(arguments) = arguments {
358                context.call(callback_arg, &this_arg, &arguments)?;
359            }
360
361            index += 1;
362        }
363
364        Ok(JsValue::Undefined)
365    }
366
367    /// `Map.prototype.has( key )`
368    ///
369    /// This method checks if the map contains an entry with the given key.
370    ///
371    /// More information:
372    ///  - [ECMAScript reference][spec]
373    ///  - [MDN documentation][mdn]
374    ///
375    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has
376    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
377    pub(crate) fn has(
378        this: &JsValue,
379        args: &[JsValue],
380        context: &mut Context,
381    ) -> JsResult<JsValue> {
382        let value = args.get_or_undefined(0);
383
384        if let JsValue::Object(ref object) = this {
385            let object = object.borrow();
386            if let Some(set) = object.as_set_ref() {
387                return Ok(set.contains(value).into());
388            }
389        }
390
391        Err(context.construct_type_error("'this' is not a Set"))
392    }
393
394    /// `Set.prototype.values( )`
395    ///
396    /// This method returns an iterator over the values of the set
397    ///
398    /// More information:
399    ///  - [ECMAScript reference][spec]
400    ///  - [MDN documentation][mdn]
401    ///
402    /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.values
403    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/values
404    pub(crate) fn values(
405        this: &JsValue,
406        _: &[JsValue],
407        context: &mut Context,
408    ) -> JsResult<JsValue> {
409        if let Some(object) = this.as_object() {
410            let object = object.borrow();
411            if !object.is_set() {
412                return context.throw_type_error(
413                    "Method Set.prototype.values called on incompatible receiver",
414                );
415            }
416        } else {
417            return context
418                .throw_type_error("Method Set.prototype.values called on incompatible receiver");
419        }
420
421        Ok(SetIterator::create_set_iterator(
422            this.clone(),
423            PropertyNameKind::Value,
424            context,
425        ))
426    }
427
428    fn size_getter(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
429        Set::get_size(this, context).map(JsValue::from)
430    }
431
432    /// Helper function to get the size of the set.
433    fn get_size(set: &JsValue, context: &mut Context) -> JsResult<usize> {
434        if let JsValue::Object(ref object) = set {
435            let object = object.borrow();
436            if let Some(set) = object.as_set_ref() {
437                Ok(set.size())
438            } else {
439                Err(context.construct_type_error("'this' is not a Set"))
440            }
441        } else {
442            Err(context.construct_type_error("'this' is not a Set"))
443        }
444    }
445}