From 53f5d6d1d62845b52e590fed5ce91a643b6e11f3 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 30 Aug 2021 05:33:16 +1000 Subject: [PATCH] feat(derive): Add `#[diagnostic(transparent,forward)]` (#36) Fixes: https://github.com/zkat/miette/issues/16 --- miette-derive/src/code.rs | 39 +++-- miette-derive/src/diagnostic.rs | 261 ++++++++++++++++++---------- miette-derive/src/diagnostic_arg.rs | 7 +- miette-derive/src/help.rs | 78 +++++---- miette-derive/src/severity.rs | 34 ++-- miette-derive/src/snippets.rs | 234 +++++++++++++------------ miette-derive/src/url.rs | 43 +++-- miette-derive/src/utils.rs | 34 +++- src/compile_test.rs | 94 ++++++++++ src/lib.rs | 3 + tests/derive.rs | 129 ++++++++++++++ 11 files changed, 675 insertions(+), 281 deletions(-) create mode 100644 src/compile_test.rs 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 { + pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 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, - help: Option, - snippets: Option, - url: Option, + ident: syn::Ident, + fields: syn::Fields, + args: DiagnosticDefArgs, }, Enum { ident: syn::Ident, generics: syn::Generics, - variants: Vec, + variants: Vec, }, } -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, pub help: Option, @@ -37,46 +42,94 @@ pub struct DiagnosticVariant { pub url: Option, } +impl DiagnosticConcreteArgs { + fn parse( + ident: &syn::Ident, + fields: &syn::Fields, + attr: &syn::Attribute, + args: impl Iterator, + ) -> Result { + 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 { + let args = + attr.parse_args_with(Punctuated::::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 { 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::::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::::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 { + #matcher + #field_name.code() + } + fn help<'a>(&'a self) -> std::option::Option> { + #matcher + #field_name.help() + } + fn url<'a>(&'a self) -> std::option::Option> { + #matcher + #field_name.url() + } + fn severity(&self) -> std::option::Option { + #matcher + #field_name.severity() + } + fn snippets(&self) -> std::option::Option + '_>> { + #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 { let ident = input.fork().parse::()?; - 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 { + pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 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 = 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 = 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::>(); 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 { } impl Severity { - pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option { + pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 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::>(); 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); @@ -290,114 +295,127 @@ impl Snippets { }) } - pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option { + pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 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 = 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(), - }; - - // 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! { - 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 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 = 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(), + }; + + // 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! { + 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 + '_>> { 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 { - 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::>(); + }) + } + } + }) + .flatten() + .collect::>(); 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/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/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/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, usize, usize); + let snips: Vec<(Snip, Vec)> = 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); +}