rquickjs-macro 0.5.1

Procedural macros for rquickjs
Documentation
use convert_case::Casing;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
    parse::{Parse, ParseStream},
    Attribute, Ident, LitStr, Type, Visibility,
};

use crate::{
    attrs::{take_attributes, FlagOption, OptionList, ValueOption},
    common::{kw, AbortResultExt, Case},
};

#[derive(Default, Debug)]
pub struct FieldConfig {
    pub get: bool,
    pub set: bool,
    pub enumerable: bool,
    pub configurable: bool,
    pub skip_trace: bool,
    pub rename: Option<String>,
}

#[derive(Debug)]
pub(crate) enum FieldOption {
    Get(FlagOption<kw::get>),
    Set(FlagOption<kw::set>),
    Enumerable(FlagOption<kw::enumerable>),
    Configurable(FlagOption<kw::configurable>),
    SkipTrace(FlagOption<kw::skip_trace>),
    Rename(ValueOption<kw::rename, LitStr>),
}

impl Parse for FieldOption {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.peek(kw::get) {
            input.parse().map(Self::Get)
        } else if input.peek(kw::set) {
            input.parse().map(Self::Set)
        } else if input.peek(kw::enumerable) {
            input.parse().map(Self::Enumerable)
        } else if input.peek(kw::configurable) {
            input.parse().map(Self::Configurable)
        } else if input.peek(kw::skip_trace) {
            input.parse().map(Self::SkipTrace)
        } else if input.peek(kw::rename) {
            input.parse().map(Self::Rename)
        } else {
            Err(syn::Error::new(
                input.span(),
                "invalid class field attribute",
            ))
        }
    }
}

impl FieldConfig {
    pub(crate) fn from_attributes(attrs: &mut Vec<syn::Attribute>) -> Self {
        let mut config = Self::default();

        take_attributes(attrs, |attr| {
            if !attr.path().is_ident("qjs") {
                return Ok(false);
            }

            let separated_options: OptionList<FieldOption> = attr.parse_args()?;
            separated_options.0.iter().for_each(|x| config.apply(x));
            Ok(true)
        })
        .unwrap_or_abort();

        config
    }

    pub(crate) fn apply(&mut self, option: &FieldOption) {
        match option {
            FieldOption::Get(ref x) => {
                self.get = x.is_true();
            }
            FieldOption::Set(ref x) => {
                self.set = x.is_true();
            }
            FieldOption::Enumerable(ref x) => {
                self.enumerable = x.is_true();
            }
            FieldOption::Configurable(ref x) => {
                self.configurable = x.is_true();
            }
            FieldOption::SkipTrace(ref x) => {
                self.skip_trace = x.is_true();
            }
            FieldOption::Rename(ref x) => {
                self.rename = Some(x.value.value());
            }
        }
    }
}

#[derive(Debug)]
pub(crate) enum Fields {
    Named(Vec<Field>),
    Unnamed(Vec<Field>),
    Unit,
}

impl Fields {
    pub fn from_fields(input: syn::Fields) -> Self {
        match input {
            syn::Fields::Named(x) => {
                Fields::Named(x.named.into_iter().map(Field::from_field).collect())
            }
            syn::Fields::Unnamed(x) => {
                Fields::Unnamed(x.unnamed.into_iter().map(Field::from_field).collect())
            }
            syn::Fields::Unit => Self::Unit,
        }
    }
}

#[derive(Debug)]
pub(crate) struct Field {
    pub config: FieldConfig,
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub ident: Option<Ident>,
    pub ty: Type,
}

impl Field {
    fn from_field(
        syn::Field {
            mut attrs,
            vis,
            ident,
            ty,
            ..
        }: syn::Field,
    ) -> Self {
        let config = FieldConfig::from_attributes(&mut attrs);
        Field {
            config,
            attrs,
            vis,
            ident,
            ty,
        }
    }
}

