aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2023-01-30 21:29:21 +0100
committerHampusM <hampus@hampusmat.com>2023-01-30 21:29:21 +0100
commit178267c701c233542078c09fe6b19802f9642dbd (patch)
tree3d3010806f6509c062ca86dbbbe9c3a6cd2fe547
parent17ca46e95af38a914197958bbcc1e759865b6005 (diff)
feat: improve macro error messages
-rw-r--r--Cargo.toml2
-rw-r--r--macros/Cargo.toml2
-rw-r--r--macros/src/declare_interface_args.rs9
-rw-r--r--macros/src/factory/declare_default_args.rs2
-rw-r--r--macros/src/factory/macro_args.rs2
-rw-r--r--macros/src/fn_trait.rs4
-rw-r--r--macros/src/injectable/dependency.rs149
-rw-r--r--macros/src/injectable/implementation.rs229
-rw-r--r--macros/src/injectable/macro_args.rs135
-rw-r--r--macros/src/lib.rs111
-rw-r--r--macros/src/macro_flag.rs43
-rw-r--r--macros/src/util/error.rs116
-rw-r--r--macros/src/util/iterator_ext.rs42
-rw-r--r--macros/src/util/mod.rs23
14 files changed, 677 insertions, 192 deletions
diff --git a/Cargo.toml b/Cargo.toml
index ced765a..3e43668 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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};