testdata-macros 0.1.0

File-based testing helper -- subpackage for proc-macro
Documentation
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{parse, Attribute, FnArg, Lit, Meta, NestedMeta, Token};

#[derive(Debug, Clone)]
pub(crate) struct MacroArgs {
    pub(crate) rebuild: Option<String>,
    pub(crate) root: Option<String>,
}

impl MacroArgs {
    pub(crate) fn parse(raw: TokenStream) -> Result<Self, syn::Error> {
        let meta = parse::Parser::parse2(Punctuated::parse_terminated, raw)?;
        Self::parse_meta(&meta)
    }

    pub(crate) fn parse_meta(meta: &Punctuated<NestedMeta, Token![,]>) -> Result<Self, syn::Error> {
        let mut rebuild = None;
        let mut root = None;
        for arg in meta {
            if let NestedMeta::Meta(arg) = arg {
                if arg.path().is_ident("root") {
                    if root.is_some() {
                        return Err(syn::Error::new(arg.path().span(), "duplicate argument"));
                    }
                    if let Meta::NameValue(arg) = arg {
                        if let Lit::Str(lit) = &arg.lit {
                            root = Some(lit.value());
                            continue;
                        } else {
                            return Err(syn::Error::new(arg.lit.span(), "invalid argument value"));
                        }
                    } else {
                        return Err(syn::Error::new(arg.span(), "invalid argument value"));
                    }
                } else if arg.path().is_ident("rebuild") {
                    if rebuild.is_some() {
                        return Err(syn::Error::new(arg.path().span(), "duplicate argument"));
                    }
                    if let Meta::NameValue(arg) = arg {
                        if let Lit::Str(lit) = &arg.lit {
                            rebuild = Some(lit.value());
                            continue;
                        } else {
                            return Err(syn::Error::new(arg.lit.span(), "invalid argument value"));
                        }
                    } else {
                        return Err(syn::Error::new(arg.span(), "invalid argument value"));
                    }
                } else {
                    return Err(syn::Error::new(
                        arg.path().span(),
                        format_args!("unknown argument: {}", arg.path().to_token_stream()),
                    ));
                }
            } else {
                return Err(syn::Error::new(arg.span(), "invalid argument"));
            };
        }
        Ok(MacroArgs { rebuild, root })
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ArgAttrs {
    pub(crate) glob: String,
}

impl ArgAttrs {
    pub(crate) fn parse(arg: &FnArg) -> Result<Self, syn::Error> {
        let attrs = match arg {
            FnArg::Receiver(arg) => &arg.attrs,
            FnArg::Typed(arg) => &arg.attrs,
        };
        Self::parse_attrs(attrs, arg.span())
    }
    pub(crate) fn parse_attrs(attrs: &[Attribute], span: Span) -> Result<Self, syn::Error> {
        let mut glob = None;
        for attr in attrs {
            let meta = if let Ok(meta) = attr.parse_meta() {
                meta
            } else {
                continue;
            };
            if meta.path().is_ident("glob") {
                if glob.is_some() {
                    return Err(syn::Error::new(attr.span(), "Duplicate #[glob] attribute"));
                }
                let meta = if let Meta::NameValue(meta) = &meta {
                    meta
                } else {
                    return Err(syn::Error::new(meta.span(), "Expected #[glob = ...]"));
                };
                let lit = if let Lit::Str(lit) = &meta.lit {
                    lit
                } else {
                    return Err(syn::Error::new(
                        meta.lit.span(),
                        "Expected a string literal",
                    ));
                };
                glob = Some(lit.value());
            }
        }
        let glob = glob.ok_or_else(|| syn::Error::new(span, "Missing argument: #[glob = ...]"))?;
        Ok(Self { glob })
    }
}

#[cfg(test)]
mod tests {
    use quote::quote;
    use syn::parse_quote;

    use super::*;

    #[test]
    fn test_parse_macro_args_empty() {
        let args = quote! {};
        let args = MacroArgs::parse(args).unwrap();
        assert_eq!(args.rebuild, None);
        assert_eq!(args.root, None);
    }

    #[test]
    fn test_parse_macro_args_root() {
        let args = quote! {root = "."};
        let args = MacroArgs::parse(args).unwrap();
        assert_eq!(args.rebuild, None);
        assert_eq!(args.root, Some(".".to_owned()));
    }

    #[test]
    fn test_parse_macro_args_rebuild() {
        let args = quote! {rebuild = "tests/test.rs"};
        let args = MacroArgs::parse(args).unwrap();
        assert_eq!(args.rebuild, Some("tests/test.rs".to_owned()));
        assert_eq!(args.root, None);
    }

    #[test]
    fn test_parse_macro_args_unknown_arg() {
        let args = quote! {foo = 42};
        let e = MacroArgs::parse(args).unwrap_err();
        assert_eq!(e.to_string(), "unknown argument: foo");
    }

    #[test]
    fn test_parse_macro_args_duplicate_arg() {
        {
            let args = quote! {root = ".", root = "tests"};
            let e = MacroArgs::parse(args).unwrap_err();
            assert_eq!(e.to_string(), "duplicate argument");
        }
        {
            let args = quote! {rebuild = "tests/foo.rs", rebuild = "tests/bar.rs"};
            let e = MacroArgs::parse(args).unwrap_err();
            assert_eq!(e.to_string(), "duplicate argument");
        }
    }

    #[test]
    fn test_parse_macro_args_invalid_arg() {
        {
            let args = quote! {"foo"};
            let e = MacroArgs::parse(args).unwrap_err();
            assert_eq!(e.to_string(), "invalid argument");
        }
        {
            let args = quote! {root = 42};
            let e = MacroArgs::parse(args).unwrap_err();
            assert_eq!(e.to_string(), "invalid argument value");
        }
        {
            let args = quote! {root(".")};
            let e = MacroArgs::parse(args).unwrap_err();
            assert_eq!(e.to_string(), "invalid argument value");
        }
        {
            let args = quote! {rebuild = 42};
            let e = MacroArgs::parse(args).unwrap_err();
            assert_eq!(e.to_string(), "invalid argument value");
        }
        {
            let args = quote! {rebuild(".")};
            let e = MacroArgs::parse(args).unwrap_err();
            assert_eq!(e.to_string(), "invalid argument value");
        }
    }

    #[test]
    fn test_parse_arg_attrs() {
        let item = parse_quote! {
            #[glob = "tests/fixtures/**/*-in.txt"]
            #[other_attr.invalid]
            x: PathBuf
        };
        let attrs = ArgAttrs::parse(&item).unwrap();
        assert_eq!(attrs.glob, "tests/fixtures/**/*-in.txt");
    }

    #[test]
    fn test_parse_arg_attrs_self() {
        let item = parse_quote! {
            #[glob = "tests/fixtures/**/*-in.txt"]
            #[other_attr.invalid]
            &self
        };
        let attrs = ArgAttrs::parse(&item).unwrap();
        assert_eq!(attrs.glob, "tests/fixtures/**/*-in.txt");
    }

    #[test]
    fn test_parse_arg_attrs_empty() {
        let item = parse_quote! {
            x: PathBuf
        };
        let e = ArgAttrs::parse(&item).unwrap_err();
        assert_eq!(e.to_string(), "Missing argument: #[glob = ...]");
    }

    #[test]
    fn test_parse_arg_attrs_duplicate() {
        let item = parse_quote! {
            #[glob = "tests/fixtures/**/*-in.txt"]
            #[glob = "tests/fixtures/**/*-out.txt"]
            x: PathBuf
        };
        let e = ArgAttrs::parse(&item).unwrap_err();
        assert_eq!(e.to_string(), "Duplicate #[glob] attribute");
    }

    #[test]
    fn test_parse_arg_attrs_invalid_format() {
        let item = parse_quote! {
            #[glob("tests/fixtures/**/*-in.txt")]
            x: PathBuf
        };
        let e = ArgAttrs::parse(&item).unwrap_err();
        assert_eq!(e.to_string(), "Expected #[glob = ...]");
    }

    #[test]
    fn test_parse_arg_attrs_invalid_value() {
        let item = parse_quote! {
            #[glob = 42]
            x: PathBuf
        };
        let e = ArgAttrs::parse(&item).unwrap_err();
        assert_eq!(e.to_string(), "Expected a string literal");
    }
}