diff --git a/snafu-derive/Cargo.toml b/snafu-derive/Cargo.toml index 8507ecde..7cfd148e 100644 --- a/snafu-derive/Cargo.toml +++ b/snafu-derive/Cargo.toml @@ -20,3 +20,4 @@ proc-macro = true syn = { version = "1.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" +heck = "0.3.3" diff --git a/snafu-derive/src/lib.rs b/snafu-derive/src/lib.rs index 57ec792d..573ce94f 100644 --- a/snafu-derive/src/lib.rs +++ b/snafu-derive/src/lib.rs @@ -24,6 +24,11 @@ type MultiSynResult = std::result::Result>; /// Some arbitrary tokens we treat as a black box type UserInput = Box; +enum ModuleName { + Default, + Custom(syn::Ident), +} + enum SnafuInfo { Enum(EnumInfo), NamedStruct(NamedStructInfo), @@ -36,6 +41,7 @@ struct EnumInfo { generics: syn::Generics, variants: Vec, default_visibility: UserInput, + module: Option, } struct FieldContainer { @@ -45,6 +51,7 @@ struct FieldContainer { display_format: Option, doc_comment: String, visibility: Option, + module: Option, } enum SuffixKind { @@ -505,6 +512,11 @@ const ATTR_VISIBILITY: OnlyValidOn = OnlyValidOn { valid_on: "an enum, enum variants, or a struct with named fields", }; +const ATTR_MODULE: OnlyValidOn = OnlyValidOn { + attribute: "module", + valid_on: "an enum or structs with named fields", +}; + const ATTR_CONTEXT: OnlyValidOn = OnlyValidOn { attribute: "context", valid_on: "enum variants or structs with named fields", @@ -534,6 +546,7 @@ fn parse_snafu_enum( let mut errors = SyntaxErrors::default(); + let mut modules = AtMostOne::new("module", ErrorLocation::OnEnum); let mut default_visibilities = AtMostOne::new("visibility", ErrorLocation::OnEnum); let mut crate_roots = AtMostOne::new("crate_root", ErrorLocation::OnEnum); let mut enum_errors = errors.scoped(ErrorLocation::OnEnum); @@ -555,6 +568,7 @@ fn parse_snafu_enum( SnafuAttribute::CrateRoot(tokens, root) => { crate_roots.add(root, tokens); } + SnafuAttribute::Module(tokens, v) => modules.add(v, tokens), SnafuAttribute::Backtrace(tokens, ..) => enum_errors.add(tokens, ATTR_BACKTRACE), SnafuAttribute::Context(tokens, ..) => enum_errors.add(tokens, ATTR_CONTEXT), SnafuAttribute::Whatever(tokens) => enum_errors.add(tokens, ATTR_WHATEVER), @@ -562,8 +576,20 @@ fn parse_snafu_enum( } } + let (module, errs) = modules.finish(); + errors.extend(errs); + + let default_default_visibility = match module { + Some(_) => { + // Default to pub visibility since private context selectors wouldn't + // be accessible outside the module. + pub_visibility + } + None => private_visibility, + }; + let (maybe_default_visibility, errs) = default_visibilities.finish(); - let default_visibility = maybe_default_visibility.unwrap_or_else(private_visibility); + let default_visibility = maybe_default_visibility.unwrap_or_else(default_default_visibility); errors.extend(errs); let (maybe_crate_root, errs) = crate_roots.finish(); @@ -610,6 +636,7 @@ fn parse_snafu_enum( generics, variants, default_visibility, + module, }) } @@ -627,6 +654,7 @@ fn field_container( let mut outer_errors = errors.scoped(outer_error_location); + let mut modules = AtMostOne::new("module", outer_error_location); let mut display_formats = AtMostOne::new("display", outer_error_location); let mut visibilities = AtMostOne::new("visibility", outer_error_location); let mut contexts = AtMostOne::new("context", outer_error_location); @@ -636,6 +664,7 @@ fn field_container( for attr in attrs { match attr { + SnafuAttribute::Module(tokens, n) => modules.add(n, tokens), SnafuAttribute::Display(tokens, d) => display_formats.add(d, tokens), SnafuAttribute::Visibility(tokens, v) => visibilities.add(v, tokens), SnafuAttribute::Context(tokens, c) => contexts.add(c, tokens), @@ -738,6 +767,7 @@ fn field_container( field_errors.add(tokens, ATTR_BACKTRACE_FALSE); } } + SnafuAttribute::Module(tokens, ..) => field_errors.add(tokens, ATTR_MODULE), SnafuAttribute::Visibility(tokens, ..) => field_errors.add(tokens, ATTR_VISIBILITY), SnafuAttribute::Display(tokens, ..) => field_errors.add(tokens, ATTR_DISPLAY), SnafuAttribute::Context(tokens, ..) => field_errors.add(tokens, ATTR_CONTEXT), @@ -814,6 +844,9 @@ fn field_container( _ => {} // no conflict } + let (module, errs) = modules.finish(); + errors.extend(errs); + let (display_format, errs) = display_formats.finish(); errors.extend(errs); @@ -908,6 +941,7 @@ fn field_container( display_format, doc_comment, visibility, + module, }) } @@ -995,6 +1029,7 @@ fn parse_snafu_tuple_struct( for attr in attributes_from_syn(attrs)? { match attr { + SnafuAttribute::Module(tokens, ..) => struct_errors.add(tokens, ATTR_MODULE), SnafuAttribute::Display(tokens, ..) => struct_errors.add(tokens, ATTR_DISPLAY), SnafuAttribute::Visibility(tokens, ..) => struct_errors.add(tokens, ATTR_VISIBILITY), SnafuAttribute::Source(tokens, ss) => { @@ -1085,6 +1120,7 @@ enum SnafuAttribute { Whatever(proc_macro2::TokenStream), CrateRoot(proc_macro2::TokenStream, UserInput), DocComment(proc_macro2::TokenStream, String), + Module(proc_macro2::TokenStream, ModuleName), } fn default_crate_root() -> UserInput { @@ -1095,6 +1131,10 @@ fn private_visibility() -> UserInput { Box::new(quote! {}) } +fn pub_visibility() -> UserInput { + Box::new(syn::token::Pub(proc_macro2::Span::call_site())) +} + impl From for proc_macro::TokenStream { fn from(other: SnafuInfo) -> proc_macro::TokenStream { match other { @@ -1207,8 +1247,24 @@ impl EnumInfo { let error_impl = ErrorImpl(&self); let error_compat_impl = ErrorCompatImpl(&self); + let context = match self.module { + None => quote! { #context_selectors }, + Some(ref module_name) => { + use crate::shared::ContextModule; + + let context_module = ContextModule { + container_name: self.name(), + body: &context_selectors, + visibility: Some(&self.default_visibility), + module_name, + }; + + quote! { #context_module } + } + }; + quote! { - #context_selectors + #context #display_impl #error_impl #error_compat_impl @@ -1433,6 +1489,7 @@ impl NamedStructInfo { display_format, doc_comment, visibility, + module, }, .. } = &self; @@ -1504,6 +1561,17 @@ impl NamedStructInfo { let selector_doc_string = format!("SNAFU context selector for the `{}` error", name); + let pub_visibility = pub_visibility(); + let selector_visibility = match (visibility, module) { + (Some(ref v), _) => Some(&**v), + (None, Some(_)) => { + // Default to pub visibility since private context selectors + // wouldn't be accessible outside the module. + Some(&pub_visibility as &dyn quote::ToTokens) + } + (None, None) => None, + }; + let context_selector = ContextSelector { backtrace_field: backtrace_field.as_ref(), crate_root: &crate_root, @@ -1514,15 +1582,31 @@ impl NamedStructInfo { selector_kind: &selector_kind, selector_name: &field_container.name, user_fields: &user_fields, - visibility: visibility.as_ref().map(|x| &**x), + visibility: selector_visibility, where_clauses: &where_clauses, }; + let context = match module { + None => quote! { #context_selector }, + Some(module_name) => { + use crate::shared::ContextModule; + + let context_module = ContextModule { + container_name: self.name(), + body: &context_selector, + visibility: visibility.as_ref().map(|x| &**x), + module_name, + }; + + quote! { #context_module } + } + }; + quote! { #error_impl #error_compat_impl #display_impl - #context_selector + #context } } } diff --git a/snafu-derive/src/parse.rs b/snafu-derive/src/parse.rs index 8c3b02cf..3ed87e14 100644 --- a/snafu-derive/src/parse.rs +++ b/snafu-derive/src/parse.rs @@ -1,4 +1,4 @@ -use crate::SnafuAttribute; +use crate::{ModuleName, SnafuAttribute}; use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ @@ -18,6 +18,7 @@ mod kw { custom_keyword!(whatever); custom_keyword!(source); custom_keyword!(visibility); + custom_keyword!(module); custom_keyword!(from); @@ -65,6 +66,7 @@ enum Attribute { Whatever(Whatever), Source(Source), Visibility(Visibility), + Module(Module), } impl From for SnafuAttribute { @@ -79,6 +81,7 @@ impl From for SnafuAttribute { Whatever(o) => SnafuAttribute::Whatever(o.to_token_stream()), Source(s) => SnafuAttribute::Source(s.to_token_stream(), s.into_components()), Visibility(v) => SnafuAttribute::Visibility(v.to_token_stream(), v.into_arbitrary()), + Module(v) => SnafuAttribute::Module(v.to_token_stream(), v.into_value()), } } } @@ -100,6 +103,8 @@ impl Parse for Attribute { input.parse().map(Attribute::Source) } else if lookahead.peek(kw::visibility) { input.parse().map(Attribute::Visibility) + } else if lookahead.peek(kw::module) { + input.parse().map(Attribute::Module) } else { Err(lookahead.error()) } @@ -532,6 +537,36 @@ impl ToTokens for Visibility { } } +struct Module { + module_token: kw::module, + arg: MaybeArg, +} + +impl Module { + fn into_value(self) -> ModuleName { + match self.arg.into_option() { + None => ModuleName::Default, + Some(name) => ModuleName::Custom(name), + } + } +} + +impl Parse for Module { + fn parse(input: ParseStream) -> Result { + Ok(Self { + module_token: input.parse()?, + arg: input.parse()?, + }) + } +} + +impl ToTokens for Module { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.module_token.to_tokens(tokens); + self.arg.to_tokens(tokens); + } +} + enum MaybeArg { None, Some { diff --git a/snafu-derive/src/shared.rs b/snafu-derive/src/shared.rs index 0f0c4193..b4720c07 100644 --- a/snafu-derive/src/shared.rs +++ b/snafu-derive/src/shared.rs @@ -1,8 +1,53 @@ +pub(crate) use self::context_module::ContextModule; pub(crate) use self::context_selector::ContextSelector; pub(crate) use self::display::{Display, DisplayMatchArm}; pub(crate) use self::error::{Error, ErrorSourceMatchArm}; pub(crate) use self::error_compat::{ErrorCompat, ErrorCompatBacktraceMatchArm}; +pub mod context_module { + use crate::ModuleName; + use heck::SnakeCase; + use proc_macro2::TokenStream; + use quote::{quote, ToTokens}; + use syn::Ident; + + #[derive(Copy, Clone)] + pub(crate) struct ContextModule<'a, T> { + pub container_name: &'a Ident, + pub module_name: &'a ModuleName, + pub visibility: Option<&'a dyn ToTokens>, + pub body: &'a T, + } + + impl<'a, T> ToTokens for ContextModule<'a, T> + where + T: ToTokens, + { + fn to_tokens(&self, stream: &mut TokenStream) { + let module_name = match self.module_name { + ModuleName::Default => { + let name_str = self.container_name.to_string().to_snake_case(); + syn::Ident::new(&name_str, self.container_name.span()) + } + ModuleName::Custom(name) => name.clone(), + }; + + let visibility = self.visibility; + let body = self.body; + + let module_tokens = quote! { + #visibility mod #module_name { + use super::*; + + #body + } + }; + + stream.extend(module_tokens); + } + } +} + pub mod context_selector { use crate::{ContextSelectorKind, Field, SuffixKind}; use proc_macro2::TokenStream; diff --git a/tests/module.rs b/tests/module.rs new file mode 100644 index 00000000..4b4fad82 --- /dev/null +++ b/tests/module.rs @@ -0,0 +1,102 @@ +pub mod inner { + use snafu::Snafu; + + #[derive(Debug)] + pub struct Dummy0; + + #[derive(Debug)] + pub struct Dummy1; + + #[derive(Debug)] + pub struct Dummy2; + + #[derive(Debug)] + pub struct Dummy3; + + #[derive(Debug, Snafu)] + #[snafu(module, visibility(pub))] + pub enum PubError { + Variant { v: Dummy0 }, + } + + #[derive(Debug, Snafu)] + #[snafu(module(custom_pub), visibility(pub))] + pub enum PubWithCustomModError { + Variant { v: Dummy1 }, + } + + #[derive(Debug, Snafu)] + #[snafu(module(custom_pub_crate), visibility(pub(crate)))] + pub(crate) enum PubCrateWithCustomModError { + Variant { v: Dummy2 }, + } + + mod child { + use super::Dummy3; + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(module, visibility(pub(in crate::inner)))] + pub enum RestrictedError { + Variant { v: Dummy3 }, + } + } + + #[test] + fn can_set_module_visibility_restricted() { + let _ = self::child::restricted_error::VariantSnafu { v: Dummy3 }.build(); + } +} + +use self::inner::Dummy1; +use snafu::Snafu; + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum SomeError { + Variant { v: i32 }, +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum QualifiedError { + Variant { + unqualified: Dummy1, + mod_struct: inner::Dummy0, + self_struct: self::Dummy1, + crate_struct: crate::Dummy1, + boxed_trait: Box, + }, +} + +#[test] +fn can_use_qualified_names_in_module() { + let _ = qualified_error::VariantSnafu { + unqualified: Dummy1, + mod_struct: inner::Dummy0, + self_struct: self::Dummy1, + crate_struct: crate::Dummy1, + boxed_trait: Box::new(()) as Box<_>, + } + .build(); +} + +#[test] +fn can_set_module() { + let _ = some_error::VariantSnafu { v: 0i32 }.build(); +} + +#[test] +fn can_set_module_visibility_pub() { + let _ = inner::pub_error::VariantSnafu { v: inner::Dummy0 }.build(); +} + +#[test] +fn can_set_module_visibility_pub_with_custom_name() { + let _ = inner::custom_pub::VariantSnafu { v: inner::Dummy1 }.build(); +} + +#[test] +fn can_set_module_visibility_pub_crate_with_custom_name() { + let _ = inner::custom_pub_crate::VariantSnafu { v: inner::Dummy2 }.build(); +} diff --git a/tests/structs.rs b/tests/structs.rs index f69bdfb0..0a47df0c 100644 --- a/tests/structs.rs +++ b/tests/structs.rs @@ -5,6 +5,7 @@ mod structs { mod display; mod from_option; mod generics; + mod module; mod no_context; mod single_use_lifetimes; mod source_attributes; diff --git a/tests/structs/module.rs b/tests/structs/module.rs new file mode 100644 index 00000000..d1cb1e04 --- /dev/null +++ b/tests/structs/module.rs @@ -0,0 +1,79 @@ +pub mod inner { + use snafu::Snafu; + + #[derive(Debug)] + pub struct Dummy0; + + #[derive(Debug)] + pub struct Dummy1; + + #[derive(Debug, Snafu)] + #[snafu(module, visibility(pub))] + pub struct PubError; + + #[derive(Debug, Snafu)] + #[snafu(module(custom_pub), visibility(pub))] + pub struct PubWithCustomModError; + + #[derive(Debug, Snafu)] + #[snafu(module(custom_pub_crate), visibility(pub(crate)))] + pub(crate) struct PubCrateWithCustomModError; + + #[derive(Debug, Snafu)] + #[snafu(module, visibility(pub(in crate::structs::module)))] + pub struct RestrictedError; +} + +use self::inner::Dummy1; +use snafu::Snafu; + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub struct SomeError; + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub struct QualifiedError { + unqualified: Dummy1, + mod_struct: inner::Dummy0, + self_struct: self::Dummy1, + crate_struct: crate::structs::module::Dummy1, + boxed_trait: Box, +} + +#[test] +fn can_use_qualified_names_in_module() { + let _ = qualified_error::QualifiedSnafu { + unqualified: Dummy1, + mod_struct: inner::Dummy0, + self_struct: self::Dummy1, + crate_struct: crate::structs::module::Dummy1, + boxed_trait: Box::new(()) as Box<_>, + } + .build(); +} + +#[test] +fn can_set_module() { + let _ = some_error::SomeSnafu.build(); +} + +#[test] +fn can_set_module_visibility_pub() { + let _ = inner::pub_error::PubSnafu.build(); +} + +#[test] +fn can_set_module_visibility_restricted() { + let _ = inner::restricted_error::RestrictedSnafu.build(); +} + +#[test] +fn can_set_module_visibility_pub_with_custom_name() { + let _ = inner::custom_pub::PubWithCustomModSnafu.build(); +} + +#[test] +fn can_set_module_visibility_pub_crate_with_custom_name() { + let _ = inner::custom_pub_crate::PubCrateWithCustomModSnafu.build(); +}