Repository: shepmaster/snafu (Rust)

Issue:
Option to put generated context selectors into a submodule
First, thanks for making SNAFU, I'm really enjoying its ergonomics!

I started a new project and tried to create a couple nested errors, but hit a name collision between the selectors that SNAFU was generating and one of my existing structs.

My inner module looks like this:

```rust
pub struct Config {
    // ...
}

#[derive(Debug, Snafu)]
pub enum ConfigError {
    // ... 
}

impl Config {
    pub fn load() -> Result<Self, ConfigError> {
        // ...
    }
}
```

My outer module imports a couple things from this struct and exports a new error type that wraps `ConfigError`:

```rust
use crate::{Config, ConfigError};

#[derive(Debug, Snafu)]
pub enum SyncError {
    Config { // oh no, a name collision!
        source: ConfigError,
    }
}

pub fn sync() -> Result<(), SyncError> {
    let config = Config::load().context(Config)?; // Collision here, too!

    // ...
}
```

The `Snafu` derive macro on `SyncError` tries to define a struct named `Config` as the context selector, which ends up colliding with the struct `Config` that I imported before!

One solution for this particular case I thought of was to put my SNAFU error into its own `mod` block to scope everything the macro defines. Combined with `#[snafu(visibility)]` and re-exporting the error type, this seems to work:

```rust
use crate::Config;

mod error {
    use crate::config::ConfigError;
    use snafu::Snafu;

    #[derive(Debug, Snafu)]
    #[snafu(visibility = "pub(super)")]
    pub enum SyncError {
        Config {
            source: ConfigError,
        },
    }
}

pub use error::SyncError;

pub fn sync() -> Result<(), SyncError> {
    let config = Config::load().context(error::Config)?;

    // ...
}
```

Another way we could solve this is by adding a configuration option like `#[snafu(mod = "error")]` to instruct SNAFU to wrap all of its context selectors into a module with the given name.

Does this feature make sense? Is there a better solution to this particular problem that I'm not seeing?

Thanks!


Discussion:
Yes, I think such an option does make sense. I expect that most people who have run into the problem have done as you suggest: put it in a module "by hand" and perform the relevant exports.

I think that your general sketch of the resulting code also is the good default when this is opted-in-to: create a module (name chosen by user), put all the context selectors in that module, then mark them as `pub(super)`. The error itself stays where it is.

There will undoubtedly be some nuances that are run into during development, but this seems like it's in a good state to start work on.

Would you be interested in taking a crack at it?
i actually wanted to suggest the same thing, main difference i would prefer is it to be named something like `namespace` (because you are basically "namespacing" the variants)
@shepmaster Sorry I haven't gotten back sooner! I'm interested in helping build this, but after using SNAFU in more projects I wonder if it's the right solution to the name-collision problem.

Does it make sense for SNAFU to generate the context selectors with a name that _doesn't_ match the enum variants directly? Could we introduce an enum-variant attribute to change the generated selector name?

SNAFU could automatically name your selectors in a way that isn't your enum variant verbatim (which would obviously be a breaking change), SNAFU could name the selector `FooContext` or `FooCtx`.

For manual names, it could be reasonable to let the user specify:

```rust
use crate::{Config, ConfigError};

#[derive(Debug, Snafu)]
pub enum SyncError {
    #[snafu(selector = ConfigContext)]
    Config {
        source: ConfigError,
    }
}

pub fn sync() -> Result<(), SyncError> {
    let config = Config::load().context(ConfigContext)?; // All good now!

    // ...
}
```

This is a smaller hammer than namespacing _all_ the context selectors. Alternatively, we could implement this feature as proposed, and then encourage users to then re-export their selectors. That would look like this:

```rust
use crate::{Config, ConfigError};

#[derive(Debug, Snafu)]
#[snafu(mod = "error")]
pub enum SyncError {
    Config {
        source: ConfigError,
    }
}

// We can't glob-import the context selectors if we need to rename any of them.
use error::Config as ConfigContext;

pub fn sync() -> Result<(), SyncError> {
    let config = Config::load().context(ConfigContext)?; // All good now!

    // ...
}
```



