Repository: zkat/miette
Pull request #37 (2021-08-29 19:29:34)

Only inc the line count if we haven't already done so with a char
The tests reflected the incorrect behaviour, skipping a line at at the very end.


Patch:
diff --git a/miette-derive/src/code.rs b/miette-derive/src/code.rs
index c1edd17..acbacc7 100644
--- a/miette-derive/src/code.rs
+++ b/miette-derive/src/code.rs
@@ -6,7 +6,10 @@ use syn::{
     Token,
 };
 
-use crate::diagnostic::DiagnosticVariant;
+use crate::{
+    diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
+    utils::forward_to_single_field_variant,
+};
 
 #[derive(Debug)]
 pub struct Code(pub String);
@@ -44,23 +47,31 @@ impl Parse for Code {
 }
 
 impl Code {
-    pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
+    pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
         let code_pairs = variants.iter().map(
-            |DiagnosticVariant {
-                 ref ident,
-                 ref code,
-                 ref fields,
-                 ..
+            |DiagnosticDef {
+                 ident,
+                 fields,
+                 args,
              }| {
-                let code = &code.0;
-                match fields {
-                    syn::Fields::Named(_) => {
-                        quote! { Self::#ident { .. } => std::boxed::Box::new(#code), }
+                match args {
+                    DiagnosticDefArgs::Transparent => {
+                        forward_to_single_field_variant(ident, fields, quote! { code() })
                     }
-                    syn::Fields::Unnamed(_) => {
-                        quote! { Self::#ident(..) => std::boxed::Box::new(#code), }
+                    DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { code, .. }) => {
+                        let code = &code.0;
+                        match fields {
+                            syn::Fields::Named(_) => {
+                                quote! { Self::#ident { .. } => std::boxed::Box::new(#code), }
+                            }
+                            syn::Fields::Unnamed(_) => {
+                                quote! { Self::#ident(..) => std::boxed::Box::new(#code), }
+                            }
+                            syn::Fields::Unit => {
+                                quote! { Self::#ident => std::boxed::Box::new(#code), }
+                            }
+                        }
                     }
-                    syn::Fields::Unit => quote! { Self::#ident => std::boxed::Box::new(#code), },
                 }
             },
         );
diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs
index ce61085..f84b861 100644
--- a/miette-derive/src/diagnostic.rs
+++ b/miette-derive/src/diagnostic.rs
@@ -1,5 +1,5 @@
 use proc_macro2::TokenStream;
-use quote::quote;
+use quote::{format_ident, quote};
 use syn::{punctuated::Punctuated, DeriveInput, Token};
 
 use crate::code::Code;
@@ -11,25 +11,30 @@ use crate::url::Url;
 
 pub enum Diagnostic {
     Struct {
-        fields: syn::Fields,
-        ident: syn::Ident,
         generics: syn::Generics,
-        code: Code,
-        severity: Option<Severity>,
-        help: Option<Help>,
-        snippets: Option<Snippets>,
-        url: Option<Url>,
+        ident: syn::Ident,
+        fields: syn::Fields,
+        args: DiagnosticDefArgs,
     },
     Enum {
         ident: syn::Ident,
         generics: syn::Generics,
-        variants: Vec<DiagnosticVariant>,
+        variants: Vec<DiagnosticDef>,
     },
 }
 
-pub struct DiagnosticVariant {
+pub struct DiagnosticDef {
     pub ident: syn::Ident,
     pub fields: syn::Fields,
+    pub args: DiagnosticDefArgs,
+}
+
+pub enum DiagnosticDefArgs {
+    Transparent,
+    Concrete(DiagnosticConcreteArgs),
+}
+
+pub struct DiagnosticConcreteArgs {
     pub code: Code,
     pub severity: Option<Severity>,
     pub help: Option<Help>,
@@ -37,46 +42,94 @@ pub struct DiagnosticVariant {
     pub url: Option<Url>,
 }
 
+impl DiagnosticConcreteArgs {
+    fn parse(
+        ident: &syn::Ident,
+        fields: &syn::Fields,
+        attr: &syn::Attribute,
+        args: impl Iterator<Item = DiagnosticArg>,
+    ) -> Result<Self, syn::Error> {
+        let mut code = None;
+        let mut severity = None;
+        let mut help = None;
+        let mut url = None;
+        for arg in args {
+            match arg {
+                DiagnosticArg::Transparent => {
+                    return Err(syn::Error::new_spanned(attr, "transparent not allowed"));
+                }
+                DiagnosticArg::Code(new_code) => {
+                    // TODO: error on multiple?
+                    code = Some(new_code);
+                }
+                DiagnosticArg::Severity(sev) => {
+                    severity = Some(sev);
+                }
+                DiagnosticArg::Help(hl) => {
+                    help = Some(hl);
+                }
+                DiagnosticArg::Url(u) => {
+                    url = Some(u);
+                }
+            }
+        }
+        let snippets = Snippets::from_fields(fields)?;
+        let concrete = DiagnosticConcreteArgs {
+            code: code
+                .ok_or_else(|| syn::Error::new(ident.span(), "Diagnostic code is required."))?,
+            help,
+            severity,
+            snippets,
+            url,
+        };
+        Ok(concrete)
+    }
+}
+
+impl DiagnosticDefArgs {
+    fn parse(
+        ident: &syn::Ident,
+        fields: &syn::Fields,
+        attr: &syn::Attribute,
+        allow_transparent: bool,
+    ) -> Result<Self, syn::Error> {
+        let args =
+            attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)?;
+        if allow_transparent
+            && args.len() == 1
+            && matches!(args.first(), Some(DiagnosticArg::Transparent))
+        {
+            return Ok(Self::Transparent);
+        } else if args.iter().any(|x| matches!(x, DiagnosticArg::Transparent)) {
+            return Err(syn::Error::new_spanned(
+                attr,
+                if allow_transparent {
+                    "diagnostic(transparent) not allowed in combination with other args"
+                } else {
+                    "diagnostic(transparent) not allowed here"
+                },
+            ));
+        }
+        let args = args
+            .into_iter()
+            .filter(|x| !matches!(x, DiagnosticArg::Transparent));
+        let concrete = DiagnosticConcreteArgs::parse(ident, fields, attr, args)?;
+        Ok(DiagnosticDefArgs::Concrete(concrete))
+    }
+}
+
 impl Diagnostic {
     pub fn from_derive_input(input: DeriveInput) -> Result<Self, syn::Error> {
         Ok(match input.data {
             syn::Data::Struct(data_struct) => {
                 if let Some(attr) = input.attrs.iter().find(|x| x.path.is_ident("diagnostic")) {
-                    let args = attr.parse_args_with(
-                        Punctuated::<DiagnosticArg, Token![,]>::parse_terminated,
-                    )?;
-                    let mut code = None;
-                    let mut severity = None;
-                    let mut help = None;
-                    let mut url = None;
-                    for arg in args {
-                        match arg {
-                            DiagnosticArg::Code(new_code) => {
-                                // TODO: error on multiple?
-                                code = Some(new_code);
-                            }
-                            DiagnosticArg::Url(u) => {
-                                url = Some(u);
-                            }
-                            DiagnosticArg::Severity(sev) => {
-                                severity = Some(sev);
-                            }
-                            DiagnosticArg::Help(hl) => help = Some(hl),
-                        }
-                    }
-                    let snippets = Snippets::from_fields(&data_struct.fields)?;
-                    let ident = input.ident.clone();
+                    let args =
+                        DiagnosticDefArgs::parse(&input.ident, &data_struct.fields, attr, true)?;
                     Diagnostic::Struct {
                         fields: data_struct.fields,
                         ident: input.ident,
                         generics: input.generics,
-                        code: code.ok_or_else(|| {
-                            syn::Error::new(ident.span(), "Diagnostic code is required.")
-                        })?,
-                        help,
-                        severity,
-                        snippets,
-                        url,
+                        args,
                     }
                 } else {
                     // Also handle when there's multiple `#[diagnostic]` attrs?
@@ -90,42 +143,11 @@ impl Diagnostic {
                 let mut vars = Vec::new();
                 for var in variants {
                     if let Some(attr) = var.attrs.iter().find(|x| x.path.is_ident("diagnostic")) {
-                        let args = attr.parse_args_with(
-                            Punctuated::<DiagnosticArg, Token![,]>::parse_terminated,
-                        )?;
-                        let mut code = None;
-                        let mut severity = None;
-                        let mut help = None;
-                        let mut url = None;
-                        for arg in args {
-                            match arg {
-                                DiagnosticArg::Code(new_code) => {
-                                    // TODO: error on multiple?
-                                    code = Some(new_code);
-                                }
-                                DiagnosticArg::Severity(sev) => {
-                                    severity = Some(sev);
-                                }
-                                DiagnosticArg::Help(hl) => {
-                                    help = Some(hl);
-                                }
-                                DiagnosticArg::Url(u) => {
-                                    url = Some(u);
-                                }
-                            }
-                        }
-                        let snippets = Snippets::from_fields(&var.fields)?;
-                        let ident = input.ident.clone();
-                        vars.push(DiagnosticVariant {
+                        let args = DiagnosticDefArgs::parse(&var.ident, &var.fields, attr, true)?;
+                        vars.push(DiagnosticDef {
                             ident: var.ident,
                             fields: var.fields,
-                            code: code.ok_or_else(|| {
-                                syn::Error::new(ident.span(), "Diagnostic code is required.")
-                            })?,
-                            help,
-                            severity,
-                            snippets,
-                            url,
+                            args,
                         });
                     } else {
                         // Also handle when there's multiple `#[diagnostic]` attrs?
@@ -153,29 +175,82 @@ impl Diagnostic {
     pub fn gen(&self) -> TokenStream {
         match self {
             Self::Struct {
-                fields,
                 ident,
+                fields,
                 generics,
-                code,
-                severity,
-                help,
-                snippets,
-                url,
+                args,
             } => {
                 let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
-                let code_body = code.gen_struct();
-                let help_body = help.as_ref().and_then(|x| x.gen_struct(fields));
-                let sev_body = severity.as_ref().and_then(|x| x.gen_struct());
-                let snip_body = snippets.as_ref().and_then(|x| x.gen_struct(fields));
-                let url_body = url.as_ref().and_then(|x| x.gen_struct(ident, fields));
+                match args {
+                    DiagnosticDefArgs::Transparent => {
+                        if fields.iter().len() != 1 {
+                            return quote! {
+                                compile_error!("you can only use #[diagnostic(transparent)] on a struct with exactly one field");
+                            };
+                        }
+                        let field = fields
+                            .iter()
+                            .next()
+                            .expect("MIETTE BUG: thought we knew we had exactly one field");
+                        let field_name = field
+                            .ident
+                            .clone()
+                            .unwrap_or_else(|| format_ident!("unnamed"));
+                        let matcher = match fields {
+                            syn::Fields::Named(_) => quote! { let Self { #field_name } = self; },
+                            syn::Fields::Unnamed(_) => quote! { let Self(#field_name) = self; },
+                            syn::Fields::Unit => {
+                                unreachable!("MIETTE BUG: thought we knew we had exactly one field")
+                            }
+                        };
 
-                quote! {
-                    impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
-                        #code_body
-                        #help_body
-                        #sev_body
-                        #snip_body
-                        #url_body
+                        quote! {
+                            impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
+                                fn code<'a>(&'a self) -> std::boxed::Box<dyn std::fmt::Display + 'a> {
+                                    #matcher
+                                    #field_name.code()
+                                }
+                                fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
+                                    #matcher
+                                    #field_name.help()
+                                }
+                                fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
+                                    #matcher
+                                    #field_name.url()
+                                }
+                                fn severity(&self) -> std::option::Option<miette::Severity> {
+                                    #matcher
+                                    #field_name.severity()
+                                }
+                                fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> {
+                                    #matcher
+                                    #field_name.snippets()
+                                }
+                            }
+                        }
+                    }
+                    DiagnosticDefArgs::Concrete(concrete) => {
+                        let code_body = concrete.code.gen_struct();
+                        let help_body = concrete.help.as_ref().and_then(|x| x.gen_struct(fields));
+                        let sev_body = concrete.severity.as_ref().and_then(|x| x.gen_struct());
+                        let snip_body = concrete
+                            .snippets
+                            .as_ref()
+                            .and_then(|x| x.gen_struct(fields));
+                        let url_body = concrete
+                            .url
+                            .as_ref()
+                            .and_then(|x| x.gen_struct(ident, fields));
+
+                        quote! {
+                            impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
+                                #code_body
+                                #help_body
+                                #sev_body
+                                #snip_body
+                                #url_body
+                            }
+                        }
                     }
                 }
             }
diff --git a/miette-derive/src/diagnostic_arg.rs b/miette-derive/src/diagnostic_arg.rs
index 69cc784..e56cc7a 100644
--- a/miette-derive/src/diagnostic_arg.rs
+++ b/miette-derive/src/diagnostic_arg.rs
@@ -6,6 +6,7 @@ use crate::severity::Severity;
 use crate::url::Url;
 
 pub enum DiagnosticArg {
+    Transparent,
     Code(Code),
     Severity(Severity),
     Help(Help),
@@ -15,7 +16,11 @@ pub enum DiagnosticArg {
 impl Parse for DiagnosticArg {
     fn parse(input: ParseStream) -> syn::Result<Self> {
         let ident = input.fork().parse::<syn::Ident>()?;
-        if ident == "code" {
+        if ident == "transparent" {
+            // consume the token
+            let _: syn::Ident = input.parse()?;
+            Ok(DiagnosticArg::Transparent)
+        } else if ident == "code" {
             Ok(DiagnosticArg::Code(input.parse()?))
         } else if ident == "severity" {
             Ok(DiagnosticArg::Severity(input.parse()?))
diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs
index 3d7ecf9..35154fa 100644
--- a/miette-derive/src/help.rs
+++ b/miette-derive/src/help.rs
@@ -9,8 +9,11 @@ use syn::{
     Fields, Token,
 };
 
-use crate::diagnostic::DiagnosticVariant;
 use crate::fmt::{self, Display};
+use crate::{
+    diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
+    utils::forward_to_single_field_variant,
+};
 
 pub struct Help {
     pub display: Display,
@@ -54,46 +57,53 @@ impl Parse for Help {
 }
 
 impl Help {
-    pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
+    pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
         let help_pairs = variants
             .iter()
-            .filter(|v| v.help.is_some())
             .map(
-                |DiagnosticVariant {
-                     ref ident,
-                     ref help,
-                     ref fields,
+                |DiagnosticDef {
+                     ident,
+                     fields,
+                     args,
                      ..
                  }| {
-                    let mut display = help.as_ref().expect("already checked for Some").display.clone();
-                    let member_idents = fields.iter().enumerate().map(|(i, field)| {
-                        field
-                            .ident
-                            .as_ref()
-                            .cloned()
-                            .unwrap_or_else(|| format_ident!("_{}", i))
-                    });
-                    let members: HashSet<syn::Member> = fields.iter().enumerate().map(|(i, field)| {
-                        if let Some(ident) = field.ident.as_ref().cloned() {
-                            syn::Member::Named(ident)
-                        } else {
-                            syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
-                        }
-                    }).collect();
-                    display.expand_shorthand(&members);
-                    let Display { fmt, args, .. } = display;
-                    match fields {
-                        syn::Fields::Named(_) => {
-                            quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
-                        }
-                        syn::Fields::Unnamed(_) => {
-                            quote! { Self::#ident( #(#member_idents),* ) => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
-                        }
-                        syn::Fields::Unit =>
-                            quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), },
-                    }
+                     match args {
+                         DiagnosticDefArgs::Transparent => {
+                             Some(forward_to_single_field_variant(ident, fields, quote!{ help() } ))
+                         }
+                         DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { help, .. }) => {
+                             let mut display = help.as_ref()?.display.clone();
+                             let member_idents = fields.iter().enumerate().map(|(i, field)| {
+                                 field
+                                     .ident
+                                     .as_ref()
+                                     .cloned()
+                                     .unwrap_or_else(|| format_ident!("_{}", i))
+                             });
+                             let members: HashSet<syn::Member> = fields.iter().enumerate().map(|(i, field)| {
+                                 if let Some(ident) = field.ident.as_ref().cloned() {
+                                     syn::Member::Named(ident)
+                                 } else {
+                                     syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
+                                 }
+                             }).collect();
+                             display.expand_shorthand(&members);
+                             let Display { fmt, args, .. } = display;
+                             Some(match fields {
+                                 syn::Fields::Named(_) => {
+                                     quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
+                                 }
+                                 syn::Fields::Unnamed(_) => {
+                                     quote! { Self::#ident( #(#member_idents),* ) => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
+                                 }
+                                 syn::Fields::Unit =>
+                                     quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), },
+                             })
+                         }
+                     }
                  },
             )
+            .flatten()
             .collect::<Vec<_>>();
         if help_pairs.is_empty() {
             None
diff --git a/miette-derive/src/severity.rs b/miette-derive/src/severity.rs
index ce1e7ba..633e8f7 100644
--- a/miette-derive/src/severity.rs
+++ b/miette-derive/src/severity.rs
@@ -6,7 +6,10 @@ use syn::{
     Token,
 };
 
-use crate::diagnostic::DiagnosticVariant;
+use crate::{
+    diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
+    utils::forward_to_single_field_variant,
+};
 
 pub struct Severity(pub syn::Ident);
 
@@ -56,23 +59,30 @@ fn get_severity(input: &str, span: Span) -> syn::Result<String> {
 }
 
 impl Severity {
-    pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
+    pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
         let sev_pairs = variants
             .iter()
-            .filter(|v| v.severity.is_some())
             .map(
-                |DiagnosticVariant {
-                     ident, severity, fields, ..
+                |DiagnosticDef {
+                     ident, fields, args
                  }| {
-                     let severity = &severity.as_ref().unwrap().0;
-                     let fields = match fields {
-                        syn::Fields::Named(_) => quote! { { .. } },
-                        syn::Fields::Unnamed(_) => quote! { (..) },
-                        syn::Fields::Unit => quote!{},
-                    };
-                     quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), }
+                     match args {
+                         DiagnosticDefArgs::Transparent => {
+                             Some(forward_to_single_field_variant(ident, fields, quote!{ severity() }))
+                         }
+                         DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { severity, .. }) => {
+                             let severity = &severity.as_ref()?.0;
+                             let fields = match fields {
+                                 syn::Fields::Named(_) => quote! { { .. } },
+                                 syn::Fields::Unnamed(_) => quote! { (..) },
+                                 syn::Fields::Unit => quote!{},
+                             };
+                             Some(quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), })
+                         }
+                     }
                 },
             )
+            .flatten()
             .collect::<Vec<_>>();
         if sev_pairs.is_empty() {
             None
diff --git a/miette-derive/src/snippets.rs b/miette-derive/src/snippets.rs
index 62e06ed..fedc8e2 100644
--- a/miette-derive/src/snippets.rs
+++ b/miette-derive/src/snippets.rs
@@ -9,8 +9,13 @@ use syn::{
     Token,
 };
 
-use crate::fmt;
-use crate::{diagnostic::DiagnosticVariant, fmt::Display};
+use crate::{
+    diagnostic::DiagnosticConcreteArgs, fmt::Display, utils::forward_to_single_field_variant,
+};
+use crate::{
+    diagnostic::{DiagnosticDef, DiagnosticDefArgs},
+    fmt,
+};
 
 pub struct Snippets(Vec<Snippet>);
 
@@ -290,114 +295,127 @@ impl Snippets {
         })
     }
 
-    pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
+    pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
         let variant_arms = variants.iter().map(|variant| {
-            variant.snippets.as_ref().map(|snippets| {
-                let variant_snippets = snippets.0.iter().map(|snippet| {
-                    // snippet message
-                    let msg = if let Some(display) = &snippet.message {
-                    let members: HashSet<syn::Member> = variant.fields.iter().enumerate().map(|(i, field)| {
-                        if let Some(ident) = field.ident.as_ref().cloned() {
-                            syn::Member::Named(ident)
-                        } else {
-                            syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
-                        }
-                    }).collect();
-                        let mut display = display.clone();
-                        display.expand_shorthand(&members);
-                        let Display { fmt, args, .. } = display;
-                        quote! {
-                            message: std::option::Option::Some(format!(#fmt, #args)),
-                        }
-                    } else {
-                        quote! {
-                            message: std::option::Option::None,
-                        }
-                    };
-                    // Source field
-                    let src_ident = match &snippet.source {
-                        syn::Member::Named(id) => id.clone(),
-                        syn::Member::Unnamed(syn::Index { index, .. }) => {
-                            format_ident!("_{}", index)
-                        }
-                    };
-                    let src_ident = quote! {
-                        // TODO: I don't like this. Think about it more and maybe improve protocol?
-                        source: #src_ident,
-                    };
+            let DiagnosticDef { ident, fields, args: def_args } = variant;
+            match def_args {
+                DiagnosticDefArgs::Transparent => {
+                    Some(forward_to_single_field_variant(
+                        ident,
+                        fields,
+                        quote! { snippets() },
+                    ))
+                }
+                DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { snippets, .. }) => {
+                    snippets.as_ref().and_then(|snippets| {
+                        let variant_snippets = snippets.0.iter().map(|snippet| {
+                            // snippet message
+                            let msg = if let Some(display) = &snippet.message {
+                            let members: HashSet<syn::Member> = variant.fields.iter().enumerate().map(|(i, field)| {
+                                if let Some(ident) = field.ident.as_ref().cloned() {
+                                    syn::Member::Named(ident)
+                                } else {
+                                    syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
+                                }
+                            }).collect();
+                                let mut display = display.clone();
+                                display.expand_shorthand(&members);
+                                let Display { fmt, args, .. } = display;
+                                quote! {
+                                    message: std::option::Option::Some(format!(#fmt, #args)),
+                                }
+                            } else {
+                                quote! {
+                                    message: std::option::Option::None,
+                                }
+                            };
+                            // Source field
+                            let src_ident = match &snippet.source {
+                                syn::Member::Named(id) => id.clone(),
+                                syn::Member::Unnamed(syn::Index { index, .. }) => {
+                                    format_ident!("_{}", index)
+                                }
+                            };
+                            let src_ident = quote! {
+                                // TODO: I don't like this. Think about it more and maybe improve protocol?
+                                source: #src_ident,
+                            };
 
-                    // Context
-                    let context = match &snippet.snippet {
-                        syn::Member::Named(id) => id.clone(),
-                        syn::Member::Unnamed(syn::Index { index, .. }) => {
-                            format_ident!("_{}", index)
-                        }
-                    };
-                    let context = quote! {
-                        context: #context.clone().into(),
-                    };
+                            // Context
+                            let context = match &snippet.snippet {
+                                syn::Member::Named(id) => id.clone(),
+                                syn::Member::Unnamed(syn::Index { index, .. }) => {
+                                    format_ident!("_{}", index)
+                                }
+                            };
+                            let context = quote! {
+                                context: #context.clone().into(),
+                            };
 
-                    // Highlights
-                    let highlights = snippet.highlights.iter().map(|highlight| {
-                        let Highlight { highlight, label } = highlight;
-                        let m = match highlight {
-                            syn::Member::Named(id) => id.clone(),
-                            syn::Member::Unnamed(syn::Index { index, .. }) => {
-                                format_ident!("_{}", index)
-                            }
-                        };
-                        if let Some(Display { fmt, args, ..}) = label {
-                            quote! {
-                                (
-                                    std::option::Option::Some(format!(#fmt, #args)),
-                                    #m.clone().into()
-                                )
-                            }
-                        } else {
+                            // Highlights
+                            let highlights = snippet.highlights.iter().map(|highlight| {
+                                let Highlight { highlight, label } = highlight;
+                                let m = match highlight {
+                                    syn::Member::Named(id) => id.clone(),
+                                    syn::Member::Unnamed(syn::Index { index, .. }) => {
+                                        format_ident!("_{}", index)
+                                    }
+                                };
+                                if let Some(Display { fmt, args, ..}) = label {
+                                    quote! {
+                                        (
+                                            std::option::Option::Some(format!(#fmt, #args)),
+                                            #m.clone().into()
+                                        )
+                                    }
+                                } else {
+                                    quote! {
+                                        (std::option::Option::None, #m.clone().into())
+                                    }
+                                }
+                            });
+                            let highlights = quote! {
+                                highlights: std::option::Option::Some(vec![
+                                    #(#highlights),*
+                                ]),
+                            };
+
+                            // Generate the snippet itself
                             quote! {
-                                (std::option::Option::None, #m.clone().into())
+                                miette::DiagnosticSnippet {
+                                    #msg
+                                    #src_ident
+                                    #context
+                                    #highlights
+                                }
                             }
+                        });
+                        let variant_name = variant.ident.clone();
+                        let members = variant.fields.iter().enumerate().map(|(i, field)| {
+                            field
+                                .ident
+                                .as_ref()
+                                .cloned()
+                                .unwrap_or_else(|| format_ident!("_{}", i))
+                        });
+                        match &variant.fields {
+                            syn::Fields::Unit => None,
+                            syn::Fields::Named(_) => Some(quote! {
+                                Self::#variant_name { #(#members),* } => std::option::Option::Some(std::boxed::Box::new(vec![
+                                    #(#variant_snippets),*
+                                ].into_iter())),
+                            }),
+                            syn::Fields::Unnamed(_) => Some(quote! {
+                                Self::#variant_name(#(#members),*) => std::option::Option::Some(Box::new(vec![
+                                    #(#variant_snippets),*
+                                ].into_iter())),
+                            }),
                         }
-                    });
-                    let highlights = quote! {
-                        highlights: std::option::Option::Some(vec![
-                            #(#highlights),*
-                        ]),
-                    };
-
-                    // Generate the snippet itself
-                    quote! {
-                        miette::DiagnosticSnippet {
-                            #msg
-                            #src_ident
-                            #context
-                            #highlights
-                        }
-                    }
-                });
-                let variant_name = variant.ident.clone();
-                let members = variant.fields.iter().enumerate().map(|(i, field)| {
-                    field
-                        .ident
-                        .as_ref()
-                        .cloned()
-                        .unwrap_or_else(|| format_ident!("_{}", i))
-                });
-                match &variant.fields {
-                    syn::Fields::Unit => None,
-                    syn::Fields::Named(_) => Some(quote! {
-                        Self::#variant_name { #(#members),* } => std::option::Option::Some(std::boxed::Box::new(vec![
-                            #(#variant_snippets),*
-                        ].into_iter())),
-                    }),
-                    syn::Fields::Unnamed(_) => Some(quote! {
-                        Self::#variant_name(#(#members),*) => std::option::Option::Some(Box::new(vec![
-                            #(#variant_snippets),*
-                        ].into_iter())),
-                    }),
+                    })
                 }
-            })
-        });
+            }
+        })
+        .flatten();
         Some(quote! {
             #[allow(unused_variables)]
             fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> {
diff --git a/miette-derive/src/url.rs b/miette-derive/src/url.rs
index 0097700..080e0ca 100644
--- a/miette-derive/src/url.rs
+++ b/miette-derive/src/url.rs
@@ -9,8 +9,11 @@ use syn::{
     Fields, Token,
 };
 
-use crate::diagnostic::DiagnosticVariant;
 use crate::fmt::{self, Display};
+use crate::{
+    diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
+    utils::forward_to_single_field_variant,
+};
 
 pub enum Url {
     Display(Display),
@@ -64,18 +67,19 @@ impl Parse for Url {
 impl Url {
     pub(crate) fn gen_enum(
         enum_name: &syn::Ident,
-        variants: &[DiagnosticVariant],
+        variants: &[DiagnosticDef],
     ) -> Option<TokenStream> {
-        let url_pairs = variants
-            .iter()
-            .filter(|v| v.url.is_some())
-            .map(
-                |DiagnosticVariant {
-                     ref ident,
-                     ref url,
-                     ref fields,
-                     ..
-                 }| {
+        let url_pairs = variants.iter().map(|variant| {
+            let DiagnosticDef { ident, fields, args: def_args } = variant;
+            match def_args {
+                DiagnosticDefArgs::Transparent => {
+                    Some(forward_to_single_field_variant(
+                        ident,
+                        fields,
+                        quote! { url() },
+                    ))
+                }
+                DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { ref url, .. }) => {
                     let member_idents = fields.iter().enumerate().map(|(i, field)| {
                         field
                             .ident
@@ -90,7 +94,8 @@ impl Url {
                             syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
                         }
                     }).collect();
-                    let (fmt, args) = match url.as_ref().expect("MIETTE BUG: we already checked for `Some`") {
+                    let (fmt, args) = match url.as_ref()? {
+                        // fall through to `_ => None` below
                         Url::Display(display) => {
                             let mut display = display.clone();
                             display.expand_shorthand(&members);
@@ -108,7 +113,7 @@ impl Url {
                             (fmt, args)
                         }
                     };
-                    match fields {
+                    Some(match fields {
                         syn::Fields::Named(_) => {
                             quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
                         }
@@ -117,10 +122,12 @@ impl Url {
                         }
                         syn::Fields::Unit =>
                             quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), },
-                    }
-                 },
-            )
-            .collect::<Vec<_>>();
+                    })
+                }
+            }
+         })
+        .flatten()
+        .collect::<Vec<_>>();
         if url_pairs.is_empty() {
             None
         } else {
diff --git a/miette-derive/src/utils.rs b/miette-derive/src/utils.rs
index 389d849..428b14f 100644
--- a/miette-derive/src/utils.rs
+++ b/miette-derive/src/utils.rs
@@ -1,5 +1,5 @@
 use proc_macro2::TokenStream;
-use quote::ToTokens;
+use quote::{format_ident, quote, ToTokens};
 use syn::parse::{Parse, ParseStream};
 
 pub(crate) enum MemberOrString {
@@ -32,3 +32,35 @@ impl Parse for MemberOrString {
         }
     }
 }
+
+// bool here is whether to use curly braces
+pub fn single_field_name(fields: &syn::Fields) -> Option<(bool, &syn::Field)> {
+    match fields {
+        syn::Fields::Named(f) if f.named.len() == 1 => f.named.first().map(|x| (true, x)),
+        syn::Fields::Unnamed(f) if f.unnamed.len() == 1 => f.unnamed.first().map(|x| (false, x)),
+        _ => None,
+    }
+}
+
+// Returns a match arm
+pub fn forward_to_single_field_variant(
+    ident: &syn::Ident,
+    fields: &syn::Fields,
+    method_call: TokenStream,
+) -> TokenStream {
+    if let Some((curly, single_field)) = single_field_name(fields) {
+        let field_name = single_field
+            .ident
+            .clone()
+            .unwrap_or_else(|| format_ident!("unnamed"));
+        if curly {
+            quote! { Self::#ident { #field_name } => #field_name.#method_call, }
+        } else {
+            quote! { Self::#ident(#field_name) => #field_name.#method_call, }
+        }
+    } else {
+        quote! {
+            _ => compile_error!("miette: used `#[diagnostic(transparent)]` on variant without one single field"),
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 320da08..1068ba7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -15,3 +15,6 @@ mod printer;
 mod protocol;
 mod source_impls;
 mod utils;
+
+#[cfg(doctest)]
+mod compile_test;
diff --git a/src/printer/graphical_printer.rs b/src/printer/graphical_printer.rs
index 82a76b8..897c65f 100644
--- a/src/printer/graphical_printer.rs
+++ b/src/printer/graphical_printer.rs
@@ -461,6 +461,7 @@ impl GraphicalReportPrinter {
         let mut lines = Vec::new();
         while let Some(char) = iter.next() {
             offset += char.len_utf8();
+            let mut at_end_of_file = false;
             match char {
                 '\r' => {
                     if iter.next_if_eq(&'\n').is_some() {
@@ -471,8 +472,10 @@ impl GraphicalReportPrinter {
                         line_str.push(char);
                         column += 1;
                     }
+                    at_end_of_file = iter.peek().is_none();
                 }
                 '\n' => {
+                    at_end_of_file = iter.peek().is_none();
                     line += 1;
                     column = 0;
                 }
@@ -481,7 +484,8 @@ impl GraphicalReportPrinter {
                     column += 1;
                 }
             }
-            if iter.peek().is_none() {
+
+            if iter.peek().is_none() && !at_end_of_file {
                 line += 1;
             }
 


Test patch:
diff --git a/src/compile_test.rs b/src/compile_test.rs
new file mode 100644
index 0000000..eeda312
--- /dev/null
+++ b/src/compile_test.rs
@@ -0,0 +1,94 @@
+//! A hacky but perfectly good method of adding compile_fail doctests. You can't do this in a
+//! regular tests/blah.rs file.
+
+/// ```compile_fail
+/// use thiserror::Error;
+/// use miette_derive::Diagnostic;
+/// #[derive(Debug, Diagnostic, Error)]
+/// #[error("welp")]
+/// #[diagnostic(code(foo::bar::baz))]
+/// struct Foo {}
+///
+/// #[derive(Debug, Diagnostic, Error)]
+/// enum Variants {
+///     #[error("no")]
+///     #[diagnostic(transparent)]
+///     One,
+/// }
+/// ```
+///
+/// ```compile_fail
+/// use thiserror::Error;
+/// use miette_derive::Diagnostic;
+/// #[derive(Debug, Diagnostic, Error)]
+/// #[error("welp")]
+/// #[diagnostic(code(foo::bar::baz))]
+/// struct Foo {}
+///
+/// #[derive(Debug, Diagnostic, Error)]
+/// enum Variants {
+///     #[error("no")]
+///     #[diagnostic(transparent)]
+///     One {
+///         one: Foo,
+///         two: u32,
+///     },
+/// }
+/// ```
+///
+/// ```compile_fail
+/// use thiserror::Error;
+/// use miette_derive::Diagnostic;
+/// #[derive(Debug, Diagnostic, Error)]
+/// #[error("welp")]
+/// #[diagnostic(code(foo::bar::baz))]
+/// struct Foo {}
+///
+/// #[derive(Debug, Diagnostic, Error)]
+/// enum Variants {
+///     #[error("no")]
+///     #[diagnostic(transparent)]
+///     One(Foo, u32),
+/// }
+/// ```
+///
+#[allow(dead_code)]
+#[doc(hidden)]
+struct SingleFieldTests;
+
+/// Directly on a struct with any other arg
+///
+/// ```compile_fail
+/// use thiserror::Error;
+/// use miette_derive::Diagnostic;
+/// #[derive(Debug, Diagnostic, Error)]
+/// #[error("welp")]
+/// #[diagnostic(code(foo::bar::baz))]
+/// struct Foo {}
+/// #[derive(Debug, Diagnostic, Error)]
+/// #[error("welp")]
+/// #[diagnostic(transparent, code(invalid::combo))]
+/// struct Bar(Foo);
+/// ```
+///
+/// With any other arg to diagnostic()
+///
+/// ```compile_fail
+/// use thiserror::Error;
+/// use miette_derive::Diagnostic;
+/// #[derive(Debug, Diagnostic, Error)]
+/// #[error("welp")]
+/// #[diagnostic(code(foo::bar::baz))]
+/// struct Foo {}
+///
+/// #[derive(Debug, Diagnostic, Error)]
+/// enum Variants {
+///     #[error("no")]
+///     #[diagnostic(transparent, code(invalid::combo))]
+///     One(Foo),
+/// }
+/// ```
+///
+#[allow(dead_code)]
+#[doc(hidden)]
+struct TransparentCombinations;
diff --git a/tests/derive.rs b/tests/derive.rs
index 59666de..97c391b 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -310,3 +310,132 @@ fn url_docsrs() {
         Foo {}.url().unwrap().to_string()
     );
 }
+
+const SNIPPET_TEXT: &str = "hello from miette";
+
+#[derive(Debug, Diagnostic, Error)]
+#[error("welp")]
+#[diagnostic(
+    code(foo::bar::baz),
+    url("https://example.com"),
+    help("help"),
+    severity(Warning)
+)]
+struct ForwardsTo {
+    src: String,
+    #[snippet(src, message("snippet text"))]
+    snip: miette::SourceSpan,
+    #[highlight(snip, label("highlight text"))]
+    highlight: miette::SourceSpan,
+}
+
+impl ForwardsTo {
+    fn new() -> Self {
+        ForwardsTo {
+            src: SNIPPET_TEXT.into(),
+            snip: SourceSpan::new(0.into(), SNIPPET_TEXT.len().into()),
+            highlight: SourceSpan::new(11.into(), 6.into()),
+        }
+    }
+}
+
+fn check_snippets(diag: &impl Diagnostic) {
+    // check Diagnostic impl forwards all these methods
+    assert_eq!(diag.code().to_string(), "foo::bar::baz");
+    assert_eq!(diag.url().unwrap().to_string(), "https://example.com");
+    assert_eq!(diag.help().unwrap().to_string(), "help");
+    assert_eq!(diag.severity().unwrap(), miette::Severity::Warning);
+
+    type Snip = (Option<String>, usize, usize);
+    let snips: Vec<(Snip, Vec<Snip>)> = diag
+        .snippets()
+        .unwrap()
+        .map(
+            |miette::DiagnosticSnippet {
+                 message,
+                 context,
+                 highlights,
+                 ..
+             }| {
+                (
+                    (message, context.offset(), context.len()),
+                    highlights
+                        .into_iter()
+                        .flatten()
+                        .map(|(msg, span)| (msg, span.offset(), span.len()))
+                        .collect(),
+                )
+            },
+        )
+        .collect();
+    assert_eq!(
+        &snips[..],
+        &[(
+            (Some("snippet text".into()), 0, SNIPPET_TEXT.len()),
+            vec![(Some("highlight text".into()), 11, 6)]
+        )]
+    );
+}
+
+#[test]
+fn test_transparent_enum_unnamed() {
+    #[derive(Debug, Diagnostic, Error)]
+    enum Enum {
+        #[error("enum")]
+        #[diagnostic(transparent)]
+        FooVariant(#[from] ForwardsTo),
+    }
+
+    let variant = Enum::FooVariant(ForwardsTo::new());
+
+    check_snippets(&variant);
+}
+
+#[test]
+fn test_transparent_enum_named() {
+    #[derive(Debug, Diagnostic, Error)]
+    enum Enum {
+        #[error("enum")]
+        #[diagnostic(transparent)]
+        FooVariant {
+            #[from]
+            single_field: ForwardsTo,
+        },
+        #[error("foo")]
+        #[diagnostic(code(foo::bar::bar_variant))]
+        BarVariant,
+    }
+
+    let variant = Enum::FooVariant {
+        single_field: ForwardsTo::new(),
+    };
+
+    check_snippets(&variant);
+
+    let bar_variant = Enum::BarVariant;
+    assert_eq!(bar_variant.code().to_string(), "foo::bar::bar_variant");
+}
+
+#[test]
+fn test_transparent_struct_named() {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    struct Struct {
+        #[from]
+        single_field: ForwardsTo,
+    }
+    // Also check the From impl here
+    let variant: Struct = ForwardsTo::new().into();
+    check_snippets(&variant);
+}
+
+#[test]
+fn test_transparent_struct_unnamed() {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    struct Struct(#[from] ForwardsTo);
+    let variant = Struct(ForwardsTo::new());
+    check_snippets(&variant);
+}
diff --git a/tests/printer.rs b/tests/printer.rs
index 11330da..2591899 100644
--- a/tests/printer.rs
+++ b/tests/printer.rs
@@ -277,7 +277,7 @@ line5
  3 │ ││   line3
  4 │ │├─▶ line4
    · │╰──── block 2
- 6 │ ├──▶ line5
+ 5 │ ├──▶ line5
    · ╰───── block 1
 
     ‽ try doing it better next time?
@@ -329,7 +329,7 @@ line5
  2 │ │╭─▶ line2
  3 │ ││   line3
  4 │ │╰─▶ line4
- 6 │ ├──▶ line5
+ 5 │ ├──▶ line5
    · ╰───── block 1
 
     ‽ try doing it better next time?