meta_merge_core 0.1.0

Core logic for meta_merge
Documentation
use crate::extensions::item::ItemFnStructEnum;
use crate::flag_or_meta::FlagOrMeta;
use crate::macro_api::ApiPaths;
use crate::macro_api::copy::{CopyArgs, macro_rules_copy_inner_tokens};
use crate::macro_api::merge::{MergeArgs, macro_rules_merge_inner_tokens};
use crate::macros::ok_or_compiler_error;
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::spanned::Spanned;
use syn::*;

#[derive(FromMeta, Default)]
#[darling(default, derive_syn_parse)]
struct ExportArgs {
    /// when present, the macro will be named after the value (prefixed with `Copy` and/or `Merge`)
    /// when not present, the macro will be named after the item's ident
    name: Option<Ident>,
    /// when present, merges all attribute metas into the target items
    copy: FlagOrMeta<CopyArgs>,
    /// when present, merges all attribute metas into the target items
    merge: FlagOrMeta<MergeArgs>,
    /// when present, filter out all attributes that are in the blacklist
    blacklist: darling::util::Flag,
    /// acts as a whitelist when `blacklist` isn't set, otherwise only attributes in the `filter` will be exported
    filter: darling::util::PathList,
    /// #[macro_export]
    export: darling::util::Flag,
    /// pub(crate) use macro;
    pub_crate_use: darling::util::Flag,
    #[cfg(test)]
    /// macro_rules! emits to `TokenStream` for testing
    test: darling::util::Flag,
}

impl ExportArgs {
    fn resolve_macro_rules_ident(&self, prefix: &str, item: &ItemFnStructEnum) -> Ident {
        let ident = self.name.as_ref().unwrap_or_else(|| item.ident());
        format_ident!("{prefix}{ident}")
    }
}

