boa/builtins/symbol/
mod.rs

1//! This module implements the global `Symbol` object.
2//!
3//! The data type symbol is a primitive data type.
4//! The `Symbol()` function returns a value of type symbol, has static properties that expose
5//! several members of built-in objects, has static methods that expose the global symbol registry,
6//! and resembles a built-in object class, but is incomplete as a constructor because it does not
7//! support the syntax "`new Symbol()`".
8//!
9//! Every symbol value returned from `Symbol()` is unique.
10//!
11//! More information:
12//! - [MDN documentation][mdn]
13//! - [ECMAScript reference][spec]
14//!
15//! [spec]: https://tc39.es/ecma262/#sec-symbol-value
16//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
17
18#[cfg(test)]
19mod tests;
20
21use crate::{
22    builtins::BuiltIn,
23    object::{ConstructorBuilder, FunctionBuilder},
24    property::Attribute,
25    symbol::{JsSymbol, WellKnownSymbols},
26    value::JsValue,
27    BoaProfiler, Context, JsResult, JsString,
28};
29
30use std::cell::RefCell;
31
32use rustc_hash::FxHashMap;
33
34use super::JsArgs;
35
36thread_local! {
37    static GLOBAL_SYMBOL_REGISTRY: RefCell<GlobalSymbolRegistry> = RefCell::new(GlobalSymbolRegistry::new());
38}
39
40struct GlobalSymbolRegistry {
41    keys: FxHashMap<JsString, JsSymbol>,
42    symbols: FxHashMap<JsSymbol, JsString>,
43}
44
45impl GlobalSymbolRegistry {
46    fn new() -> Self {
47        Self {
48            keys: FxHashMap::default(),
49            symbols: FxHashMap::default(),
50        }
51    }
52
53    fn get_or_insert_key(&mut self, key: JsString) -> JsSymbol {
54        if let Some(symbol) = self.keys.get(&key) {
55            return symbol.clone();
56        }
57
58        let symbol = JsSymbol::new(Some(key.clone()));
59        self.keys.insert(key.clone(), symbol.clone());
60        self.symbols.insert(symbol.clone(), key);
61        symbol
62    }
63
64    fn get_symbol(&self, sym: JsSymbol) -> Option<JsString> {
65        if let Some(key) = self.symbols.get(&sym) {
66            return Some(key.clone());
67        }
68
69        None
70    }
71}
72
73#[derive(Debug, Clone, Copy)]
74pub struct Symbol;
75
76impl BuiltIn for Symbol {
77    const NAME: &'static str = "Symbol";
78
79    fn attribute() -> Attribute {
80        Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
81    }
82
83    fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) {
84        let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
85
86        let symbol_async_iterator = WellKnownSymbols::async_iterator();
87        let symbol_has_instance = WellKnownSymbols::has_instance();
88        let symbol_is_concat_spreadable = WellKnownSymbols::is_concat_spreadable();
89        let symbol_iterator = WellKnownSymbols::iterator();
90        let symbol_match = WellKnownSymbols::match_();
91        let symbol_match_all = WellKnownSymbols::match_all();
92        let symbol_replace = WellKnownSymbols::replace();
93        let symbol_search = WellKnownSymbols::search();
94        let symbol_species = WellKnownSymbols::species();
95        let symbol_split = WellKnownSymbols::split();
96        let symbol_to_primitive = WellKnownSymbols::to_primitive();
97        let symbol_to_string_tag = WellKnownSymbols::to_string_tag();
98        let symbol_unscopables = WellKnownSymbols::unscopables();
99
100        let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
101
102        let get_description = FunctionBuilder::native(context, Self::get_description)
103            .name("get description")
104            .constructable(false)
105            .build();
106
107        let symbol_object = ConstructorBuilder::with_standard_object(
108            context,
109            Self::constructor,
110            context.standard_objects().symbol_object().clone(),
111        )
112        .name(Self::NAME)
113        .length(Self::LENGTH)
114        .static_method(Self::for_, "for", 1)
115        .static_method(Self::key_for, "keyFor", 1)
116        .static_property("asyncIterator", symbol_async_iterator, attribute)
117        .static_property("hasInstance", symbol_has_instance, attribute)
118        .static_property("isConcatSpreadable", symbol_is_concat_spreadable, attribute)
119        .static_property("iterator", symbol_iterator, attribute)
120        .static_property("match", symbol_match, attribute)
121        .static_property("matchAll", symbol_match_all, attribute)
122        .static_property("replace", symbol_replace, attribute)
123        .static_property("search", symbol_search, attribute)
124        .static_property("species", symbol_species, attribute)
125        .static_property("split", symbol_split, attribute)
126        .static_property("toPrimitive", symbol_to_primitive, attribute)
127        .static_property("toStringTag", symbol_to_string_tag.clone(), attribute)
128        .static_property("unscopables", symbol_unscopables, attribute)
129        .method(Self::to_string, "toString", 0)
130        .accessor(
131            "description",
132            Some(get_description),
133            None,
134            Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE,
135        )
136        .callable(true)
137        .constructable(false)
138        .property(
139            symbol_to_string_tag,
140            Self::NAME,
141            Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
142        )
143        .build();
144
145        (Self::NAME, symbol_object.into(), Self::attribute())
146    }
147}
148
149impl Symbol {
150    /// The amount of arguments this function object takes.
151    pub(crate) const LENGTH: usize = 0;
152
153    /// The `Symbol()` constructor returns a value of type symbol.
154    ///
155    /// It is incomplete as a constructor because it does not support
156    /// the syntax `new Symbol()` and it is not intended to be subclassed.
157    ///
158    /// More information:
159    /// - [ECMAScript reference][spec]
160    /// - [MDN documentation][mdn]
161    ///
162    /// [spec]: https://tc39.es/ecma262/#sec-symbol-description
163    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/Symbol
164    pub(crate) fn constructor(
165        new_target: &JsValue,
166        args: &[JsValue],
167        context: &mut Context,
168    ) -> JsResult<JsValue> {
169        if new_target.is_undefined() {
170            return context.throw_type_error("Symbol is not a constructor");
171        }
172        let description = match args.get(0) {
173            Some(value) if !value.is_undefined() => Some(value.to_string(context)?),
174            _ => None,
175        };
176
177        Ok(JsSymbol::new(description).into())
178    }
179
180    fn this_symbol_value(value: &JsValue, context: &mut Context) -> JsResult<JsSymbol> {
181        match value {
182            JsValue::Symbol(ref symbol) => return Ok(symbol.clone()),
183            JsValue::Object(ref object) => {
184                let object = object.borrow();
185                if let Some(symbol) = object.as_symbol() {
186                    return Ok(symbol);
187                }
188            }
189            _ => {}
190        }
191
192        Err(context.construct_type_error("'this' is not a Symbol"))
193    }
194
195    /// `Symbol.prototype.toString()`
196    ///
197    /// This method returns a string representing the specified `Symbol` object.
198    ///
199    /// More information:
200    /// - [MDN documentation][mdn]
201    /// - [ECMAScript reference][spec]
202    ///
203    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.tostring
204    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toString
205    #[allow(clippy::wrong_self_convention)]
206    pub(crate) fn to_string(
207        this: &JsValue,
208        _: &[JsValue],
209        context: &mut Context,
210    ) -> JsResult<JsValue> {
211        let symbol = Self::this_symbol_value(this, context)?;
212        Ok(symbol.to_string().into())
213    }
214
215    /// `get Symbol.prototype.description`
216    ///
217    /// This accessor returns the description of the `Symbol` object.
218    ///
219    /// More information:
220    /// - [MDN documentation][mdn]
221    /// - [ECMAScript reference][spec]
222    ///
223    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.description
224    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/description
225    pub(crate) fn get_description(
226        this: &JsValue,
227        _: &[JsValue],
228        context: &mut Context,
229    ) -> JsResult<JsValue> {
230        let symbol = Self::this_symbol_value(this, context)?;
231        if let Some(ref description) = symbol.description() {
232            Ok(description.clone().into())
233        } else {
234            Ok(JsValue::undefined())
235        }
236    }
237
238    /// `Symbol.for( key )`
239    ///
240    /// More information:
241    /// - [MDN documentation][mdn]
242    /// - [ECMAScript reference][spec]
243    ///
244    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.for
245    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for
246    pub(crate) fn for_(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
247        // 1. Let stringKey be ? ToString(key).
248        let string_key = args
249            .get(0)
250            .cloned()
251            .unwrap_or_default()
252            .to_string(context)?;
253        // 2. For each element e of the GlobalSymbolRegistry List, do
254        //     a. If SameValue(e.[[Key]], stringKey) is true, return e.[[Symbol]].
255        // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for stringKey.
256        // 4. Let newSymbol be a new unique Symbol value whose [[Description]] value is stringKey.
257        // 5. Append the Record { [[Key]]: stringKey, [[Symbol]]: newSymbol } to the GlobalSymbolRegistry List.
258        // 6. Return newSymbol.
259        Ok(GLOBAL_SYMBOL_REGISTRY
260            .with(move |registry| {
261                let mut registry = registry.borrow_mut();
262                registry.get_or_insert_key(string_key)
263            })
264            .into())
265    }
266
267    /// `Symbol.keyFor( sym )`
268    ///
269    ///
270    /// More information:
271    /// - [MDN documentation][mdn]
272    /// - [ECMAScript reference][spec]
273    ///
274    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.keyfor
275    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/keyFor
276    pub(crate) fn key_for(
277        _: &JsValue,
278        args: &[JsValue],
279        context: &mut Context,
280    ) -> JsResult<JsValue> {
281        let sym = args.get_or_undefined(0);
282        // 1. If Type(sym) is not Symbol, throw a TypeError exception.
283        if let Some(sym) = sym.as_symbol() {
284            // 2. For each element e of the GlobalSymbolRegistry List (see 20.4.2.2), do
285            //     a. If SameValue(e.[[Symbol]], sym) is true, return e.[[Key]].
286            // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for sym.
287            // 4. Return undefined.
288            let symbol = GLOBAL_SYMBOL_REGISTRY.with(move |registry| {
289                let registry = registry.borrow();
290                registry.get_symbol(sym)
291            });
292
293            Ok(symbol.map(JsValue::from).unwrap_or_default())
294        } else {
295            context.throw_type_error("Symbol.keyFor: sym is not a symbol")
296        }
297    }
298}