Repository: shepmaster/snafu (rust)

Issue:
Consider adding #[snafu(transparent)]
While working on the playground recently, I found myself wanting a "transparent" error. Someone has mentioned this to me before, but we don't seem to have an issue for it. 

The TL;DR of my case:

- 2 data types that are parsed from strings (`Edition`, `Mode`)
- 2 HTTP requests that use the data types (`Run`, `Build`)

In the original playground implementation, the two data types shared one error type (e.g. `enum Error { BadEdition, BadMode }`) but that caused some annoying circular type problems and runs contrary to the normal Rust practice of one error per parse (e.g. `ParseBoolError`). Moving them to separate types leads to a catch — what do you put as the `Display` implementation on the outer error?

Concretely, code like

```rust
if let Err(e) = parse_run("moo", "debug") {
    println!("Couldn't parse `Run`: {e}");
}
```

Would print out:

```
Couldn't parse `Run`: Edition: 'moo' is not a valid edition
```

The `Edition: ` is the auto-generated error message, but there's not really any better text to put here. Ideally I'd just completely *remove* that middle text and end up with ``Couldn't parse `Run`: 'moo' is not a valid edition``.

Since we would want to use the source's `Display`, we probably shouldn't return the source as an error itself, which is why this sounds like "transparent" to me.

### Rough suggested implementation

Adding `#[snafu(transparent)]` to an enum variant will delegate the implementation of `Display` and `Error::source` to the `source` field.

- This attribute should be an error in other places
- Should be an error to combine it with `display`

### Open questions

- This would often be paired with `context(false)` — would it **always**? Should `transparent` imply `context(false)`?

<details>
<summary>Complete code example</summary>

```rust
use snafu::prelude::*;
use std::str::FromStr;

// Data types

enum Edition {
    Rust2015,
    Rust2018,
}

impl FromStr for Edition {
    type Err = ParseEditionError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "2015" => Edition::Rust2015,
            "2018" => Edition::Rust2018,
            value => return ParseEditionSnafu { value }.fail(),
        })
    }
}

#[derive(Debug, Snafu)]
#[snafu(display("'{value}' is not a valid edition"))]
struct ParseEditionError {
    value: String,
}

enum Mode {
    Debug,
    Release,
}

impl FromStr for Mode {
    type Err = ParseModeError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "debug" => Mode::Debug,
            "release" => Mode::Release,
            value => return ParseModeSnafu { value }.fail(),
        })
    }
}

#[derive(Debug, Snafu)]
#[snafu(display("'{value}' is not a valid mode"))]
struct ParseModeError {
    value: String,
}

// Requests

#[allow(dead_code)]
struct Run {
    edition: Edition,
    mode: Mode,
}

fn parse_run(edition: &str, mode: &str) -> Result<Run, ParseRunError> {
    Ok(Run {
        edition: edition.parse()?,
        mode: mode.parse()?,
    })
}

#[derive(Debug, Snafu)]
enum ParseRunError {
    #[snafu(context(false))]
    Edition { source: ParseEditionError },

    #[snafu(context(false))]
    Mode { source: ParseModeError },
}

#[allow(dead_code)]
struct Build {
    edition: Edition,
    mode: Mode,
}

fn parse_build(edition: &str, mode: &str) -> Result<Build, ParseBuildError> {
    Ok(Build {
        edition: edition.parse()?,
        mode: mode.parse()?,
    })
}

#[derive(Debug, Snafu)]
enum ParseBuildError {
    #[snafu(context(false))]
    Edition { source: ParseEditionError },

    #[snafu(context(false))]
    Mode { source: ParseModeError },
}

// Usage

fn main() {
    if let Err(e) = parse_run("moo", "debug") {
        println!("Couldn't parse `Run`: {e}");
    }

    if let Err(e) = parse_build("2018", "cow") {
        println!("Couldn't parse `Build`: {e}");
    }
}
```

</details>

Pull request description:
Implement `snafu(transparent)`
Fixes #406


Patch:
diff --git a/snafu-derive/src/lib.rs b/snafu-derive/src/lib.rs
index a5f5dde..f325144 100644
--- a/snafu-derive/src/lib.rs
+++ b/snafu-derive/src/lib.rs
@@ -53,6 +53,7 @@ struct EnumInfo {
     module: Option<ModuleName>,
 }
 
