Skip to main content

boa_macros/
lib.rs

1//! Macros for the Boa JavaScript engine.
2#![doc = include_str!("../ABOUT.md")]
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg",
5    html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg"
6)]
7#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
8
9use crate::utils::RenameScheme;
10use cow_utils::CowUtils;
11use proc_macro::TokenStream;
12use proc_macro2::Literal;
13use quote::{ToTokens, quote};
14use syn::{
15    Data, DeriveInput, Expr, ExprLit, Fields, FieldsNamed, Ident, Lit, LitStr, Token,
16    parse::{Parse, ParseStream},
17    parse_macro_input,
18    punctuated::Punctuated,
19};
20use synstructure::{AddBounds, Structure, decl_derive};
21
22mod embedded_module_loader;
23
24mod class;
25mod module;
26mod utils;
27mod value;
28
29/// The `js_value!` macro creates a `JsValue` instance based on a JSON-like DSL.
30#[proc_macro]
31pub fn js_value(input: TokenStream) -> TokenStream {
32    value::js_value_impl(proc_macro2::TokenStream::from(input)).into()
33}
34
35/// Create a `JsObject` object from a simpler DSL that resembles JSON.
36#[proc_macro]
37pub fn js_object(input: TokenStream) -> TokenStream {
38    value::js_object_impl(proc_macro2::TokenStream::from(input)).into()
39}
40
41/// Implementation of the inner iterator of the `embed_module!` macro. All
42/// arguments are required.
43///
44/// # Warning
45/// This should not be used directly as is, and instead should be used through
46/// the `embed_module!` macro in `boa_engine` for convenience.
47#[proc_macro]
48pub fn embed_module_inner(input: TokenStream) -> TokenStream {
49    embedded_module_loader::embed_module_impl(input)
50}
51
52/// `boa_class` proc macro attribute that applies to an `impl XYZ` block and
53/// add a `[boa_engine::JsClass]` implementation for it.
54///
55/// It will transform functions in the `impl ...` block as follow (by default, see
56/// below):
57/// 1. `fn some_method(&self, ...) -> ... {}` will be added as class methods with
58///    the name `some_method`, borrowing the object for the ref. This is dangerous
59///    if the function execute/eval JavaScript back (potentially leading to a
60///    `BorrowError`).
61/// 2. `fn some_method(&mut self, ...) -> ... {}` will be added as class methods,
62///    similar to the above but borrowing as mutable at runtime.
63/// 3. `fn some_method(...) -> ... {}` (no self mention) will be added as a
64///    static method.
65/// 4. `#[boa(constructor)] fn ...(...) -> Self {}` (or returning `JsResult<Self>`)
66///    will be used as the constructor of the class. If no constructor is declared,
67///    `Default::default()` will be used instead. If the `Default` trait is not
68///    defined for the type, an error will happen.
69/// 5. `#[boa(getter)]`
70///
71/// To change this behaviour, you can use the following attributes on the function
72/// declarations:
73/// 1. `#[boa(rename = "...")]` renames the function in JavaScript with the string.
74/// 2. `#[boa(getter)]` will declare a getter accessor.
75/// 2. `#[boa(setter)]` will declare a setter accessor.
76/// 3. `#[boa(static)]` will declare a static method.
77/// 4. `#[boa(method)]` will declare a method.
78/// 5. `#[boa(constructor)]` will declare a constructor.
79/// 6. `#[boa(length = 123)]` sets the length of the function in JavaScript (ie. its
80///    number of arguments accepted).
81///
82/// Multiple of those attributes can be added to a single method.
83///
84/// The top level `boa_class` supports the following:
85/// 1. `#[boa_class(rename = "...")]` sets the name of the class in JavaScript.
86/// 2. `#[boa(rename_all = "camelCase")]` will change the naming scheme of verbatim
87///    to using "camelCase" or "none".
88///
89/// # Warning
90/// This should not be used directly as is, and instead should be used through
91/// the `embed_module!` macro in `boa_engine` for convenience.
92#[proc_macro_attribute]
93pub fn boa_class(attr: TokenStream, item: TokenStream) -> TokenStream {
94    class::class_impl(attr, item)
95}
96
97/// `boa_module` proc macro attribute for declaring a `boa_engine::Module` based
98/// on a Rust module. The original Rust module will also be exposed as is.
99///
100/// This macro exports two functions out of the existing module (and those
101/// functions must not exist in the declared module):
102///
103/// ## `boa_module(realm: Option<Realm>, context: &mut Context) -> JsResult<Module>`
104/// Create a JavaScript module from the rust module's content.
105///
106/// ## `boa_register(realm: Option<Realm>, context: &mut Context) -> JsResult<()>`
107/// Register the constants, classes and functions from the module in the global
108/// scope of the Realm (if specified) or the context (if no realm).
109#[proc_macro_attribute]
110pub fn boa_module(attr: TokenStream, item: TokenStream) -> TokenStream {
111    module::module_impl(attr, item)
112}
113
114struct Static {
115    literal: LitStr,
116    ident: Ident,
117}
118
119impl Parse for Static {
120    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
121        let expr = Expr::parse(input)?;
122        match expr {
123            Expr::Tuple(expr) => {
124                let mut elems = expr.elems.iter().cloned();
125                let literal = elems
126                    .next()
127                    .ok_or_else(|| syn::Error::new_spanned(&expr, "invalid empty tuple"))?;
128                let ident = elems.next();
129                if elems.next().is_some() {
130                    return Err(syn::Error::new_spanned(
131                        &expr,
132                        "invalid tuple with more than two elements",
133                    ));
134                }
135                let Expr::Lit(ExprLit {
136                    lit: Lit::Str(literal),
137                    ..
138                }) = literal
139                else {
140                    return Err(syn::Error::new_spanned(
141                        literal,
142                        "expected an UTF-8 string literal",
143                    ));
144                };
145
146                let ident = if let Some(ident) = ident {
147                    syn::parse2::<Ident>(ident.into_token_stream())?
148                } else {
149                    Ident::new(&literal.value().cow_to_uppercase(), literal.span())
150                };
151
152                Ok(Self { literal, ident })
153            }
154            Expr::Lit(expr) => match expr.lit {
155                Lit::Str(str) => Ok(Self {
156                    ident: Ident::new(&str.value().cow_to_uppercase(), str.span()),
157                    literal: str,
158                }),
159                _ => Err(syn::Error::new_spanned(
160                    expr,
161                    "expected an UTF-8 string literal",
162                )),
163            },
164            _ => Err(syn::Error::new_spanned(
165                expr,
166                "expected a string literal or a tuple expression",
167            )),
168        }
169    }
170}
171
172struct Syms(Vec<Static>);
173
174impl Parse for Syms {
175    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
176        let parsed = Punctuated::<Static, Token![,]>::parse_terminated(input)?;
177        let literals = parsed.into_iter().collect();
178        Ok(Self(literals))
179    }
180}
181
182#[doc(hidden)]
183#[proc_macro]
184pub fn static_syms(input: TokenStream) -> TokenStream {
185    let literals = parse_macro_input!(input as Syms).0;
186
187    let consts = literals.iter().enumerate().map(|(mut idx, lit)| {
188        let doc = format!(
189            "Symbol for the \"{}\" string.",
190            lit.literal
191                .value()
192                .cow_replace('<', r"\<")
193                .cow_replace('>', r"\>")
194                .cow_replace('*', r"\*")
195        );
196        let ident = &lit.ident;
197        idx += 1;
198        quote! {
199            #[doc = #doc]
200            pub const #ident: Self = unsafe { Self::new_unchecked(#idx) };
201        }
202    });
203
204    let literals = literals.iter().map(|lit| &lit.literal).collect::<Vec<_>>();
205
206    let caches = quote! {
207        type Set<T> = ::indexmap::IndexSet<T, ::core::hash::BuildHasherDefault<::rustc_hash::FxHasher>>;
208
209        /// Ordered set of commonly used static `UTF-8` strings.
210        ///
211        /// # Note
212        ///
213        /// `COMMON_STRINGS_UTF8`, `COMMON_STRINGS_UTF16` and the constants
214        /// defined in [`Sym`] must always be in sync.
215        pub(super) static COMMON_STRINGS_UTF8: ::phf::OrderedSet<&'static str> = {
216            const COMMON_STRINGS: ::phf::OrderedSet<&'static str> = ::phf::phf_ordered_set! {
217                #(#literals),*
218            };
219            // A `COMMON_STRINGS` of size `usize::MAX` would cause an overflow on our `Interner`
220            ::static_assertions::const_assert!(COMMON_STRINGS.len() < usize::MAX);
221            COMMON_STRINGS
222        };
223
224        /// Ordered set of commonly used static `UTF-16` strings.
225        ///
226        /// # Note
227        ///
228        /// `COMMON_STRINGS_UTF8`, `COMMON_STRINGS_UTF16` and the constants
229        /// defined in [`Sym`] must always be in sync.
230        // FIXME: use phf when const expressions are allowed.
231        // <https://github.com/rust-phf/rust-phf/issues/188>
232        pub(super) static COMMON_STRINGS_UTF16: ::once_cell::sync::Lazy<Set<&'static [u16]>> =
233            ::once_cell::sync::Lazy::new(|| {
234                let mut set = Set::with_capacity_and_hasher(
235                    COMMON_STRINGS_UTF8.len(),
236                    ::core::hash::BuildHasherDefault::default()
237                );
238                #(
239                    set.insert(::boa_macros::utf16!(#literals));
240                )*
241                set
242            });
243    };
244
245    quote! {
246        impl Sym {
247            #(#consts)*
248        }
249        #caches
250    }
251    .into()
252}
253
254/// Construct a utf-16 array literal from a utf-8 [`str`] literal.
255#[proc_macro]
256pub fn utf16(input: TokenStream) -> TokenStream {
257    let literal = parse_macro_input!(input as LitStr);
258    let utf8 = literal.value();
259    let utf16 = utf8.encode_utf16().collect::<Vec<_>>();
260    quote! {
261        [#(#utf16),*].as_slice()
262    }
263    .into()
264}
265
266/// Convert a utf8 string literal to a `JsString`
267#[proc_macro]
268pub fn js_str(input: TokenStream) -> TokenStream {
269    let literal = parse_macro_input!(input as LitStr);
270
271    let mut is_latin1 = true;
272    let codepoints = literal
273        .value()
274        .encode_utf16()
275        .map(|x| {
276            if x > u8::MAX.into() {
277                is_latin1 = false;
278            }
279            Literal::u16_unsuffixed(x)
280        })
281        .collect::<Vec<_>>();
282    if is_latin1 {
283        quote! {
284            ::boa_engine::string::JsStr::latin1([#(#codepoints),*].as_slice())
285        }
286    } else {
287        quote! {
288            ::boa_engine::string::JsStr::utf16([#(#codepoints),*].as_slice())
289        }
290    }
291    .into()
292}
293
294decl_derive! {
295    [Trace, attributes(boa_gc, unsafe_ignore_trace)] =>
296    /// Derive the `Trace` trait.
297    derive_trace
298}
299
300/// Derives the `Trace` trait.
301#[allow(clippy::too_many_lines)]
302fn derive_trace(mut s: Structure<'_>) -> proc_macro2::TokenStream {
303    struct EmptyTrace {
304        copy: bool,
305        drop: bool,
306    }
307
308    impl Parse for EmptyTrace {
309        fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
310            let i: Ident = input.parse()?;
311
312            if i != "empty_trace" && i != "unsafe_empty_trace" && i != "unsafe_no_drop" {
313                let msg = format!(
314                    "expected token \"empty_trace\", \"unsafe_empty_trace\" or \"unsafe_no_drop\", found {i:?}"
315                );
316                return Err(syn::Error::new_spanned(i.clone(), msg));
317            }
318
319            Ok(Self {
320                copy: i == "empty_trace",
321                drop: i == "empty_trace" || i != "unsafe_no_drop",
322            })
323        }
324    }
325
326    let mut drop = true;
327
328    for attr in &s.ast().attrs {
329        if attr.path().is_ident("boa_gc") {
330            let trace = match attr.parse_args::<EmptyTrace>() {
331                Ok(t) => t,
332                Err(e) => return e.into_compile_error(),
333            };
334
335            if trace.copy {
336                s.add_where_predicate(syn::parse_quote!(Self: Copy));
337            }
338
339            if !trace.drop {
340                drop = false;
341                continue;
342            }
343
344            return s.unsafe_bound_impl(
345                quote!(::boa_gc::Trace),
346                quote! {
347                    #[inline(always)]
348                    unsafe fn trace(&self, _tracer: &mut ::boa_gc::Tracer) {}
349                    #[inline(always)]
350                    unsafe fn trace_non_roots(&self) {}
351                    #[inline]
352                    fn run_finalizer(&self) {
353                        ::boa_gc::Finalize::finalize(self)
354                    }
355                },
356            );
357        }
358    }
359
360    s.filter(|bi| {
361        !bi.ast()
362            .attrs
363            .iter()
364            .any(|attr| attr.path().is_ident("unsafe_ignore_trace"))
365    });
366    let trace_body = s.each(|bi| quote!(::boa_gc::Trace::trace(#bi, tracer)));
367    let trace_other_body = s.each(|bi| quote!(mark(#bi)));
368
369    s.add_bounds(AddBounds::Fields);
370    let trace_impl = s.unsafe_bound_impl(
371        quote!(::boa_gc::Trace),
372        quote! {
373            #[inline]
374            unsafe fn trace(&self, tracer: &mut ::boa_gc::Tracer) {
375                #[allow(dead_code)]
376                let mut mark = |it: &dyn ::boa_gc::Trace| {
377                    // SAFETY: The implementor must ensure that `trace` is correctly implemented.
378                    unsafe {
379                        ::boa_gc::Trace::trace(it, tracer);
380                    }
381                };
382                match *self { #trace_body }
383            }
384            #[inline]
385            unsafe fn trace_non_roots(&self) {
386                #[allow(dead_code)]
387                fn mark<T: ::boa_gc::Trace + ?Sized>(it: &T) {
388                    // SAFETY: The implementor must ensure that `trace_non_roots` is correctly implemented.
389                    unsafe {
390                        ::boa_gc::Trace::trace_non_roots(it);
391                    }
392                }
393                match *self { #trace_other_body }
394            }
395            #[inline]
396            fn run_finalizer(&self) {
397                ::boa_gc::Finalize::finalize(self);
398                #[allow(dead_code)]
399                fn mark<T: ::boa_gc::Trace + ?Sized>(it: &T) {
400                    unsafe {
401                        ::boa_gc::Trace::run_finalizer(it);
402                    }
403                }
404                match *self { #trace_other_body }
405            }
406        },
407    );
408
409    // We also implement drop to prevent unsafe drop implementations on this
410    // type and encourage people to use Finalize. This implementation will
411    // call `Finalize::finalize` if it is safe to do so.
412    let drop_impl = if drop {
413        s.unbound_impl(
414            quote!(::core::ops::Drop),
415            quote! {
416                #[allow(clippy::inline_always)]
417                #[inline(always)]
418                fn drop(&mut self) {
419                    if ::boa_gc::finalizer_safe() {
420                        ::boa_gc::Finalize::finalize(self);
421                    }
422                }
423            },
424        )
425    } else {
426        quote!()
427    };
428
429    quote! {
430        #trace_impl
431        #drop_impl
432    }
433}
434
435decl_derive! {
436    [Finalize] =>
437    /// Derive the `Finalize` trait.
438    derive_finalize
439}
440
441/// Derives the `Finalize` trait.
442#[allow(clippy::needless_pass_by_value)]
443fn derive_finalize(s: Structure<'_>) -> proc_macro2::TokenStream {
444    s.unbound_impl(quote!(::boa_gc::Finalize), quote!())
445}
446
447decl_derive! {
448    [JsData] =>
449    /// Derive the `JsData` trait.
450    derive_js_data
451}
452
453/// Derives the `JsData` trait.
454#[allow(clippy::needless_pass_by_value)]
455fn derive_js_data(s: Structure<'_>) -> proc_macro2::TokenStream {
456    s.unbound_impl(quote!(::boa_engine::JsData), quote!())
457}
458
459/// Derives the `TryFromJs` trait, with the `#[boa()]` attribute.
460///
461/// # Panics
462///
463/// It will panic if the user tries to derive the `TryFromJs` trait in an `enum` or a tuple struct.
464#[proc_macro_derive(TryFromJs, attributes(boa))]
465pub fn derive_try_from_js(input: TokenStream) -> TokenStream {
466    // Parse the input tokens into a syntax tree
467    let input = parse_macro_input!(input as DeriveInput);
468
469    let Data::Struct(data) = input.data else {
470        panic!("you can only derive TryFromJs for structs");
471    };
472
473    let Fields::Named(fields) = data.fields else {
474        panic!("you can only derive TryFromJs for named-field structs")
475    };
476
477    let renaming = match RenameScheme::from_named_attrs(&mut input.attrs.clone(), "rename_all") {
478        Ok(renaming) => renaming.unwrap_or(RenameScheme::None),
479        Err((span, msg)) => return syn::Error::new(span, msg).to_compile_error().into(),
480    };
481
482    let conv = generate_conversion(fields, renaming).unwrap_or_else(to_compile_errors);
483
484    let type_name = input.ident;
485
486    // Build the output, possibly using quasi-quotation
487    let expanded = quote! {
488        impl ::boa_engine::value::TryFromJs for #type_name {
489            fn try_from_js(value: &boa_engine::JsValue, context: &mut boa_engine::Context)
490                -> boa_engine::JsResult<Self> {
491                let o = value.as_object().ok_or_else(|| ::boa_engine::JsError::from(
492                    ::boa_engine::JsNativeError::typ()
493                        .with_message("value is not an object")
494                ))?;
495                #conv
496            }
497        }
498    };
499
500    // Hand the output tokens back to the compiler
501    expanded.into()
502}
503
504/// Generates the conversion field by field.
505fn generate_conversion(
506    fields: FieldsNamed,
507    rename: RenameScheme,
508) -> Result<proc_macro2::TokenStream, Vec<syn::Error>> {
509    use syn::spanned::Spanned;
510
511    let mut field_list = Vec::with_capacity(fields.named.len());
512    let mut final_fields = Vec::with_capacity(fields.named.len());
513
514    for field in fields.named {
515        let span = field.span();
516        let name = field.ident.ok_or_else(|| {
517            vec![syn::Error::new(
518                span,
519                "you can only derive `TryFromJs` for named-field structs",
520            )]
521        })?;
522
523        field_list.push(name.clone());
524
525        let mut from_js_with = None;
526        let mut field_name = rename.rename(format!("{name}"));
527        if let Some(attr) = field
528            .attrs
529            .into_iter()
530            .find(|attr| attr.path().is_ident("boa"))
531        {
532            attr.parse_nested_meta(|meta| {
533                if meta.path.is_ident("from_js_with") {
534                    let value = meta.value()?;
535                    from_js_with = Some(value.parse::<LitStr>()?);
536                    Ok(())
537                } else if meta.path.is_ident("rename") {
538                    let value = meta.value()?;
539                    field_name = value.parse::<LitStr>()?.value();
540                    Ok(())
541                } else {
542                    Err(meta.error(
543                        "invalid syntax in the `#[boa()]` attribute. \
544                              Note that this attribute only accepts the following syntax: \
545                            `#[boa(from_js_with = \"fully::qualified::path\")]`",
546                    ))
547                }
548            })
549            .map_err(|err| vec![err])?;
550        }
551
552        let error_str = format!("cannot get property {name} of value");
553        final_fields.push(quote! {
554            let #name = match props.get(&::boa_engine::js_string!(#field_name).into()) {
555                Some(pd) => pd.value().ok_or_else(|| ::boa_engine::JsError::from(
556                        ::boa_engine::JsNativeError::typ().with_message(#error_str)
557                    ))?.clone().try_js_into(context)?,
558                None => ::boa_engine::JsValue::undefined().try_js_into(context)?,
559            };
560        });
561
562        if let Some(method) = from_js_with {
563            let ident = Ident::new(&method.value(), method.span());
564            final_fields.push(quote! {
565                let #name = #ident(&#name, context)?;
566            });
567        }
568    }
569
570    // TODO: this could possibly skip accessors. Consider using `JsObject::get` instead.
571    Ok(quote! {
572        let o = o.borrow();
573        let props = o.properties();
574        #(#final_fields)*
575        Ok(Self {
576            #(#field_list),*
577        })
578    })
579}
580
581/// Generates a list of compile errors.
582#[allow(clippy::needless_pass_by_value)]
583fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
584    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
585    quote!(#(#compile_errors)*)
586}
587
588/// Derives the `TryIntoJs` trait, with the `#[boa()]` attribute.
589///
590/// # Panics
591///
592/// It will panic if the user tries to derive the `TryIntoJs` trait in an `enum` or a tuple struct.
593#[proc_macro_derive(TryIntoJs, attributes(boa))]
594pub fn derive_try_into_js(input: TokenStream) -> TokenStream {
595    // Parse the input tokens into a syntax tree
596    let input = parse_macro_input!(input as DeriveInput);
597
598    let Data::Struct(data) = input.data else {
599        panic!("you can only derive TryFromJs for structs");
600    };
601    // TODO: Enums ?
602
603    let Fields::Named(fields) = data.fields else {
604        panic!("you can only derive TryFromJs for named-field structs")
605    };
606
607    let renaming = match RenameScheme::from_named_attrs(&mut input.attrs.clone(), "rename_all") {
608        Ok(renaming) => renaming.unwrap_or(RenameScheme::None),
609        Err((span, msg)) => return syn::Error::new(span, msg).to_compile_error().into(),
610    };
611
612    let props = generate_obj_properties(fields, renaming)
613        .map_err(|err| vec![err])
614        .unwrap_or_else(to_compile_errors);
615
616    let type_name = input.ident;
617
618    // Build the output, possibly using quasi-quotation
619    let expanded = quote! {
620        impl ::boa_engine::value::TryIntoJs for #type_name {
621            fn try_into_js(&self, context: &mut boa_engine::Context) -> boa_engine::JsResult<boa_engine::JsValue> {
622                let obj = boa_engine::JsObject::default(context.intrinsics());
623                #props
624                boa_engine::JsResult::Ok(obj.into())
625            }
626        }
627    };
628
629    // Hand the output tokens back to the compiler
630    expanded.into()
631}
632
633/// Generates property creation for object.
634fn generate_obj_properties(
635    fields: FieldsNamed,
636    renaming: RenameScheme,
637) -> Result<proc_macro2::TokenStream, syn::Error> {
638    use syn::spanned::Spanned;
639
640    let mut prop_ctors = Vec::with_capacity(fields.named.len());
641
642    for field in fields.named {
643        let span = field.span();
644        let name = field.ident.ok_or_else(|| {
645            syn::Error::new(
646                span,
647                "you can only derive `TryIntoJs` for named-field structs",
648            )
649        })?;
650
651        let mut into_js_with = None;
652        let mut prop_key = renaming.rename(format!("{name}"));
653        let mut skip = false;
654
655        for attr in field
656            .attrs
657            .into_iter()
658            .filter(|attr| attr.path().is_ident("boa"))
659        {
660            attr.parse_nested_meta(|meta| {
661                if meta.path.is_ident("into_js_with") {
662                    let value = meta.value()?;
663                    into_js_with = Some(value.parse::<LitStr>()?);
664                    Ok(())
665                } else if meta.path.is_ident("rename") {
666                    let value = meta.value()?;
667                    prop_key = value.parse::<LitStr>()?.value();
668                    Ok(())
669                } else if meta.path.is_ident("skip") & meta.input.is_empty() {
670                    skip = true;
671                    Ok(())
672                } else {
673                    Err(meta.error(
674                        "invalid syntax in the `#[boa()]` attribute. \
675                              Note that this attribute only accepts the following syntax: \
676                            \n* `#[boa(into_js_with = \"fully::qualified::path\")]`\
677                            \n* `#[boa(rename = \"jsPropertyName\")]` \
678                            \n* `#[boa(skip)]` \
679                            ",
680                    ))
681                }
682            })?;
683        }
684
685        if skip {
686            continue;
687        }
688
689        let value = if let Some(into_js_with) = into_js_with {
690            let into_js_with = Ident::new(&into_js_with.value(), into_js_with.span());
691            quote! { #into_js_with(&self.#name, context)? }
692        } else {
693            quote! { boa_engine::value::TryIntoJs::try_into_js(&self.#name, context)? }
694        };
695        prop_ctors.push(quote! {
696            obj.create_data_property_or_throw(boa_engine::js_string!(#prop_key), #value, context)?;
697        });
698    }
699
700    Ok(quote! { #(#prop_ctors)* })
701}