meta_merge_core 0.1.0

Core logic for meta_merge
Documentation
use crate::extensions::item::ItemFnStructEnum;
use crate::extensions::path::PathExt;
use crate::macro_api::*;
use crate::macros::ok_or_compiler_error;
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use std::collections::{HashSet, VecDeque};
use std::ops::Deref;
use syn::*;

#[derive(FromMeta, Default, Clone)]
#[darling(default, derive_syn_parse)]
pub(super) struct CopyArgs {
    /// attribute macros to affect
    pub targets: darling::util::PathList,
    pub dedupe: darling::util::Flag,
    pub prepend: darling::util::Flag,
}

#[allow(dead_code)]
impl CopyArgs {
    pub fn from_path_vec(targets: Vec<Path>) -> Self {
        Self {
            targets: darling::util::PathList::new(targets),
            ..Default::default()
        }
    }
    pub fn with_prepend(mut self) -> Self {
        self.prepend = darling::util::Flag::present();
        self
    }

    pub fn with_dedupe(mut self) -> Self {
        self.dedupe = darling::util::Flag::present();
        self
    }

    pub fn extend_targets(&mut self, mut targets: Vec<Path>) {
        let mut current_targets = self.targets.deref().clone();
        targets.retain(|target| !current_targets.contains(target));
        current_targets.extend(targets);
        self.targets = darling::util::PathList::new(current_targets);
    }
}

impl ToTokens for CopyArgs {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        if self.targets.is_empty() {
            return;
        }
        let targets = &self.targets;
        tokens.extend(quote! { targets(#(#targets),*) });
    }
}

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

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

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

fn copy(args: CopyArgs, mut item: Item, api_paths: impl Into<ApiPaths>) -> TokenStream {
    let api_paths = api_paths.into();
    let attr_copy_source_start = api_paths.copy_source_start().to_string();
    let attr_copy_source_end = api_paths.copy_source_end().to_string();
    let mut item = ok_or_compiler_error!(ItemFnStructEnum::try_from(&mut item));

    let item_attrs = item.take_attrs();

    let mut source_attrs: Vec<Attribute> = vec![];
    let mut target_attrs: VecDeque<Attribute> = VecDeque::new();

    let mut attr_copy_source_end_found = false;
    let mut is_filling_targets = false;

    let mut sources_to_remove: HashSet<usize> = HashSet::new();

    for attr in item_attrs {
        if attr.path().eq_str(&attr_copy_source_start) {
            if !attr_copy_source_end_found {
                panic!(
                    "unexpected start of source attributes without a preceding {attr_copy_source_end}"
                )
            }
            continue;
        }
        if !attr_copy_source_end_found && attr.path().eq_str(&attr_copy_source_end) {
            attr_copy_source_end_found = true;
            is_filling_targets = true;
            continue;
        }
        if is_filling_targets {
            // only check for duplicate attributes if it matches any of our targets
            if args.dedupe.is_present() && args.targets.contains(attr.path()) {
                'source: for (ix, source) in source_attrs.iter().enumerate() {
                    if source.path() != attr.path() {
                        continue;
                    }
                    // defer remove duplicate source attribute
                    if attr.meta == source.meta {
                        sources_to_remove.insert(ix);
                        break 'source;
                    }
                }
            }
            target_attrs.push_back(attr)
        } else {
            source_attrs.push(attr);
        }
    }

    if !attr_copy_source_end_found {
        panic!("unexpected end of attributes without {attr_copy_source_end}")
    }

    if args.dedupe.is_present() {
        let sources_to_remove = {
            let mut sources_to_remove = sources_to_remove.into_iter().collect::<Vec<_>>();
            sources_to_remove.sort();
            sources_to_remove.reverse();
            sources_to_remove
        };
        for ix in sources_to_remove.iter() {
            source_attrs.remove(*ix);
        }
    }

    for source in source_attrs {
        if args.prepend.is_present() {
            target_attrs.push_front(source);
        } else {
            target_attrs.push_back(source);
        }
    }

    item.put_attrs(target_attrs.into());

    quote! {
        #item
    }
}

pub(super) fn macro_rules_copy_inner_tokens(
    args: CopyArgs,
    item_attrs: &Vec<Attribute>,
    api_paths: &ApiPaths,
) -> TokenStream {
    let attr_start = api_paths.copy_source_start();
    let attr_end = api_paths.copy_source_end();
    quote! {
        #[#attr_start(#args)]
        #(#item_attrs)*
        #[#attr_end]
        $($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_copy() -> Result<()> {
        let copy_attr: Attribute = parse_quote! {
            #[#ATTR_COPY_SOURCE_START(targets(derive))]
        };
        let item = parse_quote! {
            #[derive(PartialEq)]
            #[#ATTR_COPY_SOURCE_END]
            #[derive(Debug)]
            struct Foo;
        };
        let result_tokens = copy_attr_item(
            copy_attr.meta.require_list()?.tokens.clone(),
            item,
            API_PATHS,
        )?;
        assert_eq_ts_pretty_print!(
            result_tokens,
            quote! {
                #[derive(Debug)]
                #[derive(PartialEq)]
                struct Foo;
            },
        );
        Ok(())
    }

    #[test]
    fn test_copy_prepend() -> Result<()> {
        let copy_attr: Attribute = parse_quote! {
            #[#ATTR_COPY_SOURCE_START(targets(derive), prepend)]
        };
        let item = parse_quote! {
            #[derive(PartialEq)]
            #[#ATTR_COPY_SOURCE_END]
            #[derive(Debug)]
            struct Foo;
        };
        let result_tokens = copy_attr_item(
            copy_attr.meta.require_list()?.tokens.clone(),
            item,
            API_PATHS,
        )?;
        assert_eq_ts_pretty_print!(
            result_tokens,
            quote! {
                #[derive(PartialEq)]
                #[derive(Debug)]
                struct Foo;
            },
        );
        Ok(())
    }

    #[test]
    fn test_copy_dedupe() -> Result<()> {
        let copy_attr: Attribute = parse_quote! {
            #[#ATTR_COPY_SOURCE_START(targets(derive), dedupe)]
        };
        let item = parse_quote! {
            #[derive(PartialEq)]
            #[derive(Debug)]
            #[#ATTR_COPY_SOURCE_END]
            #[derive(Debug)]
            struct Foo;
        };
        let result_tokens = copy_attr_item(
            copy_attr.meta.require_list()?.tokens.clone(),
            item,
            API_PATHS,
        )?;
        assert_eq_ts_pretty_print!(
            result_tokens,
            quote! {
                #[derive(Debug)]
                #[derive(PartialEq)]
                struct Foo;
            },
        );
        Ok(())
    }

    #[test]
    fn test_copy_no_dedupe() -> Result<()> {
        let copy_attr: Attribute = parse_quote! {
            #[#ATTR_COPY_SOURCE_START(targets(derive))]
        };
        let item = parse_quote! {
            #[derive(PartialEq)]
            #[derive(Debug)]
            #[#ATTR_COPY_SOURCE_END]
            #[derive(Debug)]
            struct Foo;
        };
        let result_tokens = copy_attr_item(
            copy_attr.meta.require_list()?.tokens.clone(),
            item,
            API_PATHS,
        )?;
        assert_eq_ts_pretty_print!(
            result_tokens,
            quote! {
                #[derive(Debug)]
                #[derive(PartialEq)]
                #[derive(Debug)]
                struct Foo;
            },
        );
        Ok(())
    }
}