From 178267c701c233542078c09fe6b19802f9642dbd Mon Sep 17 00:00:00 2001 From: HampusM Date: Mon, 30 Jan 2023 21:29:21 +0100 Subject: feat: improve macro error messages --- macros/src/declare_interface_args.rs | 9 +- macros/src/factory/declare_default_args.rs | 2 +- macros/src/factory/macro_args.rs | 2 +- macros/src/fn_trait.rs | 4 +- macros/src/injectable/dependency.rs | 149 +++++++++++-------- macros/src/injectable/implementation.rs | 229 +++++++++++++++++++++++++---- macros/src/injectable/macro_args.rs | 135 ++++++++++++----- macros/src/lib.rs | 111 ++++++++------ macros/src/macro_flag.rs | 43 +++++- macros/src/util/error.rs | 116 +++++++++++++++ macros/src/util/iterator_ext.rs | 42 ++++-- macros/src/util/mod.rs | 23 +++ 12 files changed, 674 insertions(+), 191 deletions(-) create mode 100644 macros/src/util/error.rs (limited to 'macros/src') 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::>(); - - 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::>(); - 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::>(); - 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 = ::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::(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::(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> + 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> + 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 pub self_type: Type, pub generics: Generics, pub original_impl: ItemImpl, + + new_method: ImplItemMethod, } -impl Parse for InjectableImpl +impl InjectableImpl { #[cfg(not(tarpaulin_include))] - fn parse(input: ParseStream) -> syn::Result + pub fn parse(input: TokenStream) -> Result { - let input_fork = input.fork(); + let mut item_impl = parse2::(input).map_err(|err| { + InjectableImplError::NotAImplementation { + err_span: err.span(), + } + })?; - let mut item_impl = input.parse::()?; + 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 InjectableImpl -{ + 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 InjectableImpl } } + #[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, + syrette::errors::injectable::InjectableError> + { + unimplemented!(); + } + } + } + } + fn create_get_dep_method_calls( dependencies: &[Dep], is_async: bool, @@ -288,8 +400,9 @@ impl InjectableImpl }) } - fn build_dependencies(new_method: &ImplItemMethod) - -> Result, Box> + fn build_dependencies( + new_method: &ImplItemMethod, + ) -> Result, DependencyError> { let new_method_args = &new_method.sig.inputs; @@ -338,6 +451,74 @@ impl InjectableImpl } } +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::::build_dependencies(&method)?; + let dependencies = InjectableImpl::::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> + 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::::build_dependencies(&method)?; + let dependencies = InjectableImpl::::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, } +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 + fn parse(input: ParseStream) -> Result { let input_fork = input.fork(); @@ -50,31 +77,33 @@ impl Parse for InjectableMacroArgs let flags = Punctuated::::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::>(); - - 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> { let input_args = quote! { IFoo, haha = true, async = false }; - assert!(matches!(parse2::(input_args), Err(_))); + assert!(parse2::(input_args).is_ok()); + + Ok(()) } #[test] - fn cannot_parse_with_duplicate_flag() + fn can_parse_with_duplicate_flag() { - assert!(matches!( - parse2::(quote! { - IFoo, async = false, no_doc_hidden = true, async = false - }), - Err(_) - )); + assert!(parse2::(quote! { + IFoo, async = false, no_doc_hidden = true, async = false + }) + .is_ok()); + + assert!(parse2::(quote! { + IFoo, async = true , no_doc_hidden = true, async = false + }) + .is_ok()); + } - assert!(matches!( - parse2::(quote! { - IFoo, async = true , no_doc_hidden = true, async = false - }), - Err(_) - )); + #[test] + fn check_flags_fail_with_unknown_flag() -> Result<(), Box> + { + let input_args = quote! { + IFoo, haha = true, async = false + }; + + let injectable_macro_args = parse2::(input_args)?; + + assert!(injectable_macro_args.check_flags().is_err()); + + Ok(()) + } + + #[test] + fn check_flags_fail_with_duplicate_flag() -> Result<(), Box> + { + let macro_args = parse2::(quote! { + IFoo, async = false, no_doc_hidden = true, async = false + })?; + + assert!(macro_args.check_flags().is_err()); + + let macro_args_two = parse2::(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::(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 = match parse(impl_stream) { - Ok(injectable_impl) => injectable_impl, - Err(err) => { - panic!("{err}"); - } - }; + let injectable_impl = + InjectableImpl::::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 + 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::()?; +impl Parse for MacroFlag +{ + fn parse(input: ParseStream) -> syn::Result + { + let flag = input.parse::()?; input.parse::()?; @@ -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(&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 +where + Item: Eq + Hash, { - fn find_duplicate(&mut self) -> Option; + /// 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 IteratorExt for Iter where Iter: Iterator, - Iter::Item: Eq + Hash + Clone, + Iter::Item: Eq + Hash, { - fn find_duplicate(&mut self) -> Option + fn find_duplicate(self) -> Option<(Iter::Item, Iter::Item)> { - let mut iterated_item_map = HashMap::::new(); + let mut iterated_item_map = HashSet::::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}; -- cgit v1.2.3-18-g5258