1#![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#[proc_macro]
31pub fn js_value(input: TokenStream) -> TokenStream {
32 value::js_value_impl(proc_macro2::TokenStream::from(input)).into()
33}
34
35#[proc_macro]
37pub fn js_object(input: TokenStream) -> TokenStream {
38 value::js_object_impl(proc_macro2::TokenStream::from(input)).into()
39}
40
41#[proc_macro]
48pub fn embed_module_inner(input: TokenStream) -> TokenStream {
49 embedded_module_loader::embed_module_impl(input)
50}
51
52#[proc_macro_attribute]
93pub fn boa_class(attr: TokenStream, item: TokenStream) -> TokenStream {
94 class::class_impl(attr, item)
95}
96
97#[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 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 ::static_assertions::const_assert!(COMMON_STRINGS.len() < usize::MAX);
221 COMMON_STRINGS
222 };
223
224 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#[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#[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_trace
298}
299
300#[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 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 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 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_finalize
439}
440
441#[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_js_data
451}
452
453#[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#[proc_macro_derive(TryFromJs, attributes(boa))]
465pub fn derive_try_from_js(input: TokenStream) -> TokenStream {
466 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 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 expanded.into()
502}
503
504fn 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 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#[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#[proc_macro_derive(TryIntoJs, attributes(boa))]
594pub fn derive_try_into_js(input: TokenStream) -> TokenStream {
595 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 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 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 expanded.into()
631}
632
633fn 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}