impl Field {
    pub fn expand_prop_config(&self) -> TokenStream {
        let mut res = TokenStream::new();
        if self.config.configurable {
            res.extend(quote!(.configurable()));
        }
        if self.config.enumerable {
            res.extend(quote!(.enumerable()));
        }
        res
    }

    pub fn expand_trace_body_named(&self, lib_crate: &Ident) -> TokenStream {
        if self.config.skip_trace {
            return TokenStream::new();
        }
        let field = self.ident.as_ref().unwrap();

        quote! {
            #lib_crate::class::Trace::<'js>::trace(&self.#field,_tracer);
        }
    }

    pub fn expand_trace_body_unnamed(&self, crate_name: &Ident, which: u32) -> TokenStream {
        if self.config.skip_trace {
            return TokenStream::new();
        }
        let field = format_ident!("{which}");

        quote! {
            #crate_name::class::Trace::<'js>::trace(&self.#field,_tracer);
        }
    }

    pub fn expand_property_named(&self, crate_name: &Ident, case: Option<Case>) -> TokenStream {
        if !(self.config.get || self.config.set) {
            return TokenStream::new();
        }

        let field = self.ident.as_ref().unwrap();
        let ty = &self.ty;

        let accessor = self.expand_accessor(field, crate_name, ty);
        let prop_config = self.expand_prop_config();
        let name = if let Some(rename) = self.config.rename.clone() {
            rename
        } else if let Some(case) = case {
            field.to_string().to_case(case.to_convert_case())
        } else {
            field.to_string()
        };

        quote! {
            proto.prop(#name, #accessor #prop_config)?;
        }
    }

    pub fn expand_property_unnamed(&self, crate_name: &Ident, name: u32) -> TokenStream {
        if !(self.config.get || self.config.set) {
            return TokenStream::new();
        }

        let field = format_ident!("{}", name);
        let ty = &self.ty;
        let accessor = self.expand_accessor(&field, crate_name, ty);
        let prop_config = self.expand_prop_config();
        let name = if let Some(rename) = self.config.rename.clone() {
            quote!(#rename)
        } else {
            quote!(#name as u32)
        };

        quote! {
            proto.prop(#name, #accessor #prop_config)?;
        }
    }

    pub fn expand_accessor(&self, field: &Ident, crate_name: &Ident, ty: &Type) -> TokenStream {
        if self.config.get && self.config.set {
            quote! {
                #crate_name::object::Accessor::new(
                    |this: #crate_name::function::This<#crate_name::class::OwnedBorrow<'js, Self>>|{
                        this.0.#field.clone()
                    },
                    |mut this: #crate_name::function::This<#crate_name::class::OwnedBorrowMut<'js, Self>>, v: #ty|{
                        this.0.#field = v;
                    }
                )
            }
        } else if self.config.get {
            quote! {
                #crate_name::object::Accessor::new_get(
                    |this: #crate_name::function::This<#crate_name::class::OwnedBorrow<'js, Self>>|{
                        this.0.#field.clone()
                    },
                )
            }
        } else if self.config.set {
            quote! {
                #crate_name::object::Accessor::new_set(
                    |mut this: #crate_name::function::This<#crate_name::class::OwnedBorrowMut<'js, Self>>, v: #ty|{
                        this.0.#field = v;
                    }
                )
            }
        } else {
            panic!("called expand_accessor on non accessor field")
        }
    }

    pub fn expand_attrs(&self) -> TokenStream {
        if self.config.skip_trace {
            quote! {
                #[qjs(skip_trace)]
            }
        } else {
            TokenStream::new()
        }
    }

    pub fn expand_field(&self) -> TokenStream {
        let Field {
            ref ident,
            ref vis,
            ref ty,
            ref attrs,
            ..
        } = self;

        let rexported_attrs = self.expand_attrs();

        if let Some(ref ident) = ident {
            quote! {
                #(#attrs)*
                #rexported_attrs
                #vis #ident: #ty
            }
        } else {
            quote! {
                #(#attrs)*
                #rexported_attrs
                #vis #ty
            }
        }
    }
}