diff options
| author | HampusM <hampus@hampusmat.com> | 2023-01-30 21:29:21 +0100 | 
|---|---|---|
| committer | HampusM <hampus@hampusmat.com> | 2023-01-30 21:29:21 +0100 | 
| commit | 178267c701c233542078c09fe6b19802f9642dbd (patch) | |
| tree | 3d3010806f6509c062ca86dbbbe9c3a6cd2fe547 | |
| parent | 17ca46e95af38a914197958bbcc1e759865b6005 (diff) | |
feat: improve macro error messages
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | macros/Cargo.toml | 2 | ||||
| -rw-r--r-- | macros/src/declare_interface_args.rs | 9 | ||||
| -rw-r--r-- | macros/src/factory/declare_default_args.rs | 2 | ||||
| -rw-r--r-- | macros/src/factory/macro_args.rs | 2 | ||||
| -rw-r--r-- | macros/src/fn_trait.rs | 4 | ||||
| -rw-r--r-- | macros/src/injectable/dependency.rs | 149 | ||||
| -rw-r--r-- | macros/src/injectable/implementation.rs | 229 | ||||
| -rw-r--r-- | macros/src/injectable/macro_args.rs | 135 | ||||
| -rw-r--r-- | macros/src/lib.rs | 111 | ||||
| -rw-r--r-- | macros/src/macro_flag.rs | 43 | ||||
| -rw-r--r-- | macros/src/util/error.rs | 116 | ||||
| -rw-r--r-- | macros/src/util/iterator_ext.rs | 42 | ||||
| -rw-r--r-- | macros/src/util/mod.rs | 23 | 
14 files changed, 677 insertions, 192 deletions
| @@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "doc_cfg"]  default = ["prevent-circular"]  factory = ["syrette_macros/factory"]  prevent-circular = ["syrette_macros/prevent-circular"] -async = ["dep:async-trait", "dep:async-lock"] +async = ["dep:async-trait", "dep:async-lock", "syrette_macros/async"]  [[example]]  name = "factory" diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 3299983..d9ef7da 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -18,6 +18,7 @@ rustdoc-args = ["--cfg", "doc_cfg"]  [features]  factory = ["syrette/factory"]  prevent-circular = [] +async = []  [dependencies]  syn = { version = "1.0.96", features = ["full"] } @@ -27,6 +28,7 @@ uuid = { version = "0.8", features = ["v4"] }  regex = "1.6.0"  once_cell = "1.13.1"  thiserror = "1.0.37" +proc-macro-error = "1.0.4"  [dev_dependencies]  syrette = { version = "0.4.2", path = ".." } diff --git a/macros/src/declare_interface_args.rs b/macros/src/declare_interface_args.rs index bc15d2e..c924dca 100644 --- a/macros/src/declare_interface_args.rs +++ b/macros/src/declare_interface_args.rs @@ -40,13 +40,8 @@ impl Parse for DeclareInterfaceArgs                  }              } -            let flag_names = flags -                .iter() -                .map(|flag| flag.flag.to_string()) -                .collect::<Vec<_>>(); - -            if let Some(dupe_flag_name) = flag_names.iter().find_duplicate() { -                return Err(input.error(format!("Duplicate flag '{dupe_flag_name}'"))); +            if let Some((dupe_flag, _)) = flags.iter().find_duplicate() { +                return Err(input.error(format!("Duplicate flag '{}'", dupe_flag.flag)));              }              flags diff --git a/macros/src/factory/declare_default_args.rs b/macros/src/factory/declare_default_args.rs index 03d52bd..46185e3 100644 --- a/macros/src/factory/declare_default_args.rs +++ b/macros/src/factory/declare_default_args.rs @@ -47,7 +47,7 @@ impl Parse for DeclareDefaultFactoryMacroArgs              .map(|flag| flag.flag.to_string())              .collect::<Vec<_>>(); -        if let Some(dupe_flag_name) = flag_names.iter().find_duplicate() { +        if let Some((dupe_flag_name, _)) = flag_names.iter().find_duplicate() {              return Err(input.error(format!("Duplicate flag '{dupe_flag_name}'")));          } diff --git a/macros/src/factory/macro_args.rs b/macros/src/factory/macro_args.rs index bd09cdf..64d6e12 100644 --- a/macros/src/factory/macro_args.rs +++ b/macros/src/factory/macro_args.rs @@ -35,7 +35,7 @@ impl Parse for FactoryMacroArgs              .map(|flag| flag.flag.to_string())              .collect::<Vec<_>>(); -        if let Some(dupe_flag_name) = flag_names.iter().find_duplicate() { +        if let Some((dupe_flag_name, _)) = flag_names.iter().find_duplicate() {              return Err(input.error(format!("Duplicate flag '{dupe_flag_name}'")));          } diff --git a/macros/src/fn_trait.rs b/macros/src/fn_trait.rs index d88d391..31d7d95 100644 --- a/macros/src/fn_trait.rs +++ b/macros/src/fn_trait.rs @@ -2,7 +2,7 @@ use quote::ToTokens;  use syn::parse::Parse;  use syn::punctuated::Punctuated;  use syn::token::Paren; -use syn::{parenthesized, parse_str, Ident, Token, TraitBound, Type}; +use syn::{parenthesized, Ident, Token, TraitBound, Type};  /// A function trait. `dyn Fn(u32) -> String`  #[derive(Debug, Clone, PartialEq, Eq)] @@ -76,7 +76,7 @@ impl ToTokens for FnTrait          self.output.to_tokens(tokens);          if !self.trait_bounds.is_empty() { -            let plus: Token![+] = parse_str("+").unwrap(); +            let plus = <Token![+]>::default();              plus.to_tokens(tokens); diff --git a/macros/src/injectable/dependency.rs b/macros/src/injectable/dependency.rs index d9d904e..6ff0697 100644 --- a/macros/src/injectable/dependency.rs +++ b/macros/src/injectable/dependency.rs @@ -1,7 +1,9 @@ -use proc_macro2::Ident; +use proc_macro2::{Ident, Span}; +use syn::spanned::Spanned;  use syn::{parse2, FnArg, GenericArgument, LitStr, PathArguments, Type};  use crate::injectable::named_attr_input::NamedAttrInput; +use crate::util::error::diagnostic_error_enum;  use crate::util::syn_path::SynPathExt;  /// Interface for a representation of a dependency of a injectable type. @@ -40,29 +42,40 @@ impl IDependency for Dependency      {          let typed_new_method_arg = match new_method_arg {              FnArg::Typed(typed_arg) => Ok(typed_arg), -            FnArg::Receiver(_) => Err(DependencyError::UnexpectedSelfArgument), +            FnArg::Receiver(receiver_arg) => Err(DependencyError::UnexpectedSelf { +                self_token_span: receiver_arg.self_token.span, +            }),          }?;          let dependency_type_path = match typed_new_method_arg.ty.as_ref() {              Type::Path(arg_type_path) => Ok(arg_type_path),              Type::Reference(ref_type_path) => match ref_type_path.elem.as_ref() {                  Type::Path(arg_type_path) => Ok(arg_type_path), -                &_ => Err(DependencyError::TypeNotPath), +                other_type => Err(DependencyError::InvalidType { +                    type_span: other_type.span(), +                }),              }, -            &_ => Err(DependencyError::TypeNotPath), +            other_type => Err(DependencyError::InvalidType { +                type_span: other_type.span(), +            }),          }?; -        let ptr_path_segment = dependency_type_path -            .path -            .segments -            .last() -            .map_or_else(|| Err(DependencyError::PtrTypePathEmpty), Ok)?; +        let ptr_path_segment = dependency_type_path.path.segments.last().map_or_else( +            || { +                Err(DependencyError::MissingType { +                    arg_span: typed_new_method_arg.span(), +                }) +            }, +            Ok, +        )?;          let ptr_ident = ptr_path_segment.ident.clone(); -        let ptr_generic_args = &match &ptr_path_segment.arguments { +        let ptr_generic_args = match ptr_path_segment.arguments.clone() {              PathArguments::AngleBracketed(generic_args) => Ok(generic_args), -            &_ => Err(DependencyError::PtrTypeNoGenerics), +            _ => Err(DependencyError::DependencyTypeMissingGenerics { +                ptr_ident_span: ptr_ident.span(), +            }),          }?          .args; @@ -70,7 +83,9 @@ impl IDependency for Dependency              if let Some(GenericArgument::Type(interface)) = ptr_generic_args.first() {                  Ok(interface.clone())              } else { -                Err(DependencyError::PtrTypeNoGenerics) +                Err(DependencyError::DependencyTypeMissingGenerics { +                    ptr_ident_span: ptr_ident.span(), +                })              }?;          let arg_attrs = &typed_new_method_arg.attrs; @@ -84,15 +99,17 @@ impl IDependency for Dependency          let opt_named_attr_tokens = opt_named_attr.map(|attr| &attr.tokens); -        let opt_named_attr_input = if let Some(named_attr_tokens) = opt_named_attr_tokens -        { -            Some( -                parse2::<NamedAttrInput>(named_attr_tokens.clone()) -                    .map_err(DependencyError::ParseNamedAttrInputFailed)?, -            ) -        } else { -            None -        }; +        let opt_named_attr_input = +            if let Some(named_attr_tokens) = opt_named_attr_tokens { +                Some(parse2::<NamedAttrInput>(named_attr_tokens.clone()).map_err( +                    |err| DependencyError::InvalidNamedAttrInput { +                        arg_span: typed_new_method_arg.span(), +                        err, +                    }, +                )?) +            } else { +                None +            };          Ok(Self {              interface, @@ -117,23 +134,43 @@ impl IDependency for Dependency      }  } -#[derive(Debug, thiserror::Error)] +diagnostic_error_enum! {  pub enum DependencyError  { -    #[error("Unexpected 'self' argument in 'new' method")] -    UnexpectedSelfArgument, - -    #[error("Argument type must either be a path or a path reference")] -    TypeNotPath, - -    #[error("Expected pointer type path to not be empty")] -    PtrTypePathEmpty, - -    #[error("Expected pointer type to take generic arguments")] -    PtrTypeNoGenerics, - -    #[error("Failed to parse the input to a 'named' attribute")] -    ParseNamedAttrInputFailed(#[source] syn::Error), +    #[error("Unexpected 'self' parameter"), span = self_token_span] +    #[help("Remove the 'self' parameter"), span = self_token_span] +    UnexpectedSelf { +        self_token_span: Span +    }, + +    #[ +        error("Dependency type must either be a path or a path reference"), +        span = type_span +    ] +    InvalidType { +        type_span: Span +    }, + +    #[error("Dependency is missing a type"), span = arg_span] +    MissingType { +        arg_span: Span +    }, + +    #[ +        error("Expected dependency type to take generic parameters"), +        span = ptr_ident_span +    ] +    DependencyTypeMissingGenerics { +        ptr_ident_span: Span +    }, + +    #[error("Dependency has a 'named' attribute given invalid input"), span = arg_span] +    #[source(err)] +    InvalidNamedAttrInput { +        arg_span: Span, +        err: syn::Error +    }, +}  }  #[cfg(test)] @@ -162,9 +199,9 @@ mod tests      use crate::test_utils;      #[test] -    fn can_build_dependency() -> Result<(), Box<dyn Error>> +    fn can_build_dependency()      { -        assert_eq!( +        assert!(matches!(              Dependency::build(&FnArg::Typed(PatType {                  attrs: vec![],                  pat: Box::new(Pat::Verbatim(TokenStream::default().into())), @@ -177,17 +214,17 @@ mod tests                          ]))]                      ),                  ]))) -            }))?, -            Dependency { +            })), +            Ok(dependency) if dependency == Dependency {                  interface: test_utils::create_type(test_utils::create_path(&[                      PathSegment::from(format_ident!("Foo"))                  ])),                  ptr: format_ident!("TransientPtr"),                  name: None              } -        ); +        )); -        assert_eq!( +        assert!(matches!(              Dependency::build(&FnArg::Typed(PatType {                  attrs: vec![],                  pat: Box::new(Pat::Verbatim(TokenStream::default().into())), @@ -202,23 +239,21 @@ mod tests                          ]))]                      ),                  ]))) -            }))?, -            Dependency { +            })), +            Ok(dependency) if dependency == Dependency {                  interface: test_utils::create_type(test_utils::create_path(&[                      PathSegment::from(format_ident!("Bar"))                  ])),                  ptr: format_ident!("SingletonPtr"),                  name: None              } -        ); - -        Ok(()) +        ));      }      #[test] -    fn can_build_dependency_with_name() -> Result<(), Box<dyn Error>> +    fn can_build_dependency_with_name()      { -        assert_eq!( +        assert!(matches!(              Dependency::build(&FnArg::Typed(PatType {                  attrs: vec![Attribute {                      pound_token: Pound::default(), @@ -240,17 +275,17 @@ mod tests                          ]))]                      ),                  ]))) -            }))?, -            Dependency { +            })), +            Ok(dependency) if dependency == Dependency {                  interface: test_utils::create_type(test_utils::create_path(&[                      PathSegment::from(format_ident!("Foo"))                  ])),                  ptr: format_ident!("TransientPtr"),                  name: Some(LitStr::new("cool", Span::call_site()))              } -        ); +        )); -        assert_eq!( +        assert!(matches!(              Dependency::build(&FnArg::Typed(PatType {                  attrs: vec![Attribute {                      pound_token: Pound::default(), @@ -274,17 +309,15 @@ mod tests                          ]))]                      ),                  ]))) -            }))?, -            Dependency { +            })), +            Ok(dependency) if dependency == Dependency {                  interface: test_utils::create_type(test_utils::create_path(&[                      PathSegment::from(format_ident!("Bar"))                  ])),                  ptr: format_ident!("FactoryPtr"),                  name: Some(LitStr::new("awesome", Span::call_site()))              } -        ); - -        Ok(()) +        ));      }      #[test] diff --git a/macros/src/injectable/implementation.rs b/macros/src/injectable/implementation.rs index 9b7236c..0fd73de 100644 --- a/macros/src/injectable/implementation.rs +++ b/macros/src/injectable/implementation.rs @@ -1,11 +1,22 @@  use std::error::Error; -use proc_macro2::Ident; +use proc_macro2::{Ident, Span, TokenStream};  use quote::{format_ident, quote, ToTokens}; -use syn::parse::{Parse, ParseStream}; -use syn::{parse_str, ExprMethodCall, FnArg, Generics, ImplItemMethod, ItemImpl, Type}; - -use crate::injectable::dependency::IDependency; +use syn::spanned::Spanned; +use syn::{ +    parse2, +    parse_str, +    ExprMethodCall, +    FnArg, +    Generics, +    ImplItemMethod, +    ItemImpl, +    ReturnType, +    Type, +}; + +use crate::injectable::dependency::{DependencyError, IDependency}; +use crate::util::error::diagnostic_error_enum;  use crate::util::item_impl::find_impl_method_by_name_mut;  use crate::util::string::camelcase_to_snakecase;  use crate::util::syn_path::SynPathExt; @@ -19,36 +30,107 @@ pub struct InjectableImpl<Dep: IDependency>      pub self_type: Type,      pub generics: Generics,      pub original_impl: ItemImpl, + +    new_method: ImplItemMethod,  } -impl<Dep: IDependency> Parse for InjectableImpl<Dep> +impl<Dep: IDependency> InjectableImpl<Dep>  {      #[cfg(not(tarpaulin_include))] -    fn parse(input: ParseStream) -> syn::Result<Self> +    pub fn parse(input: TokenStream) -> Result<Self, InjectableImplError>      { -        let input_fork = input.fork(); +        let mut item_impl = parse2::<ItemImpl>(input).map_err(|err| { +            InjectableImplError::NotAImplementation { +                err_span: err.span(), +            } +        })?; -        let mut item_impl = input.parse::<ItemImpl>()?; +        if let Some((_, trait_path, _)) = item_impl.trait_ { +            return Err(InjectableImplError::TraitImpl { +                trait_path_span: trait_path.span(), +            }); +        } -        let new_method = find_impl_method_by_name_mut(&mut item_impl, "new") -            .map_or_else(|| Err(input_fork.error("Missing a 'new' method")), Ok)?; +        let item_impl_span = item_impl.self_ty.span(); -        let dependencies = -            Self::build_dependencies(new_method).map_err(|err| input.error(err))?; +        let new_method = find_impl_method_by_name_mut(&mut item_impl, "new").ok_or( +            InjectableImplError::MissingNewMethod { +                implementation_span: item_impl_span, +            }, +        )?; + +        let dependencies = Self::build_dependencies(new_method).map_err(|err| { +            InjectableImplError::ContainsAInvalidDependency { +                implementation_span: item_impl_span, +                err, +            } +        })?;          Self::remove_method_argument_attrs(new_method); +        let new_method = new_method.clone(); +          Ok(Self {              dependencies,              self_type: item_impl.self_ty.as_ref().clone(),              generics: item_impl.generics.clone(),              original_impl: item_impl, +            new_method,          })      } -} -impl<Dep: IDependency> InjectableImpl<Dep> -{ +    pub fn validate(&self) -> Result<(), InjectableImplError> +    { +        if matches!(self.new_method.sig.output, ReturnType::Default) { +            return Err(InjectableImplError::InvalidNewMethodReturnType { +                new_method_output_span: self.new_method.sig.output.span(), +                expected: "Self".to_string(), +                found: "()".to_string(), +            }); +        } + +        if let ReturnType::Type(_, ret_type) = &self.new_method.sig.output { +            if let Type::Path(path_type) = ret_type.as_ref() { +                if path_type +                    .path +                    .get_ident() +                    .map_or_else(|| true, |ident| *ident != "Self") +                { +                    return Err(InjectableImplError::InvalidNewMethodReturnType { +                        new_method_output_span: self.new_method.sig.output.span(), +                        expected: "Self".to_string(), +                        found: ret_type.to_token_stream().to_string(), +                    }); +                } +            } else { +                return Err(InjectableImplError::InvalidNewMethodReturnType { +                    new_method_output_span: self.new_method.sig.output.span(), +                    expected: "Self".to_string(), +                    found: ret_type.to_token_stream().to_string(), +                }); +            } +        } + +        if let Some(unsafety) = self.new_method.sig.unsafety { +            return Err(InjectableImplError::NewMethodUnsafe { +                unsafety_span: unsafety.span, +            }); +        } + +        if let Some(asyncness) = self.new_method.sig.asyncness { +            return Err(InjectableImplError::NewMethodAsync { +                asyncness_span: asyncness.span, +            }); +        } + +        if !self.new_method.sig.generics.params.is_empty() { +            return Err(InjectableImplError::NewMethodGeneric { +                generics_span: self.new_method.sig.generics.span(), +            }); +        } +        Ok(()) +    } +      #[cfg(not(tarpaulin_include))]      pub fn expand(&self, no_doc_hidden: bool, is_async: bool)          -> proc_macro2::TokenStream @@ -216,6 +298,36 @@ impl<Dep: IDependency> InjectableImpl<Dep>          }      } +    #[cfg(not(tarpaulin_include))] +    pub fn expand_dummy_blocking_impl(&self) -> proc_macro2::TokenStream +    { +        let generics = &self.generics; +        let self_type = &self.self_type; + +        let di_container_var = format_ident!("{}", DI_CONTAINER_VAR_NAME); +        let dependency_history_var = format_ident!("{}", DEPENDENCY_HISTORY_VAR_NAME); + +        quote! { +            impl #generics syrette::interfaces::injectable::Injectable< +                syrette::di_container::blocking::DIContainer, +                syrette::dependency_history::DependencyHistory +            > for #self_type +            { +                fn resolve( +                    #di_container_var: &std::rc::Rc< +                        syrette::di_container::blocking::DIContainer +                    >, +                    mut #dependency_history_var: syrette::dependency_history::DependencyHistory +                ) -> Result< +                    syrette::ptr::TransientPtr<Self>, +                    syrette::errors::injectable::InjectableError> +                { +                    unimplemented!(); +                } +            } +        } +    } +      fn create_get_dep_method_calls(          dependencies: &[Dep],          is_async: bool, @@ -288,8 +400,9 @@ impl<Dep: IDependency> InjectableImpl<Dep>          })      } -    fn build_dependencies(new_method: &ImplItemMethod) -        -> Result<Vec<Dep>, Box<dyn Error>> +    fn build_dependencies( +        new_method: &ImplItemMethod, +    ) -> Result<Vec<Dep>, DependencyError>      {          let new_method_args = &new_method.sig.inputs; @@ -338,6 +451,74 @@ impl<Dep: IDependency> InjectableImpl<Dep>      }  } +diagnostic_error_enum! { +pub enum InjectableImplError +{ +    #[ +        error("The 'injectable' attribute must be placed on a implementation"), +        span = err_span +    ] +    NotAImplementation +    { +        err_span: Span +    }, + +    #[ +        error("The 'injectable' attribute cannot be placed on a trait implementation"), +        span = trait_path_span +    ] +    TraitImpl +    { +        trait_path_span: Span +    }, + +    #[error("Missing a 'new' method"), span = implementation_span] +    #[note("Required by the 'injectable' attribute macro")] +    MissingNewMethod { +        implementation_span: Span +    }, + +    #[ +        error(concat!( +            "Invalid 'new' method return type. Expected it to be '{}'. ", +            "Found '{}'" +        ), expected, found), +        span = new_method_output_span +    ] +    InvalidNewMethodReturnType +    { +        new_method_output_span: Span, +        expected: String, +        found: String +    }, + +    #[error("'new' method is not allowed to be unsafe"), span = unsafety_span] +    #[note("Required by the 'injectable' attribute macro")] +    NewMethodUnsafe { +        unsafety_span: Span +    }, + +    #[error("'new' method is not allowed to be async"), span = asyncness_span] +    #[note("Required by the 'injectable' attribute macro")] +    NewMethodAsync { +        asyncness_span: Span +    }, + +    #[error("'new' method is not allowed to have generics"), span = generics_span] +    #[note("Required by the 'injectable' attribute macro")] +    NewMethodGeneric { +        generics_span: Span +    }, + +    #[error("Has a invalid dependency"), span = implementation_span] +    #[source(err)] +    ContainsAInvalidDependency { +        implementation_span: Span, +        err: DependencyError +    }, +} +} +  #[cfg(test)]  mod tests  { @@ -436,8 +617,8 @@ mod tests              .expect()              .returning(|_| Ok(MockIDependency::new())); -        let dependencies = -            InjectableImpl::<MockIDependency>::build_dependencies(&method)?; +        let dependencies = InjectableImpl::<MockIDependency>::build_dependencies(&method) +            .expect("Expected Ok");          assert_eq!(dependencies.len(), 2); @@ -445,7 +626,7 @@ mod tests      }      #[test] -    fn can_build_named_dependencies() -> Result<(), Box<dyn Error>> +    fn can_build_named_dependencies()      {          let method = ImplItemMethod {              attrs: vec![], @@ -517,12 +698,10 @@ mod tests              .returning(|_| Ok(MockIDependency::new()))              .times(2); -        let dependencies = -            InjectableImpl::<MockIDependency>::build_dependencies(&method)?; +        let dependencies = InjectableImpl::<MockIDependency>::build_dependencies(&method) +            .expect("Expected Ok");          assert_eq!(dependencies.len(), 2); - -        Ok(())      }      #[test] diff --git a/macros/src/injectable/macro_args.rs b/macros/src/injectable/macro_args.rs index d730e0d..6582cc6 100644 --- a/macros/src/injectable/macro_args.rs +++ b/macros/src/injectable/macro_args.rs @@ -1,8 +1,10 @@ +use proc_macro2::Span;  use syn::parse::{Parse, ParseStream};  use syn::punctuated::Punctuated; -use syn::{Token, TypePath}; +use syn::{Ident, Token, TypePath};  use crate::macro_flag::MacroFlag; +use crate::util::error::diagnostic_error_enum;  use crate::util::iterator_ext::IteratorExt;  pub const INJECTABLE_MACRO_FLAGS: &[&str] = @@ -14,9 +16,34 @@ pub struct InjectableMacroArgs      pub flags: Punctuated<MacroFlag, Token![,]>,  } +impl InjectableMacroArgs +{ +    pub fn check_flags(&self) -> Result<(), InjectableMacroArgsError> +    { +        for flag in &self.flags { +            if !INJECTABLE_MACRO_FLAGS.contains(&flag.flag.to_string().as_str()) { +                return Err(InjectableMacroArgsError::UnknownFlag { +                    flag_ident: flag.flag.clone(), +                }); +            } +        } + +        if let Some((dupe_flag_first, dupe_flag_second)) = +            self.flags.iter().find_duplicate() +        { +            return Err(InjectableMacroArgsError::DuplicateFlag { +                first_flag_ident: dupe_flag_first.flag.clone(), +                last_flag_span: dupe_flag_second.flag.span(), +            }); +        } + +        Ok(()) +    } +} +  impl Parse for InjectableMacroArgs  { -    fn parse(input: ParseStream) -> syn::Result<Self> +    fn parse(input: ParseStream) -> Result<Self, syn::Error>      {          let input_fork = input.fork(); @@ -50,31 +77,33 @@ impl Parse for InjectableMacroArgs          let flags = Punctuated::<MacroFlag, Token![,]>::parse_terminated(input)?; -        for flag in &flags { -            let flag_str = flag.flag.to_string(); - -            if !INJECTABLE_MACRO_FLAGS.contains(&flag_str.as_str()) { -                return Err(input.error(format!( -                    "Unknown flag '{}'. Expected one of [ {} ]", -                    flag_str, -                    INJECTABLE_MACRO_FLAGS.join(",") -                ))); -            } -        } - -        let flag_names = flags -            .iter() -            .map(|flag| flag.flag.to_string()) -            .collect::<Vec<_>>(); - -        if let Some(dupe_flag_name) = flag_names.iter().find_duplicate() { -            return Err(input.error(format!("Duplicate flag '{dupe_flag_name}'"))); -        } -          Ok(Self { interface, flags })      }  } +diagnostic_error_enum! { +pub enum InjectableMacroArgsError +{ +    #[error("Unknown flag '{flag_ident}'"), span = flag_ident.span()] +    #[ +        help("Expected one of: {}", INJECTABLE_MACRO_FLAGS.join(", ")), +        span = flag_ident.span() +    ] +    UnknownFlag +    { +        flag_ident: Ident +    }, + +    #[error("Duplicate flag '{first_flag_ident}'"), span = first_flag_ident.span()] +    #[note("Previously mentioned here"), span = last_flag_span] +    DuplicateFlag +    { +        first_flag_ident: Ident, +        last_flag_span: Span +    }, +} +} +  #[cfg(test)]  mod tests  { @@ -188,30 +217,60 @@ mod tests      }      #[test] -    fn cannot_parse_with_invalid_flag() +    fn can_parse_with_unknown_flag() -> Result<(), Box<dyn Error>>      {          let input_args = quote! {              IFoo, haha = true, async = false          }; -        assert!(matches!(parse2::<InjectableMacroArgs>(input_args), Err(_))); +        assert!(parse2::<InjectableMacroArgs>(input_args).is_ok()); + +        Ok(())      }      #[test] -    fn cannot_parse_with_duplicate_flag() +    fn can_parse_with_duplicate_flag()      { -        assert!(matches!( -            parse2::<InjectableMacroArgs>(quote! { -                IFoo, async = false, no_doc_hidden = true, async = false -            }), -            Err(_) -        )); +        assert!(parse2::<InjectableMacroArgs>(quote! { +            IFoo, async = false, no_doc_hidden = true, async = false +        }) +        .is_ok()); + +        assert!(parse2::<InjectableMacroArgs>(quote! { +            IFoo, async = true , no_doc_hidden = true, async = false +        }) +        .is_ok()); +    } -        assert!(matches!( -            parse2::<InjectableMacroArgs>(quote! { -                IFoo, async = true , no_doc_hidden = true, async = false -            }), -            Err(_) -        )); +    #[test] +    fn check_flags_fail_with_unknown_flag() -> Result<(), Box<dyn Error>> +    { +        let input_args = quote! { +            IFoo, haha = true, async = false +        }; + +        let injectable_macro_args = parse2::<InjectableMacroArgs>(input_args)?; + +        assert!(injectable_macro_args.check_flags().is_err()); + +        Ok(()) +    } + +    #[test] +    fn check_flags_fail_with_duplicate_flag() -> Result<(), Box<dyn Error>> +    { +        let macro_args = parse2::<InjectableMacroArgs>(quote! { +            IFoo, async = false, no_doc_hidden = true, async = false +        })?; + +        assert!(macro_args.check_flags().is_err()); + +        let macro_args_two = parse2::<InjectableMacroArgs>(quote! { +            IFoo, async = true , no_doc_hidden = true, async = false +        })?; + +        assert!(macro_args_two.check_flags().is_err()); + +        Ok(())      }  } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 91cc9f0..4c78204 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -6,18 +6,18 @@  //! Macros for the [Syrette](https://crates.io/crates/syrette) crate.  use proc_macro::TokenStream; +use proc_macro_error::{proc_macro_error, set_dummy, ResultExt};  use quote::quote;  use syn::punctuated::Punctuated;  use syn::token::Dyn; -use syn::{ -    parse, -    parse_macro_input, -    TraitBound, -    TraitBoundModifier, -    Type, -    TypeParamBound, -    TypeTraitObject, -}; +use syn::{parse, TraitBound, TraitBoundModifier, Type, TypeParamBound, TypeTraitObject}; + +use crate::caster::generate_caster; +use crate::declare_interface_args::DeclareInterfaceArgs; +use crate::injectable::dependency::Dependency; +use crate::injectable::implementation::InjectableImpl; +use crate::injectable::macro_args::InjectableMacroArgs; +use crate::macro_flag::MacroFlag;  mod caster;  mod declare_interface_args; @@ -36,11 +36,8 @@ mod fn_trait;  #[cfg(test)]  mod test_utils; -use crate::caster::generate_caster; -use crate::declare_interface_args::DeclareInterfaceArgs; -use crate::injectable::dependency::Dependency; -use crate::injectable::implementation::InjectableImpl; -use crate::injectable::macro_args::InjectableMacroArgs; +#[allow(dead_code)] +const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");  /// Makes a struct injectable. Thereby usable with [`DIContainer`] or  /// [`AsyncDIContainer`]. @@ -131,38 +128,62 @@ use crate::injectable::macro_args::InjectableMacroArgs;  /// [`Injectable`]: https://docs.rs/syrette/latest/syrette/interfaces/injectable/trait.Injectable.html  /// [`di_container_bind`]: https://docs.rs/syrette/latest/syrette/macro.di_container_bind.html  #[cfg(not(tarpaulin_include))] +#[proc_macro_error]  #[proc_macro_attribute] -pub fn injectable(args_stream: TokenStream, impl_stream: TokenStream) -> TokenStream +pub fn injectable(args_stream: TokenStream, input_stream: TokenStream) -> TokenStream  { -    let InjectableMacroArgs { interface, flags } = parse_macro_input!(args_stream); +    let input_stream: proc_macro2::TokenStream = input_stream.into(); + +    set_dummy(input_stream.clone()); -    let no_doc_hidden = flags +    let args = parse::<InjectableMacroArgs>(args_stream).unwrap_or_abort(); + +    args.check_flags().unwrap_or_abort(); + +    let no_doc_hidden = args +        .flags          .iter()          .find(|flag| flag.flag.to_string().as_str() == "no_doc_hidden")          .map_or(false, |flag| flag.is_on.value); -    let no_declare_concrete_interface = flags +    let no_declare_concrete_interface = args +        .flags          .iter()          .find(|flag| flag.flag.to_string().as_str() == "no_declare_concrete_interface")          .map_or(false, |flag| flag.is_on.value); -    let is_async = flags +    let is_async_flag = args +        .flags          .iter()          .find(|flag| flag.flag.to_string().as_str() == "async") -        .map_or(false, |flag| flag.is_on.value); +        .cloned() +        .unwrap_or_else(|| MacroFlag::new_off("async")); + +    #[cfg(not(feature = "async"))] +    if is_async_flag.is_on() { +        use proc_macro_error::abort; + +        abort!( +            is_async_flag.flag.span(), +            "The 'async' Cargo feature must be enabled to use this flag"; +            suggestion = "In your Cargo.toml: syrette = {{ version = \"{}\", features = [\"async\"] }}", +            PACKAGE_VERSION +        ); +    } -    let injectable_impl: InjectableImpl<Dependency> = match parse(impl_stream) { -        Ok(injectable_impl) => injectable_impl, -        Err(err) => { -            panic!("{err}"); -        } -    }; +    let injectable_impl = +        InjectableImpl::<Dependency>::parse(input_stream).unwrap_or_abort(); + +    set_dummy(injectable_impl.expand_dummy_blocking_impl()); + +    injectable_impl.validate().unwrap_or_abort(); -    let expanded_injectable_impl = injectable_impl.expand(no_doc_hidden, is_async); +    let expanded_injectable_impl = +        injectable_impl.expand(no_doc_hidden, is_async_flag.is_on());      let self_type = &injectable_impl.self_type; -    let opt_interface = interface.map(Type::Path).or_else(|| { +    let opt_interface = args.interface.map(Type::Path).or_else(|| {          if no_declare_concrete_interface {              None          } else { @@ -171,7 +192,7 @@ pub fn injectable(args_stream: TokenStream, impl_stream: TokenStream) -> TokenSt      });      let maybe_decl_interface = if let Some(interface) = opt_interface { -        let async_flag = if is_async { +        let async_flag = if is_async_flag.is_on() {              quote! {, async = true}          } else {              quote! {} @@ -233,17 +254,22 @@ pub fn injectable(args_stream: TokenStream, impl_stream: TokenStream) -> TokenSt  #[cfg(feature = "factory")]  #[cfg_attr(doc_cfg, doc(cfg(feature = "factory")))]  #[cfg(not(tarpaulin_include))] +#[proc_macro_error]  #[proc_macro_attribute] -pub fn factory(args_stream: TokenStream, type_alias_stream: TokenStream) -> TokenStream +pub fn factory(args_stream: TokenStream, input_stream: TokenStream) -> TokenStream  {      use quote::ToTokens; -    use syn::parse_str; +    use syn::{parse2, parse_str};      use crate::factory::build_declare_interfaces::build_declare_factory_interfaces;      use crate::factory::macro_args::FactoryMacroArgs;      use crate::factory::type_alias::FactoryTypeAlias; -    let FactoryMacroArgs { flags } = parse(args_stream).unwrap(); +    let input_stream: proc_macro2::TokenStream = input_stream.into(); + +    set_dummy(input_stream.clone()); + +    let FactoryMacroArgs { flags } = parse(args_stream).unwrap_or_abort();      let mut is_threadsafe = flags          .iter() @@ -264,7 +290,7 @@ pub fn factory(args_stream: TokenStream, type_alias_stream: TokenStream) -> Toke          mut factory_interface,          arg_types: _,          return_type: _, -    } = parse(type_alias_stream).unwrap(); +    } = parse2(input_stream).unwrap_or_abort();      let output = factory_interface.output.clone(); @@ -280,11 +306,11 @@ pub fn factory(args_stream: TokenStream, type_alias_stream: TokenStream) -> Toke          }          .into(),      ) -    .unwrap(); +    .unwrap_or_abort();      if is_threadsafe { -        factory_interface.add_trait_bound(parse_str("Send").unwrap()); -        factory_interface.add_trait_bound(parse_str("Sync").unwrap()); +        factory_interface.add_trait_bound(parse_str("Send").unwrap_or_abort()); +        factory_interface.add_trait_bound(parse_str("Sync").unwrap_or_abort());      }      type_alias.ty = Box::new(Type::Verbatim(factory_interface.to_token_stream())); @@ -332,6 +358,7 @@ pub fn factory(args_stream: TokenStream, type_alias_stream: TokenStream) -> Toke  #[cfg(feature = "factory")]  #[cfg_attr(doc_cfg, doc(cfg(feature = "factory")))]  #[cfg(not(tarpaulin_include))] +#[proc_macro_error]  #[proc_macro]  pub fn declare_default_factory(args_stream: TokenStream) -> TokenStream  { @@ -341,7 +368,8 @@ pub fn declare_default_factory(args_stream: TokenStream) -> TokenStream      use crate::factory::declare_default_args::DeclareDefaultFactoryMacroArgs;      use crate::fn_trait::FnTrait; -    let DeclareDefaultFactoryMacroArgs { interface, flags } = parse(args_stream).unwrap(); +    let DeclareDefaultFactoryMacroArgs { interface, flags } = +        parse(args_stream).unwrap_or_abort();      let mut is_threadsafe = flags          .iter() @@ -372,11 +400,11 @@ pub fn declare_default_factory(args_stream: TokenStream) -> TokenStream          }          .into(),      ) -    .unwrap(); +    .unwrap_or_abort();      if is_threadsafe { -        factory_interface.add_trait_bound(parse_str("Send").unwrap()); -        factory_interface.add_trait_bound(parse_str("Sync").unwrap()); +        factory_interface.add_trait_bound(parse_str("Send").unwrap_or_abort()); +        factory_interface.add_trait_bound(parse_str("Sync").unwrap_or_abort());      }      build_declare_factory_interfaces(&factory_interface, is_threadsafe).into() @@ -404,6 +432,7 @@ pub fn declare_default_factory(args_stream: TokenStream) -> TokenStream  /// declare_interface!(Ninja -> INinja);  /// ```  #[cfg(not(tarpaulin_include))] +#[proc_macro_error]  #[proc_macro]  pub fn declare_interface(input: TokenStream) -> TokenStream  { @@ -411,7 +440,7 @@ pub fn declare_interface(input: TokenStream) -> TokenStream          implementation,          interface,          flags, -    } = parse_macro_input!(input); +    } = parse(input).unwrap_or_abort();      let opt_async_flag = flags          .iter() diff --git a/macros/src/macro_flag.rs b/macros/src/macro_flag.rs index 97a8ff2..f0e3a70 100644 --- a/macros/src/macro_flag.rs +++ b/macros/src/macro_flag.rs @@ -1,22 +1,37 @@ +use std::hash::Hash; + +use proc_macro2::Span;  use syn::parse::{Parse, ParseStream};  use syn::{Ident, LitBool, Token}; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Eq, Clone)]  pub struct MacroFlag  {      pub flag: Ident,      pub is_on: LitBool,  } -impl Parse for MacroFlag +impl MacroFlag  { -    fn parse(input: ParseStream) -> syn::Result<Self> +    pub fn new_off(flag: &str) -> Self      { -        let input_forked = input.fork(); +        Self { +            flag: Ident::new(flag, Span::call_site()), +            is_on: LitBool::new(false, Span::call_site()), +        } +    } -        let flag: Ident = input_forked.parse()?; +    pub fn is_on(&self) -> bool +    { +        self.is_on.value +    } +} -        input.parse::<Ident>()?; +impl Parse for MacroFlag +{ +    fn parse(input: ParseStream) -> syn::Result<Self> +    { +        let flag = input.parse::<Ident>()?;          input.parse::<Token![=]>()?; @@ -26,6 +41,22 @@ impl Parse for MacroFlag      }  } +impl PartialEq for MacroFlag +{ +    fn eq(&self, other: &Self) -> bool +    { +        self.flag == other.flag +    } +} + +impl Hash for MacroFlag +{ +    fn hash<H: std::hash::Hasher>(&self, state: &mut H) +    { +        self.flag.hash(state); +    } +} +  #[cfg(test)]  mod tests  { diff --git a/macros/src/util/error.rs b/macros/src/util/error.rs new file mode 100644 index 0000000..d068661 --- /dev/null +++ b/macros/src/util/error.rs @@ -0,0 +1,116 @@ +/// Used to create a error enum that converts into a [`Diagnostic`]. +/// +/// [`Diagnostic`]: proc_macro_error::Diagnostic +macro_rules! diagnostic_error_enum { +    ($(#[$meta: meta])* $visibility: vis enum $name: ident { +        $( +            #[error($($error: tt)*), span = $error_span: expr] +            $(#[note($($note: tt)*)$(, span = $note_span: expr)?])* +            $(#[help($($help: tt)*)$(, span = $help_span: expr)?])* +            $(#[err($($err: tt)*)$(, span = $err_span: expr)?])* +            $(#[source($source: ident)])? +            $variant: ident { +                $($variant_field: ident: $variant_field_type: ty),* +            }, +        )* +    }) => { +        $(#[$meta])* +        #[derive(Debug, Clone)] +        $visibility enum $name +        { +            $( +                $variant { +                    $($variant_field: $variant_field_type),* +                }, +            )* +        } + +        impl From<$name> for ::proc_macro_error::Diagnostic +        { +            #[must_use] +            fn from(err: $name) -> Self +            { +                let (error, span, notes, helps, errs, source): ( +                    String, +                    ::proc_macro2::Span, +                    Vec<(String, ::proc_macro2::Span)>, +                    Vec<(String, ::proc_macro2::Span)>, +                    Vec<(String, ::proc_macro2::Span)>, +                    Option<::proc_macro_error::Diagnostic> +                ) = match err { +                    $( +                        $name::$variant { +                            $($variant_field),* +                        } => { +                            ( +                                format!($($error)*), +                                $error_span, +                                vec![$( +                                    ( +                                        format!($($note)*), +                                        $crate::util::or!( +                                            ($($note_span)?) +                                            else (::proc_macro2::Span::call_site()) +                                        ) +                                    ) +                                ),*], +                                vec![$( +                                    ( +                                        format!($($help)*), +                                        $crate::util::or!( +                                            ($($help_span)?) +                                            else (::proc_macro2::Span::call_site()) +                                        ) +                                    ) +                                ),*], +                                vec![$( +                                    ( +                                        format!($($err)*), +                                        $crate::util::or!( +                                            ($($err_span)?) +                                            else (::proc_macro2::Span::call_site()) +                                        ) +                                    ) +                                ),*], +                                $crate::util::to_option!($($source.into())?) +                            ) +                        } +                    ),* +                }; + +                if let Some(source_diagnostic) = source { +                    source_diagnostic.emit(); +                } + +                let mut diagnostic = ::proc_macro_error::Diagnostic::spanned( +                    span, +                    ::proc_macro_error::Level::Error, +                    error +                ); + +                if !notes.is_empty() { +                    for (note, note_span) in notes { +                        diagnostic = diagnostic.span_note(note_span, note); +                    } +                } + +                if !helps.is_empty() { +                    for (help, help_span) in helps { +                        diagnostic = diagnostic.span_help(help_span, help); +                    } +                } + +                if !errs.is_empty() { +                    for (err, err_span) in errs { +                        diagnostic = diagnostic.span_error(err_span, err); +                    } +                } + +                diagnostic +            } +        } + +    }; +} + +pub(crate) use diagnostic_error_enum; diff --git a/macros/src/util/iterator_ext.rs b/macros/src/util/iterator_ext.rs index 5001068..17482ae 100644 --- a/macros/src/util/iterator_ext.rs +++ b/macros/src/util/iterator_ext.rs @@ -1,26 +1,39 @@ -use std::collections::HashMap; +use std::collections::HashSet;  use std::hash::Hash; +/// [`Iterator`] extension trait.  pub trait IteratorExt<Item> +where +    Item: Eq + Hash,  { -    fn find_duplicate(&mut self) -> Option<Item>; +    /// Finds the first occurance of a duplicate item. +    /// +    /// This function is short-circuiting. So it will immedietly return `Some` when +    /// it comes across a item it has already seen. +    /// +    /// The returned tuple contains the first item occurance & the second item occurance. +    /// In that specific order. +    /// +    /// Both items are returned in the case of the hash not being representative of the +    /// whole item. +    fn find_duplicate(self) -> Option<(Item, Item)>;  }  impl<Iter> IteratorExt<Iter::Item> for Iter  where      Iter: Iterator, -    Iter::Item: Eq + Hash + Clone, +    Iter::Item: Eq + Hash,  { -    fn find_duplicate(&mut self) -> Option<Iter::Item> +    fn find_duplicate(self) -> Option<(Iter::Item, Iter::Item)>      { -        let mut iterated_item_map = HashMap::<Iter::Item, ()>::new(); +        let mut iterated_item_map = HashSet::<Iter::Item>::new();          for item in self { -            if iterated_item_map.contains_key(&item) { -                return Some(item); +            if let Some(equal_item) = iterated_item_map.take(&item) { +                return Some((item, equal_item));              } -            iterated_item_map.insert(item, ()); +            iterated_item_map.insert(item);          }          None @@ -33,7 +46,7 @@ mod tests      use super::*;      #[test] -    fn can_find_duplicate() +    fn can_find_dupe()      {          #[derive(Debug, PartialEq, Eq, Clone, Hash)]          struct Fruit @@ -58,9 +71,14 @@ mod tests              ]              .iter()              .find_duplicate(), -            Some(&Fruit { -                name: "Apple".to_string() -            }) +            Some(( +                &Fruit { +                    name: "Apple".to_string() +                }, +                &Fruit { +                    name: "Apple".to_string() +                } +            ))          );          assert_eq!( diff --git a/macros/src/util/mod.rs b/macros/src/util/mod.rs index 0705853..d3edb67 100644 --- a/macros/src/util/mod.rs +++ b/macros/src/util/mod.rs @@ -1,4 +1,27 @@ +pub mod error;  pub mod item_impl;  pub mod iterator_ext;  pub mod string;  pub mod syn_path; + +macro_rules! to_option { +    ($($tokens: tt)+) => { +        Some($($tokens)+) +    }; + +    () => { +        None +    }; +} + +macro_rules! or { +    (($($tokens: tt)+) else ($($default: tt)*)) => { +        $($tokens)* +    }; + +    (() else ($($default: tt)*)) => { +        $($default)* +    }; +} + +pub(crate) use {or, to_option}; | 