Patch:
diff --git a/snafu-derive/Cargo.toml b/snafu-derive/Cargo.toml
index 147be9c4..9dcbe40e 100644
--- a/snafu-derive/Cargo.toml
+++ b/snafu-derive/Cargo.toml
@@ -21,3 +21,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 39938c9a..564c79d2 100644
--- a/snafu-derive/src/lib.rs
+++ b/snafu-derive/src/lib.rs
@@ -24,6 +24,11 @@ type MultiSynResult<T> = std::result::Result<T, Vec<syn::Error>>;
 /// Some arbitrary tokens we treat as a black box
 type UserInput = Box<dyn quote::ToTokens>;
 
+enum ModuleName {
+    Default,
+    Custom(syn::Ident),
+}
+
 enum SnafuInfo {
     Enum(EnumInfo),
     NamedStruct(NamedStructInfo),
@@ -35,8 +40,9 @@ struct EnumInfo {
     name: syn::Ident,
     generics: syn::Generics,
     variants: Vec<FieldContainer>,
-    default_visibility: UserInput,
+    default_visibility: Option<UserInput>,
     default_suffix: SuffixKind,
+    module: Option<ModuleName>,
 }
 
 struct FieldContainer {
@@ -47,6 +53,7 @@ struct FieldContainer {
     display_format: Option<Display>,
     doc_comment: Option<DocComment>,
     visibility: Option<UserInput>,
+    module: Option<ModuleName>,
 }
 
 enum SuffixKind {
@@ -529,6 +536,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",
@@ -563,6 +575,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 default_suffixes = AtMostOne::new("context(suffix)", ErrorLocation::OnEnum);
     let mut crate_roots = AtMostOne::new("crate_root", ErrorLocation::OnEnum);
@@ -587,6 +600,7 @@ fn parse_snafu_enum(
                 Context::Suffix(s) => default_suffixes.add(s, tokens),
                 Context::Flag(_) => enum_errors.add(tokens, ATTR_CONTEXT_FLAG),
             },
+            Att::Module(tokens, v) => modules.add(v, tokens),
             Att::Backtrace(tokens, ..) => enum_errors.add(tokens, ATTR_BACKTRACE),
             Att::Implicit(tokens, ..) => enum_errors.add(tokens, ATTR_IMPLICIT),
             Att::Whatever(tokens) => enum_errors.add(tokens, ATTR_WHATEVER),
@@ -594,8 +608,10 @@ fn parse_snafu_enum(
         }
     }
 
-    let (maybe_default_visibility, errs) = default_visibilities.finish();
-    let default_visibility = maybe_default_visibility.unwrap_or_else(private_visibility);
+    let (module, errs) = modules.finish();
+    errors.extend(errs);
+
+    let (default_visibility, errs) = default_visibilities.finish();
     errors.extend(errs);
 
     let (maybe_default_suffix, errs) = default_suffixes.finish();
@@ -647,6 +663,7 @@ fn parse_snafu_enum(
         variants,
         default_visibility,
         default_suffix,
+        module,
     })
 }
 
@@ -664,6 +681,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);
@@ -675,6 +693,7 @@ fn field_container(
         use SnafuAttribute as Att;
 
         match attr {
+            Att::Module(tokens, n) => modules.add(n, tokens),
             Att::Display(tokens, d) => display_formats.add(d, tokens),
             Att::Visibility(tokens, v) => visibilities.add(v, tokens),
             Att::Context(tokens, c) => contexts.add(c, tokens),
@@ -789,6 +808,7 @@ fn field_container(
                         field_errors.add(tokens, ATTR_IMPLICIT_FALSE);
                     }
                 }
+                Att::Module(tokens, ..) => field_errors.add(tokens, ATTR_MODULE),
                 Att::Visibility(tokens, ..) => field_errors.add(tokens, ATTR_VISIBILITY),
                 Att::Display(tokens, ..) => field_errors.add(tokens, ATTR_DISPLAY),
                 Att::Context(tokens, ..) => field_errors.add(tokens, ATTR_CONTEXT),
@@ -873,6 +893,9 @@ fn field_container(
         _ => {} // no conflict
     }
 
+    let (module, errs) = modules.finish();
+    errors.extend(errs);
+
     let (display_format, errs) = display_formats.finish();
     errors.extend(errs);
 
@@ -968,6 +991,7 @@ fn field_container(
         display_format,
         doc_comment: doc_comment.finish(),
         visibility,
+        module,
     })
 }
 