impl ToTokens for ExportArgs {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let mut items = vec![];
        // if let Some(suffix) = &self.suffix {
        //     items.push(quote! { suffix = #suffix });
        // }
        if self.blacklist.is_present() {
            items.push(quote! { blacklist });
        }
        if !self.filter.is_empty() {
            for filter_path in self.filter.iter() {
                items.push(quote! { filter(#filter_path) })
            }
        }
        tokens.extend(quote! { #(#items),* });
    }
}

pub fn export_attr_outer(
    attr: impl Into<TokenStream>,
    input: impl Into<TokenStream>,
    api_paths: impl Into<ApiPaths>,
) -> TokenStream {
    ok_or_compiler_error!(export_attr_inner(attr.into(), input.into(), api_paths))
}

fn export_attr_inner(
    attr: TokenStream,
    input: TokenStream,
    api_paths: impl Into<ApiPaths>,
) -> Result<TokenStream> {
    let item = parse2::<Item>(input)?;
    export_attr_inner_item(attr, item, api_paths)
}

fn export_attr_inner_item(
    attr: TokenStream,
    item: Item,
    api_paths: impl Into<ApiPaths>,
) -> Result<TokenStream> {
    let args: ExportArgs = parse2::<ExportArgs>(attr)?;
    Ok(export(args, item, api_paths))
}

fn export(args: ExportArgs, mut item: Item, api_paths: impl Into<ApiPaths>) -> TokenStream {
    let api_paths = api_paths.into();
    let item = ok_or_compiler_error!(ItemFnStructEnum::try_from(&mut item));

    let mut item_attrs = item.clone_attrs();

    if args.blacklist.is_present() {
        item_attrs.retain(|attr| !args.filter.contains(attr.path()));
    } else if !args.filter.is_empty() {
        item_attrs.retain(|attr| args.filter.contains(attr.path()));
    }

    let target_idents = item_attrs
        .iter()
        .map(|attr| attr.path())
        .collect::<Vec<_>>();

    let macro_export_tokens = if args.export.is_present() {
        quote! {
            #[macro_export]
        }
    } else {
        quote! {}
    };

    let get_target_idents = move || {
        target_idents
            .clone()
            .into_iter()
            .cloned()
            .collect::<Vec<_>>()
    };

    let mut exports = vec![];

    if args.copy.is_present() {
        let mut copy_args = args
            .copy
            .inner_meta
            .as_ref()
            .cloned()
            .unwrap_or_else(|| CopyArgs::from_path_vec(vec![]));

        copy_args.extend_targets(get_target_idents());
        exports.push((
            args.resolve_macro_rules_ident("Copy", &item),
            macro_rules_copy_inner_tokens(copy_args, &item_attrs, &api_paths),
        ));
    }

    if args.merge.is_present() {
        let mut merge_args = args
            .merge
            .inner_meta
            .as_ref()
            .cloned()
            .unwrap_or_else(|| MergeArgs::from_path_vec(vec![]));
        merge_args.extend_targets(get_target_idents());
        exports.push((
            args.resolve_macro_rules_ident("Merge", &item),
            macro_rules_merge_inner_tokens(merge_args, &item_attrs, &api_paths),
        ));
    }

    let mut macro_rules_tokens = quote! {};

    for (macro_rules_ident, inner_macro) in exports {
        let pub_crate_use_tokens = if args.pub_crate_use.is_present() {
            quote! {
                pub(crate) use #macro_rules_ident;
            }
        } else {
            quote! {}
        };

        #[cfg(test)]
        let inner_macro = if args.test.is_present() {
            quote!(::qoute::quote! { #inner_macro })
        } else {
            inner_macro
        };

        macro_rules_tokens.extend(quote! {
            #macro_export_tokens
            #[allow(non_snake_case)]
            macro_rules! #macro_rules_ident {
                ($($tokens:tt)*) => {
                    #inner_macro
                }
            }
            #pub_crate_use_tokens
        });
    }

    if macro_rules_tokens.is_empty() {
        return Error::new(
            args.span(),
            "no macros exported, specify `copy` and/or `merge`",
        )
        .to_compile_error();
    }

    quote! {
        #item

        #macro_rules_tokens
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::assert_eq_ts_pretty_print;
    use crate::macro_api::test_paths::*;
    use quote::quote;

    #[test]
    fn test_export_copy_merge_name() -> Result<()> {
        let export_attr: Attribute = parse_quote! {
            #[#ATTR_EXPORT(pub_crate_use, copy, merge, name = FooBar)]
        };
        let item = parse_quote! {
            #[derive(Debug)]
            struct Foo;
        };
        let result_tokens = export_attr_inner_item(
            export_attr.meta.require_list()?.tokens.clone(),
            item,
            API_PATHS,
        )?;
        assert_eq_ts_pretty_print!(
            result_tokens,
            quote! {
                #[derive(Debug)]
                struct Foo;

                #[allow(non_snake_case)]
                macro_rules! CopyFooBar {
                    ($($tokens:tt)*) => {
                        #[#ATTR_COPY_SOURCE_START(targets(derive))]
                        #[derive(Debug)]
                        #[#ATTR_COPY_SOURCE_END]
                        $($tokens)*
                    }
                }
                pub(crate) use CopyFooBar;

                                #[allow(non_snake_case)]
                macro_rules! MergeFooBar {
                    ($($tokens:tt)*) => {
                        #[#ATTR_MERGE_SOURCE_START(targets(derive))]
                        #[derive(Debug)]
                        #[#ATTR_MERGE_SOURCE_END]
                        $($tokens)*
                    }
                }
                pub(crate) use MergeFooBar;
            }
        );
        Ok(())
    }

    #[test]
    fn test_export_copy() -> Result<()> {
        let export_attr: Attribute = parse_quote! {
            #[#ATTR_EXPORT(pub_crate_use, copy)]
        };
        let item = parse_quote! {
            #[derive(Debug)]
            struct Foo;
        };
        let result_tokens = export_attr_inner_item(
            export_attr.meta.require_list()?.tokens.clone(),
            item,
            API_PATHS,
        )?;
        assert_eq_ts_pretty_print!(
            result_tokens,
            quote! {
                #[derive(Debug)]
                struct Foo;

                #[allow(non_snake_case)]
                macro_rules! CopyFoo {
                    ($($tokens:tt)*) => {
                        #[#ATTR_COPY_SOURCE_START(targets(derive))]
                        #[derive(Debug)]
                        #[#ATTR_COPY_SOURCE_END]
                        $($tokens)*
                    }
                }
                pub(crate) use CopyFoo;
            }
        );
        Ok(())
    }

    #[test]
    fn test_export_merge() -> Result<()> {
        let export_attr: Attribute = parse_quote! {
            #[#ATTR_EXPORT(pub_crate_use, merge)]
        };
        let item = parse_quote! {
            #[derive(Debug)]
            struct Foo;
        };
        let result_tokens = export_attr_inner_item(
            export_attr.meta.require_list()?.tokens.clone(),
            item,
            API_PATHS,
        )?;
        assert_eq_ts_pretty_print!(
            result_tokens,
            quote! {
                #[derive(Debug)]
                struct Foo;

                #[allow(non_snake_case)]
                macro_rules! MergeFoo {
                    ($($tokens:tt)*) => {
                        #[#ATTR_MERGE_SOURCE_START(targets(derive))]
                        #[derive(Debug)]
                        #[#ATTR_MERGE_SOURCE_END]
                        $($tokens)*
                    }
                }
                pub(crate) use MergeFoo;
            }
        );
        Ok(())
    }
}