From 2fa5551c81831734fd9a162463a4a939dff9dfba Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Tue, 14 Sep 2021 08:39:04 +1000 Subject: [PATCH] feat(derive): Add `#[diagnostic(forward(field_name), code(...))]` (#41) --- miette-derive/src/code.rs | 52 +++------ miette-derive/src/diagnostic.rs | 117 +++++++++++--------- miette-derive/src/diagnostic_arg.rs | 4 + miette-derive/src/forward.rs | 150 +++++++++++++++++++++++++ miette-derive/src/help.rs | 129 ++++----------------- miette-derive/src/lib.rs | 1 + miette-derive/src/severity.rs | 56 +++------- miette-derive/src/snippets.rs | 108 +++++------------- miette-derive/src/url.rs | 166 ++++++++-------------------- miette-derive/src/utils.rs | 122 +++++++++++++++----- src/compile_test.rs | 35 ++++++ tests/derive.rs | 124 ++++++++++++++++++++- tests/printer.rs | 5 +- 13 files changed, 606 insertions(+), 463 deletions(-) create mode 100644 miette-derive/src/forward.rs diff --git a/miette-derive/src/code.rs b/miette-derive/src/code.rs index 35bc189..59aa436 100644 --- a/miette-derive/src/code.rs +++ b/miette-derive/src/code.rs @@ -7,8 +7,9 @@ use syn::{ }; use crate::{ - diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, - utils::forward_to_single_field_variant, + diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, + forward::WhichFn, + utils::gen_all_variants_with, }; #[derive(Debug)] @@ -48,41 +49,24 @@ impl Parse for Code { impl Code { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { - let code_pairs = variants.iter().map( - |DiagnosticDef { - ident, - fields, - args, - }| { - match args { - DiagnosticDefArgs::Transparent => { - Some(forward_to_single_field_variant(ident, fields, quote! { code() })) + gen_all_variants_with( + variants, + WhichFn::Code, + |ident, fields, DiagnosticConcreteArgs { code, .. }| { + let code = &code.as_ref()?.0; + Some(match fields { + syn::Fields::Named(_) => { + quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), } } - DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { code, .. }) => { - let code = &code.as_ref()?.0; - Some(match fields { - syn::Fields::Named(_) => { - quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), } - } - syn::Fields::Unnamed(_) => { - quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), } - } - syn::Fields::Unit => { - quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), } - } - }) + syn::Fields::Unnamed(_) => { + quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), } } - } + syn::Fields::Unit => { + quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), } + } + }) }, - ); - Some(quote! { - fn code<'a>(&'a self) -> std::option::Option> { - match self { - #(#code_pairs)* - _ => std::option::Option::None, - } - } - }) + ) } pub(crate) fn gen_struct(&self) -> Option { diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index 249aba6..db75868 100644 --- a/miette-derive/src/diagnostic.rs +++ b/miette-derive/src/diagnostic.rs @@ -1,9 +1,10 @@ use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; use syn::{punctuated::Punctuated, DeriveInput, Token}; use crate::code::Code; use crate::diagnostic_arg::DiagnosticArg; +use crate::forward::{Forward, WhichFn}; use crate::help::Help; use crate::severity::Severity; use crate::snippets::Snippets; @@ -30,10 +31,29 @@ pub struct DiagnosticDef { } pub enum DiagnosticDefArgs { - Transparent, + Transparent(Forward), Concrete(DiagnosticConcreteArgs), } +impl DiagnosticDefArgs { + pub(crate) fn forward_or_override_enum( + &self, + variant: &syn::Ident, + which_fn: WhichFn, + mut f: impl FnMut(&DiagnosticConcreteArgs) -> Option, + ) -> Option { + match self { + Self::Transparent(forward) => Some(forward.gen_enum_match_arm(variant, which_fn)), + Self::Concrete(concrete) => f(concrete).or_else(|| { + concrete + .forward + .as_ref() + .map(|forward| forward.gen_enum_match_arm(variant, which_fn)) + }), + } + } +} + #[derive(Default)] pub struct DiagnosticConcreteArgs { pub code: Option, @@ -41,6 +61,7 @@ pub struct DiagnosticConcreteArgs { pub help: Option, pub snippets: Option, pub url: Option, + pub forward: Option, } impl DiagnosticConcreteArgs { @@ -54,11 +75,15 @@ impl DiagnosticConcreteArgs { let mut severity = None; let mut help = None; let mut url = None; + let mut forward = None; for arg in args { match arg { DiagnosticArg::Transparent => { return Err(syn::Error::new_spanned(attr, "transparent not allowed")); } + DiagnosticArg::Forward(to_field) => { + forward = Some(to_field); + } DiagnosticArg::Code(new_code) => { // TODO: error on multiple? code = Some(new_code); @@ -81,6 +106,7 @@ impl DiagnosticConcreteArgs { severity, snippets, url, + forward, }; Ok(concrete) } @@ -92,14 +118,15 @@ impl DiagnosticDefArgs { fields: &syn::Fields, attr: &syn::Attribute, allow_transparent: bool, - ) -> Result { + ) -> syn::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); + let forward = Forward::for_transparent_field(fields)?; + return Ok(Self::Transparent(forward)); } else if args.iter().any(|x| matches!(x, DiagnosticArg::Transparent)) { return Err(syn::Error::new_spanned( attr, @@ -177,65 +204,55 @@ impl Diagnostic { } => { let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); 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") - } - }; + DiagnosticDefArgs::Transparent(forward) => { + let code_method = forward.gen_struct_method(WhichFn::Code); + let help_method = forward.gen_struct_method(WhichFn::Help); + let url_method = forward.gen_struct_method(WhichFn::Url); + let severity_method = forward.gen_struct_method(WhichFn::Severity); + let snippets_method = forward.gen_struct_method(WhichFn::Snippets); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { - fn code<'a>(&'a self) -> std::option::Option> { - #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() - } + #code_method + #help_method + #url_method + #severity_method + #snippets_method } } } DiagnosticDefArgs::Concrete(concrete) => { - let code_body = concrete.code.as_ref().and_then(|x| x.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 forward = |which| { + concrete + .forward + .as_ref() + .map(|fwd| fwd.gen_struct_method(which)) + }; + let code_body = concrete + .code + .as_ref() + .and_then(|x| x.gen_struct()) + .or_else(|| forward(WhichFn::Code)); + let help_body = concrete + .help + .as_ref() + .and_then(|x| x.gen_struct(fields)) + .or_else(|| forward(WhichFn::Help)); + let sev_body = concrete + .severity + .as_ref() + .and_then(|x| x.gen_struct()) + .or_else(|| forward(WhichFn::Severity)); let snip_body = concrete .snippets .as_ref() - .and_then(|x| x.gen_struct(fields)); + .and_then(|x| x.gen_struct(fields)) + .or_else(|| forward(WhichFn::Snippets)); let url_body = concrete .url .as_ref() - .and_then(|x| x.gen_struct(ident, fields)); + .and_then(|x| x.gen_struct(ident, fields)) + .or_else(|| forward(WhichFn::Url)); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { diff --git a/miette-derive/src/diagnostic_arg.rs b/miette-derive/src/diagnostic_arg.rs index e56cc7a..bade6f0 100644 --- a/miette-derive/src/diagnostic_arg.rs +++ b/miette-derive/src/diagnostic_arg.rs @@ -1,6 +1,7 @@ use syn::parse::{Parse, ParseStream}; use crate::code::Code; +use crate::forward::Forward; use crate::help::Help; use crate::severity::Severity; use crate::url::Url; @@ -11,6 +12,7 @@ pub enum DiagnosticArg { Severity(Severity), Help(Help), Url(Url), + Forward(Forward), } impl Parse for DiagnosticArg { @@ -20,6 +22,8 @@ impl Parse for DiagnosticArg { // consume the token let _: syn::Ident = input.parse()?; Ok(DiagnosticArg::Transparent) + } else if ident == "forward" { + Ok(DiagnosticArg::Forward(input.parse()?)) } else if ident == "code" { Ok(DiagnosticArg::Code(input.parse()?)) } else if ident == "severity" { diff --git a/miette-derive/src/forward.rs b/miette-derive/src/forward.rs new file mode 100644 index 0000000..6c7b539 --- /dev/null +++ b/miette-derive/src/forward.rs @@ -0,0 +1,150 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream}, + spanned::Spanned, +}; + +pub enum Forward { + Unnamed(usize), + Named(syn::Ident), +} + +impl Parse for Forward { + fn parse(input: ParseStream) -> syn::Result { + let forward = input.parse::()?; + if forward != "forward" { + return Err(syn::Error::new(forward.span(), "msg")); + } + let content; + parenthesized!(content in input); + let looky = content.lookahead1(); + if looky.peek(syn::LitInt) { + let int: syn::LitInt = content.parse()?; + let index = int.base10_parse()?; + return Ok(Forward::Unnamed(index)); + } + Ok(Forward::Named(content.parse()?)) + } +} + +#[derive(Copy, Clone)] +pub enum WhichFn { + Code, + Help, + Url, + Severity, + Snippets, +} + +impl WhichFn { + pub fn method_call(&self) -> TokenStream { + match self { + Self::Code => quote! { code() }, + Self::Help => quote! { help() }, + Self::Url => quote! { url() }, + Self::Severity => quote! { severity() }, + Self::Snippets => quote! { snippets() }, + } + } + + pub fn signature(&self) -> TokenStream { + match self { + Self::Code => quote! { + fn code<'a>(&'a self) -> std::option::Option> + }, + Self::Help => quote! { + fn help<'a>(&'a self) -> std::option::Option> + }, + Self::Url => quote! { + fn url<'a>(&'a self) -> std::option::Option> + }, + Self::Severity => quote! { + fn severity(&self) -> std::option::Option + }, + Self::Snippets => quote! { + fn snippets(&self) -> std::option::Option + '_>> + }, + } + } + + pub fn catchall_arm(&self) -> TokenStream { + match self { + // required, hence method can't return None + Self::Code => quote! {}, + _ => quote! { _ => None, }, + } + } +} + +impl Forward { + pub fn for_transparent_field(fields: &syn::Fields) -> syn::Result { + let make_err = || { + syn::Error::new( + fields.span(), + "you can only use #[diagnostic(transparent)] with exactly one field", + ) + }; + match fields { + syn::Fields::Named(named) => { + let mut iter = named.named.iter(); + let field = iter.next().ok_or_else(make_err)?; + if iter.next().is_some() { + return Err(make_err()); + } + let field_name = field + .ident + .clone() + .unwrap_or_else(|| format_ident!("unnamed")); + Ok(Self::Named(field_name)) + } + syn::Fields::Unnamed(unnamed) => { + if unnamed.unnamed.iter().len() != 1 { + return Err(make_err()); + } + Ok(Self::Unnamed(0)) + } + _ => Err(syn::Error::new( + fields.span(), + "you cannot use #[diagnostic(transparent)] with a unit struct or a unit variant", + )), + } + } + + pub fn gen_struct_method(&self, which_fn: WhichFn) -> TokenStream { + let signature = which_fn.signature(); + let method_call = which_fn.method_call(); + + let field_name = match self { + Forward::Named(field_name) => quote!(#field_name), + Forward::Unnamed(index) => { + let index = syn::Index::from(*index); + quote!(#index) + } + }; + + quote! { + #[inline] + #signature { + self.#field_name.#method_call + } + } + } + + pub fn gen_enum_match_arm(&self, variant: &syn::Ident, which_fn: WhichFn) -> TokenStream { + let method_call = which_fn.method_call(); + match self { + Forward::Named(field_name) => quote! { + Self::#variant { #field_name, .. } => #field_name.#method_call, + }, + Forward::Unnamed(index) => { + let underscores: Vec<_> = core::iter::repeat(quote! { _, }).take(*index).collect(); + let unnamed = format_ident!("unnamed"); + quote! { + Self::#variant ( #(#underscores)* #unnamed, .. ) => #unnamed.#method_call, + } + } + } + } +} diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs index 35154fa..c8879ca 100644 --- a/miette-derive/src/help.rs +++ b/miette-derive/src/help.rs @@ -1,18 +1,18 @@ -use std::collections::HashSet; - use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; use syn::{ parenthesized, parse::{Parse, ParseStream}, - spanned::Spanned, Fields, Token, }; -use crate::fmt::{self, Display}; use crate::{ - diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, - utils::forward_to_single_field_variant, + diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, + utils::{display_pat_members, gen_all_variants_with}, +}; +use crate::{ + fmt::{self, Display}, + forward::WhichFn, }; pub struct Help { @@ -31,7 +31,6 @@ impl Parse for Help { let args = if content.is_empty() { TokenStream::new() } else { - content.parse::()?; fmt::parse_token_expr(&content, false)? }; let display = Display { @@ -58,110 +57,28 @@ impl Parse for Help { impl Help { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { - let help_pairs = variants - .iter() - .map( - |DiagnosticDef { - ident, - fields, - 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 - } else { - Some(quote! { - fn help<'a>(&'a self) -> std::option::Option> { - #[allow(unused_variables, deprecated)] - match self { - #(#help_pairs)* - _ => None, - } - } - }) - } + gen_all_variants_with( + variants, + WhichFn::Help, + |ident, fields, DiagnosticConcreteArgs { help, .. }| { + let (display_pat, display_members) = display_pat_members(fields); + let display = &help.as_ref()?.display; + let (fmt, args) = display.expand_shorthand_cloned(&display_members); + Some(quote! { + Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))), + }) + }, + ) } pub(crate) fn gen_struct(&self, fields: &Fields) -> Option { - let mut display = self.display.clone(); - 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 members = members.iter(); - let Display { fmt, args, .. } = display; - let fields_pat = match fields { - Fields::Named(_) => quote! { - let Self { #(#members),* } = self; - }, - Fields::Unnamed(_) => { - let vars = members.map(|member| { - if let syn::Member::Unnamed(member) = member { - format_ident!("_{}", member) - } else { - unreachable!() - } - }); - quote! { - let Self(#(#vars),*) = self; - } - } - Fields::Unit => quote! {}, - }; + let (display_pat, display_members) = display_pat_members(fields); + let (fmt, args) = self.display.expand_shorthand_cloned(&display_members); Some(quote! { fn help<'a>(&'a self) -> std::option::Option> { #[allow(unused_variables, deprecated)] - #fields_pat - std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))) + let Self #display_pat = self; + std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))) } }) } diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index 9a6d616..c99f813 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -7,6 +7,7 @@ mod code; mod diagnostic; mod diagnostic_arg; mod fmt; +mod forward; mod help; mod severity; mod snippets; diff --git a/miette-derive/src/severity.rs b/miette-derive/src/severity.rs index d828335..4f26e4e 100644 --- a/miette-derive/src/severity.rs +++ b/miette-derive/src/severity.rs @@ -7,8 +7,9 @@ use syn::{ }; use crate::{ - diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, - utils::forward_to_single_field_variant, + diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, + forward::WhichFn, + utils::gen_all_variants_with, }; pub struct Severity(pub syn::Ident); @@ -60,42 +61,21 @@ fn get_severity(input: &str, span: Span) -> syn::Result { impl Severity { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { - let sev_pairs = variants - .iter() - .map( - |DiagnosticDef { - ident, fields, args - }| { - 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 - } else { - Some(quote! { - fn severity(&self) -> std::option::Option { - match self { - #(#sev_pairs)* - _ => std::option::Option::None, - } - } - }) - } + gen_all_variants_with( + variants, + WhichFn::Severity, + |ident, fields, 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), }, + ) + }, + ) } pub(crate) fn gen_struct(&self) -> Option { diff --git a/miette-derive/src/snippets.rs b/miette-derive/src/snippets.rs index fedc8e2..26a53b1 100644 --- a/miette-derive/src/snippets.rs +++ b/miette-derive/src/snippets.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -10,11 +10,10 @@ use syn::{ }; use crate::{ - diagnostic::DiagnosticConcreteArgs, fmt::Display, utils::forward_to_single_field_variant, -}; -use crate::{ - diagnostic::{DiagnosticDef, DiagnosticDefArgs}, - fmt, + diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, + fmt::{self, Display}, + forward::WhichFn, + utils::{display_pat_members, gen_all_variants_with}, }; pub struct Snippets(Vec); @@ -57,7 +56,6 @@ impl Parse for SnippetAttr { let args = if content.is_empty() { TokenStream::new() } else { - content.parse::()?; fmt::parse_token_expr(&content, false)? }; let display = Display { @@ -106,7 +104,6 @@ impl Parse for HighlightAttr { let args = if content.is_empty() { TokenStream::new() } else { - content.parse::()?; fmt::parse_token_expr(&content, false)? }; let display = Display { @@ -210,28 +207,15 @@ impl Snippets { } pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option { + let (display_pat, display_members) = display_pat_members(fields); let snippets = self.0.iter().map(|snippet| { // snippet message let msg = if let Some(display) = &snippet.message { - 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(); - let mut display = display.clone(); - display.expand_shorthand(&members); - let Display { fmt, args, .. } = display; + let (fmt, args) = display.expand_shorthand_cloned(&display_members); quote! { - message: std::option::Option::Some(format!(#fmt, #args)), + message: { + std::option::Option::Some(format!(#fmt #args)) + }, } } else { quote! { @@ -254,12 +238,11 @@ impl Snippets { // Highlights let highlights = snippet.highlights.iter().map(|highlight| { let Highlight { highlight, label } = highlight; - if let Some(Display { fmt, args, .. }) = label { + if let Some(display) = label { + let (fmt, args) = display.expand_shorthand_cloned(&display_members); quote! { ( - std::option::Option::Some( - format!(#fmt, #args) - ), + std::option::Option::Some(format!(#fmt #args)), self.#highlight.clone().into() ) } @@ -288,6 +271,7 @@ impl Snippets { Some(quote! { #[allow(unused_variables)] fn snippets(&self) -> std::option::Option + '_>> { + let Self #display_pat = self; Some(Box::new(vec![ #(#snippets),* ].into_iter())) @@ -296,31 +280,16 @@ impl Snippets { } pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { - let variant_arms = 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! { snippets() }, - )) - } - DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { snippets, .. }) => { - snippets.as_ref().and_then(|snippets| { + gen_all_variants_with( + variants, + WhichFn::Snippets, + |ident, fields, DiagnosticConcreteArgs { snippets, .. }| { + let (display_pat, display_members) = display_pat_members(fields); + 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; + let (fmt, args) = display.expand_shorthand_cloned(&display_members); quote! { message: std::option::Option::Some(format!(#fmt, #args)), } @@ -390,40 +359,17 @@ impl Snippets { } } }); - 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 { + let variant_name = ident.clone(); + match &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![ + _ => Some(quote! { + Self::#variant_name #display_pat => std::option::Option::Some(std::boxed::Box::new(vec![ #(#variant_snippets),* ].into_iter())), }), } }) - } - } - }) - .flatten(); - Some(quote! { - #[allow(unused_variables)] - fn snippets(&self) -> std::option::Option + '_>> { - match self { - #(#variant_arms)* - _ => std::option::Option::None, - } - } - }) + }, + ) } } diff --git a/miette-derive/src/url.rs b/miette-derive/src/url.rs index 080e0ca..24f2739 100644 --- a/miette-derive/src/url.rs +++ b/miette-derive/src/url.rs @@ -1,18 +1,18 @@ -use std::collections::HashSet; - use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; use syn::{ parenthesized, parse::{Parse, ParseStream}, - spanned::Spanned, Fields, Token, }; -use crate::fmt::{self, Display}; use crate::{ - diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, - utils::forward_to_single_field_variant, + diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, + utils::{display_pat_members, gen_all_variants_with, gen_unused_pat}, +}; +use crate::{ + fmt::{self, Display}, + forward::WhichFn, }; pub enum Url { @@ -33,7 +33,6 @@ impl Parse for Url { let args = if content.is_empty() { TokenStream::new() } else { - content.parse::()?; fmt::parse_token_expr(&content, false)? }; let display = Display { @@ -69,78 +68,37 @@ impl Url { enum_name: &syn::Ident, variants: &[DiagnosticDef], ) -> Option { - 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 - .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(); - let (fmt, args) = match url.as_ref()? { - // fall through to `_ => None` below - Url::Display(display) => { - let mut display = display.clone(); - display.expand_shorthand(&members); - let Display { fmt, args, .. } = display; - (fmt.value(), args) - } - Url::DocsRs => { - let fmt = "https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into(); - let item_path = format!("enum.{}.html#variant.{}", enum_name, ident); - let args = quote! { - crate_name=env!("CARGO_PKG_NAME"), - crate_version=env!("CARGO_PKG_VERSION"), - item_path=#item_path - }; - (fmt, args) - } - }; - 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 url_pairs.is_empty() { - None - } else { - Some(quote! { - fn url<'a>(&'a self) -> std::option::Option> { - #[allow(unused_variables, deprecated)] - match self { - #(#url_pairs)* - _ => None, + gen_all_variants_with( + variants, + WhichFn::Url, + |ident, fields, DiagnosticConcreteArgs { url, .. }| { + let (pat, fmt, args) = match url.as_ref()? { + // fall through to `_ => None` below + Url::Display(display) => { + let (display_pat, display_members) = display_pat_members(fields); + let (fmt, args) = display.expand_shorthand_cloned(&display_members); + (display_pat, fmt.value(), args) } - } - }) - } + Url::DocsRs => { + let pat = gen_unused_pat(fields); + let fmt = + "https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}" + .into(); + let item_path = format!("enum.{}.html#variant.{}", enum_name, ident); + let args = quote! { + , + crate_name=env!("CARGO_PKG_NAME"), + crate_version=env!("CARGO_PKG_VERSION"), + item_path=#item_path + }; + (pat, fmt, args) + } + }; + Some(quote! { + Self::#ident #pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))), + }) + }, + ) } pub(crate) fn gen_struct( @@ -148,63 +106,31 @@ impl Url { struct_name: &syn::Ident, fields: &Fields, ) -> Option { - 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(); - let (fmt, args) = match self { + let (pat, fmt, args) = match self { Url::Display(display) => { - let mut display = display.clone(); - display.expand_shorthand(&members); - let Display { fmt, args, .. } = display; - (fmt.value(), args) + let (display_pat, display_members) = display_pat_members(fields); + let (fmt, args) = display.expand_shorthand_cloned(&display_members); + (display_pat, fmt.value(), args) } Url::DocsRs => { + let pat = gen_unused_pat(fields); let fmt = "https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into(); let item_path = format!("struct.{}.html", struct_name); let args = quote! { + , crate_name=env!("CARGO_PKG_NAME"), crate_version=env!("CARGO_PKG_VERSION"), item_path=#item_path }; - (fmt, args) + (pat, fmt, args) } }; - let members = members.iter(); - let fields_pat = match fields { - Fields::Named(_) => quote! { - let Self { #(#members),* } = self; - }, - Fields::Unnamed(_) => { - let vars = members.map(|member| { - if let syn::Member::Unnamed(member) = member { - format_ident!("_{}", member) - } else { - unreachable!() - } - }); - quote! { - let Self(#(#vars),*) = self; - } - } - Fields::Unit => quote! {}, - }; Some(quote! { fn url<'a>(&'a self) -> std::option::Option> { #[allow(unused_variables, deprecated)] - #fields_pat - std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))) + let Self #pat = self; + std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))) } }) } diff --git a/miette-derive/src/utils.rs b/miette-derive/src/utils.rs index 428b14f..1172016 100644 --- a/miette-derive/src/utils.rs +++ b/miette-derive/src/utils.rs @@ -1,6 +1,9 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; -use syn::parse::{Parse, ParseStream}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, +}; pub(crate) enum MemberOrString { Member(syn::Member), @@ -33,34 +36,103 @@ impl Parse for MemberOrString { } } -// bool here is whether to use curly braces -pub fn single_field_name(fields: &syn::Fields) -> Option<(bool, &syn::Field)> { +use crate::{ + diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, + forward::WhichFn, +}; + +pub(crate) fn gen_all_variants_with( + variants: &[DiagnosticDef], + which_fn: WhichFn, + mut f: impl FnMut(&syn::Ident, &syn::Fields, &DiagnosticConcreteArgs) -> Option, +) -> Option { + let pairs = variants + .iter() + .map(|def| { + def.args + .forward_or_override_enum(&def.ident, which_fn, |concrete| { + f(&def.ident, &def.fields, concrete) + }) + }) + .flatten() + .collect::>(); + if pairs.is_empty() { + return None; + } + let signature = which_fn.signature(); + let catchall = which_fn.catchall_arm(); + Some(quote! { + #signature { + #[allow(unused_variables, deprecated)] + match self { + #(#pairs)* + #catchall + } + } + }) +} + +use crate::fmt::Display; +use std::collections::HashSet; + +pub(crate) fn gen_unused_pat(fields: &syn::Fields) -> TokenStream { 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, + syn::Fields::Named(_) => quote! { { .. } }, + syn::Fields::Unnamed(_) => quote! { ( .. ) }, + syn::Fields::Unit => quote! {}, } } -// 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 +/// Goes in the slot `let Self #pat = self;` or `match self { Self #pat => ... }`. +fn gen_fields_pat(fields: &syn::Fields) -> TokenStream { + let member_idents = fields.iter().enumerate().map(|(i, field)| { + 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"), - } + .as_ref() + .cloned() + .unwrap_or_else(|| format_ident!("_{}", i)) + }); + match fields { + syn::Fields::Named(_) => quote! { + { #(#member_idents),* } + }, + syn::Fields::Unnamed(_) => quote! { + ( #(#member_idents),* ) + }, + syn::Fields::Unit => quote! {}, + } +} + +/// The returned tokens go in the slot `let Self #pat = self;` or `match self { Self #pat => ... }`. +/// The members can be passed to `Display::expand_shorthand[_cloned]`. +pub(crate) fn display_pat_members(fields: &syn::Fields) -> (TokenStream, HashSet) { + let pat = gen_fields_pat(fields); + 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(); + (pat, members) +} + +impl Display { + /// Returns `(fmt, args)` which must be passed to some kind of format macro without tokens in between, i.e. `format!(#fmt #args)`. + pub(crate) fn expand_shorthand_cloned( + &self, + members: &HashSet, + ) -> (syn::LitStr, TokenStream) { + let mut display = self.clone(); + display.expand_shorthand(members); + let Display { fmt, args, .. } = display; + (fmt, args) } } diff --git a/src/compile_test.rs b/src/compile_test.rs index eeda312..790867f 100644 --- a/src/compile_test.rs +++ b/src/compile_test.rs @@ -92,3 +92,38 @@ struct SingleFieldTests; #[allow(dead_code)] #[doc(hidden)] struct TransparentCombinations; + +/// Forwarding without overriding the code (struct) +/// +/// ```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(forward(0))] +/// struct Bar(Foo); +/// ``` +/// +/// Forwarding without overriding the code (enum) +/// +/// ```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 Enum { +/// #[error("welp")] +/// #[diagnostic(forward(0))] +/// Bar(Foo) } +/// ``` +/// +#[allow(dead_code)] +#[doc(hidden)] +struct ForwardWithoutCode; diff --git a/tests/derive.rs b/tests/derive.rs index 9257650..44a8827 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -322,7 +322,8 @@ const SNIPPET_TEXT: &str = "hello from miette"; #[derive(Debug, Diagnostic, Error)] #[error("welp")] #[diagnostic( - code(foo::bar::baz), + // code not necessary. + // code(foo::bar::baz), url("https://example.com"), help("help"), severity(Warning) @@ -345,13 +346,16 @@ impl ForwardsTo { } } -fn check_snippets(diag: &impl Diagnostic) { +fn check_all(diag: &impl Diagnostic) { // check Diagnostic impl forwards all these methods - assert_eq!(diag.code().unwrap().to_string(), "foo::bar::baz"); + assert_eq!(diag.code().as_ref().map(|x| x.to_string()), None); 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); + check_snippets(diag); +} +fn check_snippets(diag: &impl Diagnostic) { type Snip = (Option, usize, usize); let snips: Vec<(Snip, Vec)> = diag .snippets() @@ -394,7 +398,7 @@ fn test_transparent_enum_unnamed() { let variant = Enum::FooVariant(ForwardsTo::new()); - check_snippets(&variant); + check_all(&variant); } #[test] @@ -416,7 +420,7 @@ fn test_transparent_enum_named() { single_field: ForwardsTo::new(), }; - check_snippets(&variant); + check_all(&variant); let bar_variant = Enum::BarVariant; assert_eq!( @@ -436,7 +440,7 @@ fn test_transparent_struct_named() { } // Also check the From impl here let variant: Struct = ForwardsTo::new().into(); - check_snippets(&variant); + check_all(&variant); } #[test] @@ -446,5 +450,113 @@ fn test_transparent_struct_unnamed() { #[diagnostic(transparent)] struct Struct(#[from] ForwardsTo); let variant = Struct(ForwardsTo::new()); + check_all(&variant); +} + +#[test] +fn test_forward_struct_named() { + #[derive(Debug, Diagnostic, Error)] + #[error("display")] + #[diagnostic( + code(foo::bar::overridden), + severity(Advice), + help("{help}"), + forward(span) + )] + struct Struct { + span: ForwardsTo, + help: &'static str, + } + // Also check the From impl here + let diag = Struct { + span: ForwardsTo::new(), + help: "overridden help please", + }; + assert_eq!(diag.code().unwrap().to_string(), "foo::bar::overridden"); + assert_eq!(diag.help().unwrap().to_string(), "overridden help please"); + assert_eq!(diag.severity(), Some(Severity::Advice)); + // this comes from ::snippets() + check_snippets(&diag); +} + +#[test] +fn test_forward_struct_unnamed() { + #[derive(Debug, Diagnostic, Error)] + #[error("display")] + #[diagnostic(code(foo::bar::overridden), url("{1}"), forward(0))] + struct Struct(ForwardsTo, &'static str); + + // Also check the From impl here + let diag = Struct(ForwardsTo::new(), "url here"); + assert_eq!(diag.code().unwrap().to_string(), "foo::bar::overridden"); + assert_eq!(diag.url().unwrap().to_string(), "url here"); + // this comes from ::snippets() + check_snippets(&diag); +} + +#[test] +fn test_forward_enum_named() { + #[derive(Debug, Diagnostic, Error)] + enum Enum { + #[error("help: {help_text}")] + #[diagnostic(code(foo::bar::overridden), help("{help_text}"), forward(span))] + Variant { + span: ForwardsTo, + help_text: &'static str, + }, + } + // Also check the From impl here + let variant: Enum = Enum::Variant { + span: ForwardsTo::new(), + help_text: "overridden help please", + }; + assert_eq!(variant.code().unwrap().to_string(), "foo::bar::overridden"); + assert_eq!( + variant.help().unwrap().to_string(), + "overridden help please" + ); + // this comes from ::snippets() check_snippets(&variant); } + +#[test] +fn test_forward_enum_unnamed() { + #[derive(Debug, Diagnostic, Error)] + enum ForwardEnumUnnamed { + #[error("help: {1}")] + #[diagnostic(code(foo::bar::overridden), help("{1}"), forward(0))] + Variant(ForwardsTo, &'static str), + } + // Also check the From impl here + let variant = ForwardEnumUnnamed::Variant(ForwardsTo::new(), "overridden help please"); + assert_eq!(variant.code().unwrap().to_string(), "foo::bar::overridden"); + assert_eq!( + variant.help().unwrap().to_string(), + "overridden help please" + ); + // this comes from ::snippets() + check_snippets(&variant); +} + +#[test] +fn test_unit_struct_display() { + #[derive(Debug, Diagnostic, Error)] + #[error("unit only")] + #[diagnostic(code(foo::bar::overridden), help("hello from unit help"))] + struct UnitOnly; + assert_eq!(UnitOnly.help().unwrap().to_string(), "hello from unit help") +} + +#[test] +fn test_unit_enum_display() { + #[derive(Debug, Diagnostic, Error)] + enum Enum { + #[error("unit only")] + #[diagnostic(code(foo::bar::overridden), help("hello from unit help"))] + UnitVariant, + } + assert_eq!( + Enum::UnitVariant.help().unwrap().to_string(), + "hello from unit help" + ) +} diff --git a/tests/printer.rs b/tests/printer.rs index 0ab0e73..046a768 100644 --- a/tests/printer.rs +++ b/tests/printer.rs @@ -196,7 +196,6 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> { Ok(()) } - #[test] fn single_line_highlight_at_line_start() -> Result<(), MietteError> { #[derive(Debug, Diagnostic, Error)] @@ -234,8 +233,8 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> { ‽ try doing it better next time? "# - .trim_start() - .to_string(); + .trim_start() + .to_string(); assert_eq!(expected, out); Ok(()) }