@@ -1057,6 +1081,7 @@ fn parse_snafu_tuple_struct(
         use SnafuAttribute as Att;
 
         match attr {
+            Att::Module(tokens, ..) => struct_errors.add(tokens, ATTR_MODULE),
             Att::Display(tokens, ..) => struct_errors.add(tokens, ATTR_DISPLAY),
             Att::Visibility(tokens, ..) => struct_errors.add(tokens, ATTR_VISIBILITY),
             Att::Source(tokens, ss) => {
@@ -1180,6 +1205,7 @@ enum SnafuAttribute {
     Display(proc_macro2::TokenStream, Display),
     DocComment(proc_macro2::TokenStream, String),
     Implicit(proc_macro2::TokenStream, bool),
+    Module(proc_macro2::TokenStream, ModuleName),
     Source(proc_macro2::TokenStream, Vec<Source>),
     Visibility(proc_macro2::TokenStream, UserInput),
     Whatever(proc_macro2::TokenStream),
@@ -1193,6 +1219,12 @@ fn private_visibility() -> UserInput {
     Box::new(quote! {})
 }
 
+// Private context selectors wouldn't be accessible outside the
+// module, so we use `pub(super)`.
+fn default_context_selector_visibility_in_module() -> proc_macro2::TokenStream {
+    quote! { pub(super) }
+}
+
 impl From<SnafuInfo> for proc_macro::TokenStream {
     fn from(other: SnafuInfo) -> proc_macro::TokenStream {
         match other {
@@ -1305,8 +1337,24 @@ impl EnumInfo {
         let error_impl = ErrorImpl(&self);
         let error_compat_impl = ErrorCompatImpl(&self);
 
+        let context = match &self.module {
+            None => quote! { #context_selectors },
+            Some(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
@@ -1357,11 +1405,19 @@ impl<'a> quote::ToTokens for ContextSelector<'a> {
             ..
         } = self.1;
 
-        let visibility = self
-            .1
-            .visibility
-            .as_ref()
-            .unwrap_or(&self.0.default_visibility);
+        let default_visibility;
+        let selector_visibility = match (
+            &self.1.visibility,
+            &self.0.default_visibility,
+            &self.0.module,
+        ) {
+            (Some(v), _, _) | (_, Some(v), _) => Some(&**v),
+            (None, None, Some(_)) => {
+                default_visibility = default_context_selector_visibility_in_module();
+                Some(&default_visibility as _)
+            }
+            (None, None, None) => None,
+        };
 
         let selector_doc_string = format!(
             "SNAFU context selector for the `{}::{}` variant",
@@ -1379,7 +1435,7 @@ impl<'a> quote::ToTokens for ContextSelector<'a> {
             selector_kind: &selector_kind,
             selector_name: variant_name,
             user_fields: &selector_kind.user_fields(),
-            visibility: Some(&visibility),
+            visibility: selector_visibility,
             where_clauses: &self.0.provided_where_clauses(),
             default_suffix,
         };
@@ -1537,6 +1593,7 @@ impl NamedStructInfo {
                     display_format,
                     doc_comment,
                     visibility,
+                    module,
                 },
             ..
         } = &self;
@@ -1609,6 +1666,16 @@ impl NamedStructInfo {
 
         let selector_doc_string = format!("SNAFU context selector for the `{}` error", name);
 
+        let default_visibility;
+        let selector_visibility = match (visibility, module) {
+            (Some(v), _) => Some(&**v),
+            (None, Some(_)) => {
+                default_visibility = default_context_selector_visibility_in_module();
+                Some(&default_visibility as _)
+            }
+            (None, None) => None,
+        };
+
         let context_selector = ContextSelector {
             backtrace_field: backtrace_field.as_ref(),
             implicit_fields: implicit_fields,
@@ -1620,16 +1687,32 @@ 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,
             default_suffix: &SuffixKind::Default,
         };
 
+        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 644efd69..e61c7dcd 100644
--- a/snafu-derive/src/parse.rs
+++ b/snafu-derive/src/parse.rs
@@ -1,6 +1,6 @@
 use std::collections::BTreeSet;
 
-use crate::SnafuAttribute;
+use crate::{ModuleName, SnafuAttribute};
 use proc_macro2::TokenStream;
 use quote::{format_ident, ToTokens};
 use syn::{
@@ -18,6 +18,7 @@ mod kw {
     custom_keyword!(crate_root);
     custom_keyword!(display);
     custom_keyword!(implicit);
+    custom_keyword!(module);
     custom_keyword!(source);
     custom_keyword!(visibility);
     custom_keyword!(whatever);
@@ -66,6 +67,7 @@ enum Attribute {
     CrateRoot(CrateRoot),
     Display(Display),
     Implicit(Implicit),
+    Module(Module),
     Source(Source),
     Visibility(Visibility),
     Whatever(Whatever),
@@ -81,6 +83,7 @@ impl From<Attribute> for SnafuAttribute {
             CrateRoot(cr) => SnafuAttribute::CrateRoot(cr.to_token_stream(), cr.into_arbitrary()),
             Display(d) => SnafuAttribute::Display(d.to_token_stream(), d.into_display()),
             Implicit(d) => SnafuAttribute::Implicit(d.to_token_stream(), d.into_bool()),
+            Module(v) => SnafuAttribute::Module(v.to_token_stream(), v.into_value()),
             Source(s) => SnafuAttribute::Source(s.to_token_stream(), s.into_components()),
             Visibility(v) => SnafuAttribute::Visibility(v.to_token_stream(), v.into_arbitrary()),
             Whatever(o) => SnafuAttribute::Whatever(o.to_token_stream()),
@@ -101,6 +104,8 @@ impl Parse for Attribute {
             input.parse().map(Attribute::Display)
         } else if lookahead.peek(kw::implicit) {
             input.parse().map(Attribute::Implicit)
+        } else if lookahead.peek(kw::module) {
+            input.parse().map(Attribute::Module)
         } else if lookahead.peek(kw::source) {
             input.parse().map(Attribute::Source)
         } else if lookahead.peek(kw::visibility) {
@@ -456,6 +461,36 @@ impl ToTokens for Implicit {
     }
 }
 
+struct Module {
+    module_token: kw::module,
+    arg: MaybeArg<Ident>,
+}
+
+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<Self> {
+        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);
+    }
+}
+
 struct Source {
     source_token: kw::source,
     args: MaybeArg<Punctuated<SourceArg, token::Comma>>,
diff --git a/snafu-derive/src/shared.rs b/snafu-derive/src/shared.rs
index 7fa164a2..470b7567 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/src/Snafu.md b/src/Snafu.md
index 24b73b9c..2f2e2206 100644
--- a/src/Snafu.md
+++ b/src/Snafu.md
@@ -8,6 +8,7 @@ unique situations.
 - [`crate_root`](#controlling-how-the-snafu-crate-is-resolved)
 - [`display`](#controlling-display)
 - [`implicit`](#controlling-implicitly-generated-data)
+- [`module`](#placing-context-selectors-in-modules)
 - [`source`](#controlling-error-sources)
 - [`visibility`](#controlling-visibility)
 - [`whatever`](#controlling-stringly-typed-errors)
@@ -184,6 +185,83 @@ guaranteed. Therefore, exporting them in a crate's public API
 could cause semver breakage for such crates, should SNAFU internals
 change.
 
+## Placing context selectors in modules
+
+When you have multiple error enums that would generate conflicting
+context selectors, you can choose to place the context selectors into
+a module using `snafu(module)`:
+
+```rust
+use snafu::prelude::*;
+
+#[derive(Debug, Snafu)]
+#[snafu(module)]
+enum ReadError {
+    Opening,
+}
+
+fn example() -> Result<(), ReadError> {
+    read_error::OpeningSnafu.fail()
+}
+
+#[derive(Debug, Snafu)]
+enum WriteError {
+    Opening, // Would conflict if `snafu(module)` was not used above.
+}
+# // https://github.com/rust-lang/rust/issues/83583
+# fn main() {}
+```
+
+By default, the module name will be the `snake_case` equivalent of the
+enum name. You can override the default by providing an argument to
+`#[snafu(module(...))]`:
+
+```rust
+use snafu::prelude::*;
+
+#[derive(Debug, Snafu)]
+#[snafu(module(read))]
+enum ReadError {
+    Opening,
+}
+
+fn example() -> Result<(), ReadError> {
+    read::OpeningSnafu.fail()
+}
+# // https://github.com/rust-lang/rust/issues/83583
+# fn main() {}
+```
+
+As placing the context selectors in a module naturally namespaces
+them, you may wish to combine this option with
+`#[snafu(context(suffix(false)))]`:
+
+```rust
+use snafu::prelude::*;
+
+#[derive(Debug, Snafu)]
+#[snafu(module, context(suffix(false)))]
+enum ReadError {
+    Opening,
+}
+
+fn example() -> Result<(), ReadError> {
+    read_error::Opening.fail()
+}
+# // https://github.com/rust-lang/rust/issues/83583
+# fn main() {}
+```
+
+The generated module starts with `use super::*`, so any types or
+traits used by the context selectors need to be in scope — complicated
+paths may need to be simplified or made absolute.
+
+By default, the visibility of the generated module will be private
+while the context selectors inside will be `pub(super)`. Using
+[`#[snafu(visibility)]`](#controlling-visibility) to control the
+visibility will change the visibility of *both* the module and the
+context selectors.
+
 ## Controlling error sources
 
 ### Selecting the source field


Test patch:
diff --git a/compatibility-tests/compile-fail/tests/ui/module-visibility.rs b/compatibility-tests/compile-fail/tests/ui/module-visibility.rs
new file mode 100644
index 00000000..415b4500
--- /dev/null
+++ b/compatibility-tests/compile-fail/tests/ui/module-visibility.rs
@@ -0,0 +1,19 @@
+mod inside {
+    use snafu::prelude::*;
+
+    #[derive(Debug, Snafu)]
+    #[snafu(module)]
+    enum Error {
+        Variant,
+    }
+
+    fn can_access_in_same_module() {
+        let _ = error::VariantSnafu;
+    }
+}
+
+fn cant_access_outside_of_module() {
+    let _ = inside::error::VariantSnafu;
+}
+
+fn main() {}
diff --git a/compatibility-tests/compile-fail/tests/ui/module-visibility.stderr b/compatibility-tests/compile-fail/tests/ui/module-visibility.stderr
new file mode 100644
index 00000000..402636e2
--- /dev/null
+++ b/compatibility-tests/compile-fail/tests/ui/module-visibility.stderr
@@ -0,0 +1,12 @@
+error[E0603]: module `error` is private
+  --> $DIR/module-visibility.rs:16:21
+   |
+16 |     let _ = inside::error::VariantSnafu;
+   |                     ^^^^^ private module
+   |
+note: the module `error` is defined here
+  --> $DIR/module-visibility.rs:4:21
+   |
+4  |     #[derive(Debug, Snafu)]
+   |                     ^^^^^
+   = note: this error originates in the derive macro `Snafu` (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/compatibility-tests/compile-fail/tests/ui/structs/attribute-misuse.stderr b/compatibility-tests/compile-fail/tests/ui/structs/attribute-misuse.stderr
index e8d6b8f8..09187184 100644
--- a/compatibility-tests/compile-fail/tests/ui/structs/attribute-misuse.stderr
+++ b/compatibility-tests/compile-fail/tests/ui/structs/attribute-misuse.stderr
@@ -1,4 +1,4 @@
-error: expected one of: `backtrace`, `context`, `crate_root`, `display`, `implicit`, `source`, `visibility`, `whatever`
+error: expected one of: `backtrace`, `context`, `crate_root`, `display`, `implicit`, `module`, `source`, `visibility`, `whatever`
  --> $DIR/attribute-misuse.rs:5:13
   |
 5 |     #[snafu(unknown_attribute)]
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<dyn ::core::any::Any>,
+    },
+}
+
+#[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/main.rs b/tests/structs/main.rs
index 519fce22..8436029f 100644
--- a/tests/structs/main.rs
+++ b/tests/structs/main.rs
@@ -7,6 +7,7 @@ mod context_selector_name;
 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..30a95c66
--- /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::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::module::Dummy1,
+    boxed_trait: Box<dyn ::core::any::Any>,
+}
+
+#[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::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();
+}