+/// A struct or enum variant, with named fields.
 struct FieldContainer {
     name: syn::Ident,
     backtrace_field: Option<Field>,
@@ -63,6 +64,7 @@ struct FieldContainer {
     visibility: Option<UserInput>,
     module: Option<ModuleName>,
     provides: Vec<Provide>,
+    is_transparent: bool,
 }
 
 impl FieldContainer {
@@ -635,6 +637,15 @@ const ATTR_CRATE_ROOT: OnlyValidOn = OnlyValidOn {
     valid_on: "an enum or a struct",
 };
 
+const ATTR_TRANSPARENT: OnlyValidOn = OnlyValidOn {
+    attribute: "transparent",
+    valid_on: "enum variants or structs with named fields",
+};
+
+const ATTR_TRANSPARENT_FALSE: DoesNothing = DoesNothing {
+    attribute: "transparent(false)",
+};
+
 const SOURCE_BOOL_FROM_INCOMPATIBLE: IncompatibleAttributes =
     IncompatibleAttributes(&["source(false)", "source(from)"]);
 
@@ -683,6 +694,7 @@ fn parse_snafu_enum(
             }
             Att::Backtrace(tokens, ..) => enum_errors.add(tokens, ATTR_BACKTRACE),
             Att::Implicit(tokens, ..) => enum_errors.add(tokens, ATTR_IMPLICIT),
+            Att::Transparent(tokens, ..) => enum_errors.add(tokens, ATTR_TRANSPARENT),
             Att::Whatever(tokens) => enum_errors.add(tokens, ATTR_WHATEVER),
             Att::DocComment(..) => { /* Just a regular doc comment. */ }
         }
@@ -768,6 +780,8 @@ fn field_container(
 
     let mut contexts = AtMostOne::new("context", outer_error_location);
     let mut whatevers = AtMostOne::new("whatever", outer_error_location);
+    let mut transparents = AtMostOne::new("transparent", outer_error_location);
+
     let mut doc_comment = DocComment::default();
     let mut reached_end_of_doc_comment = false;
 
@@ -780,6 +794,13 @@ fn field_container(
             Att::Visibility(tokens, v) => visibilities.add(v, tokens),
             Att::Context(tokens, c) => contexts.add(c, tokens),
             Att::Whatever(tokens) => whatevers.add((), tokens),
+            Att::Transparent(tokens, t) => {
+                if t {
+                    transparents.add((), tokens)
+                } else {
+                    outer_errors.add(tokens, ATTR_TRANSPARENT_FALSE)
+                }
+            }
             Att::Source(tokens, ..) => outer_errors.add(tokens, ATTR_SOURCE),
             Att::Backtrace(tokens, ..) => outer_errors.add(tokens, ATTR_BACKTRACE),
             Att::Implicit(tokens, ..) => outer_errors.add(tokens, ATTR_IMPLICIT),
@@ -907,6 +928,7 @@ fn field_container(
                 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),
+                Att::Transparent(tokens, ..) => field_errors.add(tokens, ATTR_TRANSPARENT),
                 Att::Whatever(tokens) => field_errors.add(tokens, ATTR_WHATEVER),
                 Att::CrateRoot(tokens, ..) => field_errors.add(tokens, ATTR_CRATE_ROOT),
                 Att::DocComment(..) => { /* Just a regular doc comment. */ }
@@ -1007,19 +1029,48 @@ fn field_container(
     let (module, errs) = modules.finish();
     errors.extend(errs);
 
-    let (display_format, errs) = display_formats.finish();
+    let (display_format, errs) = display_formats.finish_with_location();
     errors.extend(errs);
 
     let (visibility, errs) = visibilities.finish();
     errors.extend(errs);
 
     let (is_context, errs) = contexts.finish_with_location();
-    let is_context = is_context.map(|(c, tt)| (c.into_enabled(), tt));
+    let mut is_context = is_context.map(|(c, tt)| (c.into_enabled(), tt));
+    let mut is_context_false_checks_source = "Context selectors without context";
     errors.extend(errs);
 
     let (is_whatever, errs) = whatevers.finish_with_location();
     errors.extend(errs);
 
+    let (is_transparent, errs) = transparents.finish_with_location();
+    errors.extend(errs);
+
+    if let (Some((_, d_tt)), Some((_, t_tt))) = (&display_format, &is_transparent) {
+        let txt = "`transparent` errors cannot have a display format because they delegate `Display` to their source";
+        errors.extend([
+            syn::Error::new_spanned(d_tt, txt),
+            syn::Error::new_spanned(t_tt, txt),
+        ]);
+    }
+
+    match (&is_context, &is_transparent) {
+        (Some(((true, _), c_tt)), Some((_, t_tt))) => {
+            let txt = "`transparent` errors cannot have context";
+            errors.extend([
+                syn::Error::new_spanned(c_tt, txt),
+                syn::Error::new_spanned(t_tt, txt),
+            ]);
+        }
+        (None, Some((_, t_tt))) => {
+            // `transparent` implies `context(false)`. Treat `transparent` as if setting
+            // `context(false)` as well.
+            is_context = Some(((false, SuffixKind::Default), t_tt.clone()));
+            is_context_false_checks_source = "`transparent` errors";
+        }
+        (Some(((false, _), _)), Some(_)) | (_, None) => {}
+    }
+
     let source_field = source.map(|(val, _tts)| val);
 
     let selector_kind = match (is_context, is_whatever) {
@@ -1079,14 +1130,20 @@ fn field_container(
             errors.extend(user_fields.into_iter().map(|Field { original, .. }| {
                 syn::Error::new_spanned(
                     original,
-                    "Context selectors without context must not have context fields",
+                    format_args!(
+                        "{} must not have context fields",
+                        is_context_false_checks_source
+                    ),
                 )
             }));
 
             let source_field = source_field.ok_or_else(|| {
                 vec![syn::Error::new(
                     variant_span,
-                    "Context selectors without context must have a source field",
+                    format_args!(
+                        "{} must have a source field",
+                        is_context_false_checks_source
+                    ),
                 )]
             })?;
 
@@ -1099,11 +1156,12 @@ fn field_container(
         backtrace_field: backtrace.map(|(val, _tts)| val),
         implicit_fields,
         selector_kind,
-        display_format,
+        display_format: display_format.map(|(d, _)| d),
         doc_comment: doc_comment.finish(),
         visibility,
         module,
         provides,
+        is_transparent: is_transparent.is_some(),
     })
 }
 
@@ -1235,6 +1293,7 @@ fn parse_snafu_tuple_struct(
             Att::Implicit(tokens, ..) => struct_errors.add(tokens, ATTR_IMPLICIT),
             Att::Context(tokens, ..) => struct_errors.add(tokens, ATTR_CONTEXT),
             Att::Whatever(tokens) => struct_errors.add(tokens, ATTR_CONTEXT),
+            Att::Transparent(tokens, ..) => struct_errors.add(tokens, ATTR_TRANSPARENT),
             Att::CrateRoot(tokens, root) => crate_roots.add(root, tokens),
             Att::DocComment(..) => { /* Just a regular doc comment. */ }
         }
@@ -1351,6 +1410,7 @@ enum SnafuAttribute {
     Module(proc_macro2::TokenStream, ModuleName),
     Provide(proc_macro2::TokenStream, ProvideKind),
     Source(proc_macro2::TokenStream, Vec<Source>),
+    Transparent(proc_macro2::TokenStream, bool),
     Visibility(proc_macro2::TokenStream, UserInput),
     Whatever(proc_macro2::TokenStream),
 }
diff --git a/snafu-derive/src/parse.rs b/snafu-derive/src/parse.rs
index b3b7877..ceaf2de 100644
--- a/snafu-derive/src/parse.rs
+++ b/snafu-derive/src/parse.rs
@@ -21,6 +21,7 @@ mod kw {
     custom_keyword!(module);
     custom_keyword!(provide);
     custom_keyword!(source);
+    custom_keyword!(transparent);
     custom_keyword!(visibility);
     custom_keyword!(whatever);
 
@@ -86,6 +87,7 @@ enum Attribute {
     Module(Module),
     Provide(Provide),
     Source(Source),
+    Transparent(Transparent),
     Visibility(Visibility),
     Whatever(Whatever),
 }
@@ -103,6 +105,7 @@ impl From<Attribute> for SnafuAttribute {
             Module(v) => SnafuAttribute::Module(v.to_token_stream(), v.into_value()),
             Provide(v) => SnafuAttribute::Provide(v.to_token_stream(), v.into_value()),
             Source(s) => SnafuAttribute::Source(s.to_token_stream(), s.into_components()),
+            Transparent(t) => SnafuAttribute::Transparent(t.to_token_stream(), t.into_bool()),
             Visibility(v) => SnafuAttribute::Visibility(v.to_token_stream(), v.into_arbitrary()),
             Whatever(o) => SnafuAttribute::Whatever(o.to_token_stream()),
         }
@@ -128,6 +131,8 @@ impl Parse for Attribute {
             input.parse().map(Attribute::Provide)
         } else if lookahead.peek(kw::source) {
             input.parse().map(Attribute::Source)
+        } else if lookahead.peek(kw::transparent) {
+            input.parse().map(Attribute::Transparent)
         } else if lookahead.peek(kw::visibility) {
             input.parse().map(Attribute::Visibility)
         } else if lookahead.peek(kw::whatever) {
@@ -802,6 +807,33 @@ impl ToTokens for SourceArg {
     }
 }
 
+struct Transparent {
+    transparent_token: kw::transparent,
+    arg: MaybeArg<LitBool>,
+}
+
+impl Transparent {
+    fn into_bool(self) -> bool {
+        self.arg.into_option().map_or(true, |a| a.value)
+    }
+}
+
+impl Parse for Transparent {
+    fn parse(input: ParseStream) -> Result<Self> {
+        Ok(Self {
+            transparent_token: input.parse()?,
+            arg: input.parse()?,
+        })
+    }
+}
+
+impl ToTokens for Transparent {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        self.transparent_token.to_tokens(tokens);
+        self.arg.to_tokens(tokens);
+    }
+}
+
 struct Visibility {
     visibility_token: kw::visibility,
     visibility: MaybeArg<syn::Visibility>,
diff --git a/snafu-derive/src/shared.rs b/snafu-derive/src/shared.rs
index d6ee123..2774487 100644
--- a/snafu-derive/src/shared.rs
+++ b/snafu-derive/src/shared.rs
@@ -528,6 +528,20 @@ pub mod display {
 
             let source_field = selector_kind.source_field();
 
+            if field_container.is_transparent {
+                // transparent errors always have a source field
+                let source_field_name = source_field.unwrap().name();
+
+                let match_arm = quote! {
+                    #pattern_ident { ref #source_field_name, .. } => {
+                        ::core::fmt::Display::fmt(#source_field_name, #FORMATTER_ARG)
+                    }
+                };
+
+                stream.extend(match_arm);
+                return;
+            }
+
             let mut shorthand_names = &BTreeSet::new();
             let mut assigned_names = &BTreeSet::new();
 
@@ -677,7 +691,12 @@ pub mod error {
     impl ToTokens for ErrorSourceMatchArm<'_> {
         fn to_tokens(&self, stream: &mut TokenStream) {
             let Self {
-                field_container: FieldContainer { selector_kind, .. },
+                field_container:
+                    FieldContainer {
+                        selector_kind,
+                        is_transparent,
+                        ..
+                    },
                 pattern_ident,
             } = *self;
 
@@ -693,6 +712,10 @@ pub mod error {
                         quote! {
                             #field_name.as_ref().map(|e| e.as_error_source())
                         }
+                    } else if *is_transparent {
+                        quote! {
+                            #field_name.as_error_source().source()
+                        }
                     } else {
                         quote! {
                             ::core::option::Option::Some(#field_name.as_error_source())
diff --git a/src/Snafu.md b/src/Snafu.md
index 2cda5f3..3ad6b02 100644
--- a/src/Snafu.md
+++ b/src/Snafu.md
@@ -11,6 +11,7 @@ unique situations.
 - [`module`](#placing-context-selectors-in-modules)
 - [`provide`](#providing-data-beyond-the-error-trait)
 - [`source`](#controlling-error-sources)
+- [`transparent`](#delegating-to-the-underlying-error)
 - [`visibility`](#controlling-visibility)
 - [`whatever`](#controlling-stringly-typed-errors)
 
@@ -37,6 +38,7 @@ it is valid. Detailed information on each attribute is below.
 | `context(false)`                | Skips creation of the context selector, implements `From` for the mandatory source error                                                                                                        |
 | `context(suffix(N))`            | Changes the suffix of the generated context selector to `N`                                                                                                                                     |
 | `context(suffix(false))`        | No suffix for the generated context selector                                                                                                                                                    |
+| `transparent`                   | Delegates `Display` and `Error::source` to this error's source, implies `context(false)`                                                                                                        |
 | `visibility(v)`                 | Sets the visibility of the generated context selector to `v` (e.g. `pub`)                                                                                                                       |
 | `visibility`                    | Resets visibility back to private                                                                                                                                                               |
 | `provide(flags, type => expr)`  | Provides the type using the `expr` with the optional flags                                                                                                                                      |
@@ -189,6 +191,79 @@ fn do_something_unique() -> Result<i32, VeryUniqueError> {
 }
 ```
 
+### Delegating to the underlying error
+
+When creating a contextful error you might want to reuse it in
+multiple places - composing different error types is idiomatic, after
+all. This can lead to unwanted nesting as both the enum variant and
+the source error are visible in the source chain and in autogenerated
+`Display` implementations.
+
+You can use `#[snafu(transparent)]` to delegate the `Display` and
+`Error::source` implementations to the underlying error instead,
+effectively removing the wrapping error variant from the logical error
+chain. That way you get both the maintenance and logical benefits of
+composing errors, without the redundant nesting.
+
+`#[snafu(transparent)]` implies `#[snafu(context(false))]`. Because
+`#[snafu(transparent)]` errors delegate `Display` to the source error,
+you cannot use `#[snafu(display(...))]` on them.
+
+**Example**
+
+This example allows adding or removing users from groups. The group ID
+is passed as a raw integer which needs to be validated first.
+
+Since adding and removing users have distinct error types, there's
+nothing useful to say in addition to the validation error message, so
+this is a good time to use `#[snafu(transparent)]`.
+
+```rust
+# use snafu::prelude::*;
+#
+fn add_to_group(group: u32, user: &str) -> Result<(), AddToGroupError> {
+    let group = GroupId::validate(group)?;
+    // ... do useful operation
+    Ok(())
+}
+
+fn remove_from_group(group: u32, user: &str) -> Result<(), RemoveFromGroupError> {
+    let group = GroupId::validate(group)?;
+    // ... do useful operation
+    Ok(())
+}
+
+#[derive(Debug, Snafu)]
+enum AddToGroupError {
+    #[snafu(transparent)]
+    Group { source: GroupIdError },
+
+    // ... other failure conditions
+}
+
+#[derive(Debug, Snafu)]
+enum RemoveFromGroupError {
+    #[snafu(transparent)]
+    Group { source: GroupIdError },
+
+    // ... other failure conditions
+}
+
+#[derive(Debug)]
+struct GroupId(u32);
+
+impl GroupId {
+    fn validate(id: u32) -> Result<Self, GroupIdError> {
+        // ... perform validation
+#       GroupIdSnafu { id }.fail()
+    }
+}
+
+#[derive(Debug, Snafu)]
+#[snafu(display("Group ID {id} does not exist"))]
+struct GroupIdError { id: u32 };
+```
+
 ## Controlling visibility
 
 By default, each of the context selectors and their inherent


Test patch:
diff --git a/compatibility-tests/compile-fail/tests/ui/attribute-misuse.rs b/compatibility-tests/compile-fail/tests/ui/attribute-misuse.rs
index d59dfbe..4c7fb36 100644
--- a/compatibility-tests/compile-fail/tests/ui/attribute-misuse.rs
+++ b/compatibility-tests/compile-fail/tests/ui/attribute-misuse.rs
@@ -10,6 +10,7 @@ mod enum_misuse {
     #[snafu(implicit)]
     #[snafu(provide)]
     #[snafu(provide(u8 => 0))]
+    #[snafu(transparent)]
     enum EnumError {
         AVariant,
     }
@@ -43,6 +44,7 @@ mod field_misuse {
             #[snafu(source(from(XXXX, Box::new)))]
             #[snafu(context)]
             #[snafu(crate_root(XXXX))]
+            #[snafu(transparent)]
             source: String,
 
             #[snafu(provide(false))]
@@ -68,6 +70,7 @@ mod struct_misuse {
     #[snafu(implicit)]
     #[snafu(provide)]
     #[snafu(provide(u8 => 0))]
+    #[snafu(transparent)]
     struct StructError(Box<UsableError>);
 }
 
diff --git a/compatibility-tests/compile-fail/tests/ui/attribute-misuse.stderr b/compatibility-tests/compile-fail/tests/ui/attribute-misuse.stderr
index 68a0c2a..f41c988 100644
--- a/compatibility-tests/compile-fail/tests/ui/attribute-misuse.stderr
+++ b/compatibility-tests/compile-fail/tests/ui/attribute-misuse.stderr
@@ -46,122 +46,140 @@ error: `provide(type => expression)` attribute is only valid on enum variants, s
 12 |     #[snafu(provide(u8 => 0))]
    |             ^^^^^^^^^^^^^^^^
 
+error: `transparent` attribute is only valid on enum variants or structs with named fields, not on an enum
+  --> tests/ui/attribute-misuse.rs:13:13
+   |
+13 |     #[snafu(transparent)]
+   |             ^^^^^^^^^^^
+
 error: `source` attribute is only valid on enum variant or struct fields with a name, not on an enum variant
-  --> tests/ui/attribute-misuse.rs:24:46
+  --> tests/ui/attribute-misuse.rs:25:46
    |
-24 |         #[snafu(display("an error variant"), source(from(XXXX, Box::new)))]
+25 |         #[snafu(display("an error variant"), source(from(XXXX, Box::new)))]
    |                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 error: `source` attribute is only valid on enum variant or struct fields with a name, not on an enum variant
-  --> tests/ui/attribute-misuse.rs:25:17
+  --> tests/ui/attribute-misuse.rs:26:17
    |
-25 |         #[snafu(source)]
+26 |         #[snafu(source)]
    |                 ^^^^^^
 
 error: `backtrace` attribute is only valid on enum variant or struct fields with a name, not on an enum variant
-  --> tests/ui/attribute-misuse.rs:26:17
+  --> tests/ui/attribute-misuse.rs:27:17
    |
-26 |         #[snafu(backtrace)]
+27 |         #[snafu(backtrace)]
    |                 ^^^^^^^^^
 
 error: `crate_root` attribute is only valid on an enum or a struct, not on an enum variant
-  --> tests/ui/attribute-misuse.rs:27:17
+  --> tests/ui/attribute-misuse.rs:28:17
    |
-27 |         #[snafu(crate_root(XXXX))]
+28 |         #[snafu(crate_root(XXXX))]
    |                 ^^^^^^^^^^^^^^^^
 
 error: `implicit` attribute is only valid on enum variant or struct fields with a name, not on an enum variant
-  --> tests/ui/attribute-misuse.rs:28:17
+  --> tests/ui/attribute-misuse.rs:29:17
    |
-28 |         #[snafu(implicit)]
+29 |         #[snafu(implicit)]
    |                 ^^^^^^^^
 
 error: `provide` attribute is only valid on enum variant or struct fields with a name, not on an enum variant
-  --> tests/ui/attribute-misuse.rs:29:17
+  --> tests/ui/attribute-misuse.rs:30:17
    |
-29 |         #[snafu(provide)]
+30 |         #[snafu(provide)]
    |                 ^^^^^^^
 
 error: `display` attribute is only valid on enum variants or structs with named fields, not on a field
-  --> tests/ui/attribute-misuse.rs:40:21
+  --> tests/ui/attribute-misuse.rs:41:21
    |
-40 |             #[snafu(display("display should not work here"))]
+41 |             #[snafu(display("display should not work here"))]
    |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 error: `visibility` attribute is only valid on an enum, enum variants, or a struct with named fields, not on a field
-  --> tests/ui/attribute-misuse.rs:41:21
+  --> tests/ui/attribute-misuse.rs:42:21
    |
-41 |             #[snafu(visibility(pub))]
+42 |             #[snafu(visibility(pub))]
    |                     ^^^^^^^^^^^^^^^
 
 error: Incompatible attributes [`source(false)`, `source(from)`] specified on a field
-  --> tests/ui/attribute-misuse.rs:43:21
+  --> tests/ui/attribute-misuse.rs:44:21
    |
-43 |             #[snafu(source(from(XXXX, Box::new)))]
+44 |             #[snafu(source(from(XXXX, Box::new)))]
    |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 error: `context` attribute is only valid on enum variants or structs with named fields, not on a field
-  --> tests/ui/attribute-misuse.rs:44:21
+  --> tests/ui/attribute-misuse.rs:45:21
    |
-44 |             #[snafu(context)]
+45 |             #[snafu(context)]
    |                     ^^^^^^^
 
 error: `crate_root` attribute is only valid on an enum or a struct, not on a field
-  --> tests/ui/attribute-misuse.rs:45:21
+  --> tests/ui/attribute-misuse.rs:46:21
    |
-45 |             #[snafu(crate_root(XXXX))]
+46 |             #[snafu(crate_root(XXXX))]
    |                     ^^^^^^^^^^^^^^^^
 
+error: `transparent` attribute is only valid on enum variants or structs with named fields, not on a field
+  --> tests/ui/attribute-misuse.rs:47:21
+   |
+47 |             #[snafu(transparent)]
+   |                     ^^^^^^^^^^^
+
 error: `provide(false)` attribute is only valid on a field named "source" or "backtrace", not on other fields
-  --> tests/ui/attribute-misuse.rs:48:21
+  --> tests/ui/attribute-misuse.rs:50:21
    |
-48 |             #[snafu(provide(false))]
+50 |             #[snafu(provide(false))]
    |                     ^^^^^^^^^^^^^^
 
 error: `provide(type => expression)` attribute is only valid on enum variants, structs with named fields, or tuple structs, not on a field
-  --> tests/ui/attribute-misuse.rs:49:21
+  --> tests/ui/attribute-misuse.rs:51:21
    |
-49 |             #[snafu(provide(u8 => 0))]
+51 |             #[snafu(provide(u8 => 0))]
    |                     ^^^^^^^^^^^^^^^^
 
 error: `display` attribute is only valid on enum variants or structs with named fields, not on a tuple struct
-  --> tests/ui/attribute-misuse.rs:62:13
+  --> tests/ui/attribute-misuse.rs:64:13
    |
-62 |     #[snafu(display("display should not work here"))]
+64 |     #[snafu(display("display should not work here"))]
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 error: `visibility` attribute is only valid on an enum, enum variants, or a struct with named fields, not on a tuple struct
-  --> tests/ui/attribute-misuse.rs:64:13
+  --> tests/ui/attribute-misuse.rs:66:13
    |
-64 |     #[snafu(visibility(pub))]
+66 |     #[snafu(visibility(pub))]
    |             ^^^^^^^^^^^^^^^
 
 error: `source(bool)` attribute is only valid on enum variant or struct fields with a name, not on a tuple struct
-  --> tests/ui/attribute-misuse.rs:65:13
+  --> tests/ui/attribute-misuse.rs:67:13
    |
-65 |     #[snafu(source(true))]
+67 |     #[snafu(source(true))]
    |             ^^^^^^^^^^^^
 
 error: `backtrace` attribute is only valid on enum variant or struct fields with a name, not on a tuple struct
-  --> tests/ui/attribute-misuse.rs:66:13
+  --> tests/ui/attribute-misuse.rs:68:13
    |
-66 |     #[snafu(backtrace)]
+68 |     #[snafu(backtrace)]
    |             ^^^^^^^^^
 
 error: `context` attribute is only valid on enum variants or structs with named fields, not on a tuple struct
-  --> tests/ui/attribute-misuse.rs:67:13
+  --> tests/ui/attribute-misuse.rs:69:13
    |
-67 |     #[snafu(context)]
+69 |     #[snafu(context)]
    |             ^^^^^^^
 
 error: `implicit` attribute is only valid on enum variant or struct fields with a name, not on a tuple struct
-  --> tests/ui/attribute-misuse.rs:68:13
+  --> tests/ui/attribute-misuse.rs:70:13
    |
-68 |     #[snafu(implicit)]
+70 |     #[snafu(implicit)]
    |             ^^^^^^^^
 
 error: `provide` attribute is only valid on enum variant or struct fields with a name, not on a tuple struct
-  --> tests/ui/attribute-misuse.rs:69:13
+  --> tests/ui/attribute-misuse.rs:71:13
    |
-69 |     #[snafu(provide)]
+71 |     #[snafu(provide)]
    |             ^^^^^^^
+
+error: `transparent` attribute is only valid on enum variants or structs with named fields, not on a tuple struct
+  --> tests/ui/attribute-misuse.rs:73:13
+   |
+73 |     #[snafu(transparent)]
+   |             ^^^^^^^^^^^
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 9446e95..4d57a15 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`, `module`, `provide`, `source`, `visibility`, `whatever`
+error: expected one of: `backtrace`, `context`, `crate_root`, `display`, `implicit`, `module`, `provide`, `source`, `transparent`, `visibility`, `whatever`
  --> tests/ui/structs/attribute-misuse.rs:5:13
   |
 5 |     #[snafu(unknown_attribute)]
diff --git a/compatibility-tests/compile-fail/tests/ui/transparent.rs b/compatibility-tests/compile-fail/tests/ui/transparent.rs
new file mode 100644
index 0000000..ea36499
--- /dev/null
+++ b/compatibility-tests/compile-fail/tests/ui/transparent.rs
@@ -0,0 +1,23 @@
+extern crate snafu;
+
+use snafu::prelude::*;
+
+type BoxError = Box<dyn std::error::Error>;
+
+#[derive(Debug, Snafu)]
+enum Error {
+    #[snafu(transparent)]
+    MissingSource {},
+
+    #[snafu(transparent)]
+    HasUserFields { source: BoxError, a: i32, b: i32 },
+
+    #[snafu(transparent(false))]
+    TransparentFalseDoesNothing { source: BoxError },
+
+    #[snafu(transparent)]
+    #[snafu(display("Oh snap!"))]
+    NonsensicalDisplay { source: BoxError },
+}
+
+fn main() {}
diff --git a/compatibility-tests/compile-fail/tests/ui/transparent.stderr b/compatibility-tests/compile-fail/tests/ui/transparent.stderr
new file mode 100644
index 0000000..a7f2fc9
--- /dev/null
+++ b/compatibility-tests/compile-fail/tests/ui/transparent.stderr
@@ -0,0 +1,35 @@
+error: `transparent` errors must not have context fields
+  --> tests/ui/transparent.rs:13:39
+   |
+13 |     HasUserFields { source: BoxError, a: i32, b: i32 },
+   |                                       ^^^^^^
+
+error: `transparent` errors must not have context fields
+  --> tests/ui/transparent.rs:13:47
+   |
+13 |     HasUserFields { source: BoxError, a: i32, b: i32 },
+   |                                               ^^^^^^
+
+error: `transparent(false)` attribute has no effect
+  --> tests/ui/transparent.rs:15:13
+   |
+15 |     #[snafu(transparent(false))]
+   |             ^^^^^^^^^^^^^^^^^^
+
+error: `transparent` errors cannot have a display format because they delegate `Display` to their source
+  --> tests/ui/transparent.rs:19:13
+   |
+19 |     #[snafu(display("Oh snap!"))]
+   |             ^^^^^^^^^^^^^^^^^^^
+
+error: `transparent` errors cannot have a display format because they delegate `Display` to their source
+  --> tests/ui/transparent.rs:18:13
+   |
+18 |     #[snafu(transparent)]
+   |             ^^^^^^^^^^^
+
+error: `transparent` errors must have a source field
+  --> tests/ui/transparent.rs:10:5
+   |
+10 |     MissingSource {},
+   |     ^^^^^^^^^^^^^
diff --git a/tests/location.rs b/tests/location.rs
index 8d549f8..78665e3 100644
--- a/tests/location.rs
+++ b/tests/location.rs
@@ -17,8 +17,15 @@ mod basics {
         let one = UsageSnafu.build();
         let two = UsageSnafu.build();
 
-        assert_eq!(one.to_string(), "Created at tests/location.rs:17:30");
-        assert_eq!(two.to_string(), "Created at tests/location.rs:18:30");
+        let sep = std::path::MAIN_SEPARATOR;
+        assert_eq!(
+            one.to_string(),
+            format!("Created at tests{sep}location.rs:17:30")
+        );
+        assert_eq!(
+            two.to_string(),
+            format!("Created at tests{sep}location.rs:18:30")
+        );
     }
 }
 
diff --git a/tests/transparent.rs b/tests/transparent.rs
new file mode 100644
index 0000000..8a9c49f
--- /dev/null
+++ b/tests/transparent.rs
@@ -0,0 +1,93 @@
+use snafu::prelude::*;
+use std::error::Error;
+
+#[derive(Debug, Snafu)]
+struct AlphaError;
+
+#[derive(Debug, Snafu)]
+struct BetaError {
+    source: AlphaError,
+}
+
+#[derive(Debug, Snafu)]
+enum ErrorTest {
+    #[snafu(transparent)]
+    Alpha { source: AlphaError },
+
+    #[snafu(transparent)]
+    Beta { source: BetaError },
+}
+
+fn alpha() -> Result<i32, AlphaError> {
+    AlphaSnafu.fail()
+}
+
+fn beta() -> Result<i32, BetaError> {
+    let a = alpha().context(BetaSnafu)?;
+    Ok(a + 2)
+}
+
+fn example() -> Result<i32, ErrorTest> {
+    let a = 2;
+    let b = beta()?;
+    Ok(a * 10 + b)
+}
+
+fn check<T: std::error::Error>() {}
+
+#[test]
+fn implements_error() {
+    check::<ErrorTest>();
+
+    let error = example().unwrap_err();
+    // skipping `ErrorTest`, directly going to `BetaError`'s source
+    assert_eq!(error.source().unwrap().to_string(), "AlphaError");
+}
+
+#[test]
+fn implements_display() {
+    let error = example().unwrap_err();
+    // no `Beta: ` prefix
+    assert_eq!(error.to_string(), "BetaError: AlphaError");
+}
+
+mod with_backtraces {
+    use super::*;
+    use snafu::Backtrace;
+
+    #[derive(Debug, Snafu)]
+    #[snafu(transparent)]
+    struct Error {
+        source: AlphaError,
+        backtrace: Backtrace,
+    }
+
+    #[test]
+    fn implements_error() {
+        check::<Error>();
+    }
+}
+
+mod with_bounds {
+    use super::*;
+    use std::fmt::{Debug, Display};
+
+    #[derive(Debug, Snafu)]
+    enum GenericError<T, U = i32> {
+        Something { things: T, other: U },
+    }
+
+    #[derive(Debug, Snafu)]
+    enum Error<T: 'static>
+    where
+        T: Debug + Display + Copy,
+    {
+        #[snafu(transparent)]
+        Generic { source: GenericError<T> },
+    }
+
+    #[test]
+    fn implements_error() {
+        check::<Error<i32>>();
+    }
+}