Skip to main content

boa_engine/object/builtins/
jsarray.rs

1//! A Rust API wrapper for Boa's `Array` Builtin ECMAScript Object
2use crate::{
3    Context, JsExpect, JsResult, JsString, JsValue,
4    builtins::Array,
5    error::JsNativeError,
6    object::{JsFunction, JsObject},
7    value::{IntoOrUndefined, TryFromJs},
8};
9use boa_gc::{Finalize, Trace};
10use std::ops::Deref;
11
12/// `JsArray` provides a wrapper for Boa's implementation of the JavaScript `Array` object.
13#[derive(Debug, Clone, Trace, Finalize)]
14pub struct JsArray {
15    inner: JsObject,
16}
17
18impl JsArray {
19    /// Create a new empty array.
20    ///
21    /// # Errors
22    ///
23    /// Returns a `PanicError` if creating the array fails due to an engine bug.
24    #[inline]
25    pub fn new(context: &mut Context) -> JsResult<Self> {
26        let inner = Array::array_create(0, None, context)
27            .js_expect("creating an empty array with the default prototype must not fail")?;
28
29        Ok(Self { inner })
30    }
31
32    /// Create an array from a `IntoIterator<Item = JsValue>` convertible object.
33    pub fn from_iter<I>(elements: I, context: &mut Context) -> Self
34    where
35        I: IntoIterator<Item = JsValue>,
36    {
37        Self {
38            inner: Array::create_array_from_list(elements, context),
39        }
40    }
41
42    /// Create a [`JsArray`] from a [`JsObject`], if the object is not an array throw a `TypeError`.
43    ///
44    /// This does not clone the fields of the array, it only does a shallow clone of the object.
45    #[inline]
46    pub fn from_object(object: JsObject) -> JsResult<Self> {
47        if object.is_array() {
48            Ok(Self { inner: object })
49        } else {
50            Err(JsNativeError::typ()
51                .with_message("object is not an Array")
52                .into())
53        }
54    }
55
56    /// Get the length of the array.
57    ///
58    /// Same as `array.length` in JavaScript.
59    #[inline]
60    pub fn length(&self, context: &mut Context) -> JsResult<u64> {
61        self.inner.length_of_array_like(context)
62    }
63
64    /// Check if the array is empty, i.e. the `length` is zero.
65    #[inline]
66    pub fn is_empty(&self, context: &mut Context) -> JsResult<bool> {
67        self.inner.length_of_array_like(context).map(|len| len == 0)
68    }
69
70    /// Push an element to the array.
71    pub fn push<T>(&self, value: T, context: &mut Context) -> JsResult<JsValue>
72    where
73        T: Into<JsValue>,
74    {
75        self.push_items(&[value.into()], context)
76    }
77
78    /// Pushes a slice of elements to the array.
79    #[inline]
80    pub fn push_items(&self, items: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
81        Array::push(&self.inner.clone().into(), items, context)
82    }
83
84    /// Pops an element from the array.
85    #[inline]
86    pub fn pop(&self, context: &mut Context) -> JsResult<JsValue> {
87        Array::pop(&self.inner.clone().into(), &[], context)
88    }
89
90    /// Calls `Array.prototype.at()`.
91    pub fn at<T>(&self, index: T, context: &mut Context) -> JsResult<JsValue>
92    where
93        T: Into<i64>,
94    {
95        Array::at(&self.inner.clone().into(), &[index.into().into()], context)
96    }
97
98    /// Calls `Array.prototype.shift()`.
99    #[inline]
100    pub fn shift(&self, context: &mut Context) -> JsResult<JsValue> {
101        Array::shift(&self.inner.clone().into(), &[], context)
102    }
103
104    /// Calls `Array.prototype.unshift()`.
105    #[inline]
106    pub fn unshift(&self, items: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
107        Array::unshift(&self.inner.clone().into(), items, context)
108    }
109
110    /// Calls `Array.prototype.reverse()`.
111    #[inline]
112    pub fn reverse(&self, context: &mut Context) -> JsResult<Self> {
113        Array::reverse(&self.inner.clone().into(), &[], context)?;
114        Ok(self.clone())
115    }
116
117    /// Calls `Array.prototype.concat()`.
118    #[inline]
119    pub fn concat(&self, items: &[JsValue], context: &mut Context) -> JsResult<Self> {
120        let object = Array::concat(&self.inner.clone().into(), items, context)?
121            .as_object()
122            .js_expect("Array.prototype.concat should always return object")?;
123
124        Self::from_object(object)
125    }
126
127    /// Calls `Array.prototype.join()`.
128    #[inline]
129    pub fn join(&self, separator: Option<JsString>, context: &mut Context) -> JsResult<JsString> {
130        let result = Array::join(
131            &self.inner.clone().into(),
132            &[separator.into_or_undefined()],
133            context,
134        )?;
135        result
136            .as_string()
137            .js_expect("Array.prototype.join always returns string")
138            .map_err(Into::into)
139    }
140
141    /// Calls `Array.prototype.fill()`.
142    pub fn fill<T>(
143        &self,
144        value: T,
145        start: Option<u32>,
146        end: Option<u32>,
147        context: &mut Context,
148    ) -> JsResult<Self>
149    where
150        T: Into<JsValue>,
151    {
152        Array::fill(
153            &self.inner.clone().into(),
154            &[
155                value.into(),
156                start.into_or_undefined(),
157                end.into_or_undefined(),
158            ],
159            context,
160        )?;
161        Ok(self.clone())
162    }
163
164    /// Calls `Array.prototype.indexOf()`.
165    pub fn index_of<T>(
166        &self,
167        search_element: T,
168        from_index: Option<u32>,
169        context: &mut Context,
170    ) -> JsResult<Option<u32>>
171    where
172        T: Into<JsValue>,
173    {
174        let index = Array::index_of(
175            &self.inner.clone().into(),
176            &[search_element.into(), from_index.into_or_undefined()],
177            context,
178        )?
179        .as_number()
180        .js_expect("Array.prototype.indexOf should always return number")?;
181
182        #[allow(clippy::float_cmp)]
183        if index == -1.0 {
184            Ok(None)
185        } else {
186            Ok(Some(index as u32))
187        }
188    }
189
190    /// Calls `Array.prototype.lastIndexOf()`.
191    pub fn last_index_of<T>(
192        &self,
193        search_element: T,
194        from_index: Option<u32>,
195        context: &mut Context,
196    ) -> JsResult<Option<u32>>
197    where
198        T: Into<JsValue>,
199    {
200        let index = Array::last_index_of(
201            &self.inner.clone().into(),
202            &[search_element.into(), from_index.into_or_undefined()],
203            context,
204        )?
205        .as_number()
206        .js_expect("Array.prototype.lastIndexOf should always return number")?;
207
208        #[allow(clippy::float_cmp)]
209        if index == -1.0 {
210            Ok(None)
211        } else {
212            Ok(Some(index as u32))
213        }
214    }
215
216    /// Calls `Array.prototype.find()`.
217    #[inline]
218    pub fn find(
219        &self,
220        predicate: JsFunction,
221        this_arg: Option<JsValue>,
222        context: &mut Context,
223    ) -> JsResult<JsValue> {
224        Array::find(
225            &self.inner.clone().into(),
226            &[predicate.into(), this_arg.into_or_undefined()],
227            context,
228        )
229    }
230
231    /// Calls `Array.prototype.filter()`.
232    #[inline]
233    pub fn filter(
234        &self,
235        callback: JsFunction,
236        this_arg: Option<JsValue>,
237        context: &mut Context,
238    ) -> JsResult<Self> {
239        let object = Array::filter(
240            &self.inner.clone().into(),
241            &[callback.into(), this_arg.into_or_undefined()],
242            context,
243        )?
244        .as_object()
245        .js_expect("Array.prototype.filter should always return object")?;
246
247        Self::from_object(object)
248    }
249
250    /// Calls `Array.prototype.map()`.
251    #[inline]
252    pub fn map(
253        &self,
254        callback: JsFunction,
255        this_arg: Option<JsValue>,
256        context: &mut Context,
257    ) -> JsResult<Self> {
258        let object = Array::map(
259            &self.inner.clone().into(),
260            &[callback.into(), this_arg.into_or_undefined()],
261            context,
262        )?
263        .as_object()
264        .js_expect("Array.prototype.map should always return object")?;
265
266        Self::from_object(object)
267    }
268
269    /// Calls `Array.prototype.every()`.
270    #[inline]
271    pub fn every(
272        &self,
273        callback: JsFunction,
274        this_arg: Option<JsValue>,
275        context: &mut Context,
276    ) -> JsResult<bool> {
277        let result = Array::every(
278            &self.inner.clone().into(),
279            &[callback.into(), this_arg.into_or_undefined()],
280            context,
281        )?
282        .as_boolean()
283        .js_expect("Array.prototype.every should always return boolean")?;
284
285        Ok(result)
286    }
287
288    /// Calls `Array.prototype.some()`.
289    #[inline]
290    pub fn some(
291        &self,
292        callback: JsFunction,
293        this_arg: Option<JsValue>,
294        context: &mut Context,
295    ) -> JsResult<bool> {
296        let result = Array::some(
297            &self.inner.clone().into(),
298            &[callback.into(), this_arg.into_or_undefined()],
299            context,
300        )?
301        .as_boolean()
302        .js_expect("Array.prototype.some should always return boolean")?;
303
304        Ok(result)
305    }
306
307    /// Calls `Array.prototype.sort()`.
308    #[inline]
309    pub fn sort(&self, compare_fn: Option<JsFunction>, context: &mut Context) -> JsResult<Self> {
310        Array::sort(
311            &self.inner.clone().into(),
312            &[compare_fn.into_or_undefined()],
313            context,
314        )?;
315
316        Ok(self.clone())
317    }
318
319    /// Calls `Array.prototype.slice()`.
320    #[inline]
321    pub fn slice(
322        &self,
323        start: Option<u32>,
324        end: Option<u32>,
325        context: &mut Context,
326    ) -> JsResult<Self> {
327        let object = Array::slice(
328            &self.inner.clone().into(),
329            &[start.into_or_undefined(), end.into_or_undefined()],
330            context,
331        )?
332        .as_object()
333        .js_expect("Array.prototype.slice should always return object")?;
334
335        Self::from_object(object)
336    }
337
338    /// Calls `Array.prototype.values()`.
339    #[inline]
340    pub fn values(&self, context: &mut Context) -> JsResult<JsValue> {
341        Array::values(&self.inner.clone().into(), &[], context)
342    }
343
344    /// Calls `Array.prototype.splice()`.
345    ///
346    /// Removes and/or inserts elements from the array, returning the removed elements.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// # use boa_engine::{Context, JsValue};
352    /// # use boa_engine::object::builtins::JsArray;
353    /// let context = &mut Context::default();
354    /// let array = JsArray::from_iter([1, 2, 3].map(JsValue::from), context);
355    ///
356    /// // Insert elements at index 1 without removing
357    /// let removed = array.splice(
358    ///     1,
359    ///     Some(0),
360    ///     &[JsValue::from(10), JsValue::from(20)],
361    ///     context,
362    /// ).unwrap();
363    ///
364    /// assert_eq!(array.length(context).unwrap(), 5);
365    /// assert_eq!(removed.length(context).unwrap(), 0);
366    /// ```
367    #[inline]
368    pub fn splice(
369        &self,
370        start: u32,
371        delete_count: Option<u32>,
372        items: &[JsValue],
373        context: &mut Context,
374    ) -> JsResult<Self> {
375        let start = JsValue::from(start);
376        let delete_count = delete_count.map(JsValue::from);
377        let object = Array::splice_internal(
378            &self.inner.clone().into(),
379            Some(&start),
380            delete_count.as_ref(),
381            items,
382            context,
383        )?
384        .as_object()
385        .js_expect("Array.prototype.splice should always return object")?;
386
387        Self::from_object(object)
388    }
389
390    /// Calls `Array.prototype.reduce()`.
391    #[inline]
392    pub fn reduce(
393        &self,
394        callback: JsFunction,
395        initial_value: Option<JsValue>,
396        context: &mut Context,
397    ) -> JsResult<JsValue> {
398        Array::reduce(
399            &self.inner.clone().into(),
400            &[callback.into(), initial_value.into_or_undefined()],
401            context,
402        )
403    }
404
405    /// Calls `Array.prototype.reduceRight()`.
406    #[inline]
407    pub fn reduce_right(
408        &self,
409        callback: JsFunction,
410        initial_value: Option<JsValue>,
411        context: &mut Context,
412    ) -> JsResult<JsValue> {
413        Array::reduce_right(
414            &self.inner.clone().into(),
415            &[callback.into(), initial_value.into_or_undefined()],
416            context,
417        )
418    }
419
420    /// Calls `Array.prototype.toReversed`.
421    #[inline]
422    pub fn to_reversed(&self, context: &mut Context) -> JsResult<Self> {
423        let array = Array::to_reversed(&self.inner.clone().into(), &[], context)?;
424
425        Ok(Self {
426            inner: array
427                .as_object()
428                .js_expect("`to_reversed` must always return an `Array` on success")?,
429        })
430    }
431
432    /// Calls `Array.prototype.toSorted`.
433    #[inline]
434    pub fn to_sorted(
435        &self,
436        compare_fn: Option<JsFunction>,
437        context: &mut Context,
438    ) -> JsResult<Self> {
439        let array = Array::to_sorted(
440            &self.inner.clone().into(),
441            &[compare_fn.into_or_undefined()],
442            context,
443        )?;
444
445        Ok(Self {
446            inner: array
447                .as_object()
448                .js_expect("`to_sorted` must always return an `Array` on success")?,
449        })
450    }
451
452    /// Calls `Array.prototype.with`.
453    #[inline]
454    pub fn with(&self, index: u64, value: JsValue, context: &mut Context) -> JsResult<Self> {
455        let array = Array::with(&self.inner.clone().into(), &[index.into(), value], context)?;
456
457        Ok(Self {
458            inner: array
459                .as_object()
460                .js_expect("`with` must always return an `Array` on success")?,
461        })
462    }
463}
464
465impl From<JsArray> for JsObject {
466    #[inline]
467    fn from(o: JsArray) -> Self {
468        o.inner.clone()
469    }
470}
471
472impl From<JsArray> for JsValue {
473    #[inline]
474    fn from(o: JsArray) -> Self {
475        o.inner.clone().into()
476    }
477}
478
479impl Deref for JsArray {
480    type Target = JsObject;
481
482    #[inline]
483    fn deref(&self) -> &Self::Target {
484        &self.inner
485    }
486}
487
488impl TryFromJs for JsArray {
489    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
490        if let Some(o) = value.as_object() {
491            Self::from_object(o.clone())
492        } else {
493            Err(JsNativeError::typ()
494                .with_message("value is not an Array object")
495                .into())
496        }
497    }
498}
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn splice_remove() {
505        let context = &mut Context::default();
506        let array = JsArray::from_iter([1, 2, 3].map(JsValue::from), context);
507
508        let removed = array.splice(1, Some(1), &[], context).unwrap();
509
510        assert_eq!(array.length(context).unwrap(), 2);
511        assert_eq!(removed.length(context).unwrap(), 1);
512    }
513
514    #[test]
515    fn splice_insert() {
516        let context = &mut Context::default();
517        let array = JsArray::from_iter([1, 2, 3].map(JsValue::from), context);
518
519        let removed = array
520            .splice(1, Some(0), &[JsValue::from(10), JsValue::from(20)], context)
521            .unwrap();
522
523        assert_eq!(array.length(context).unwrap(), 5);
524        assert_eq!(removed.length(context).unwrap(), 0);
525    }
526
527    #[test]
528    fn splice_replace() {
529        let context = &mut Context::default();
530        let array = JsArray::from_iter([1, 2, 3].map(JsValue::from), context);
531
532        let removed = array
533            .splice(1, Some(1), &[JsValue::from(99)], context)
534            .unwrap();
535
536        assert_eq!(array.length(context).unwrap(), 3);
537        assert_eq!(removed.length(context).unwrap(), 1);
538    }
539
540    #[test]
541    fn splice_from_start() {
542        let context = &mut Context::default();
543        let array = JsArray::from_iter([1, 2, 3].map(JsValue::from), context);
544
545        let removed = array.splice(0, Some(1), &[], context).unwrap();
546
547        assert_eq!(array.length(context).unwrap(), 2);
548        assert_eq!(removed.length(context).unwrap(), 1);
549    }
550
551    #[test]
552    fn splice_empty_array() {
553        let context = &mut Context::default();
554        let array = JsArray::new(context).unwrap();
555
556        let removed = array.splice(0, Some(0), &[], context).unwrap();
557
558        assert_eq!(array.length(context).unwrap(), 0);
559        assert_eq!(removed.length(context).unwrap(), 0);
560    }
561}