feat(derive): Add `#[diagnostic(forward(field_name), code(...))]` (#41)

This commit is contained in:
Cormac Relf 2021-09-14 08:39:04 +10:00 committed by GitHub
parent d994add912
commit 2fa5551c81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 606 additions and 463 deletions

View File

@ -7,8 +7,9 @@ use syn::{
}; };
use crate::{ use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
utils::forward_to_single_field_variant, forward::WhichFn,
utils::gen_all_variants_with,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -48,41 +49,24 @@ impl Parse for Code {
impl Code { impl Code {
pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let code_pairs = variants.iter().map( gen_all_variants_with(
|DiagnosticDef { variants,
ident, WhichFn::Code,
fields, |ident, fields, DiagnosticConcreteArgs { code, .. }| {
args, let code = &code.as_ref()?.0;
}| { Some(match fields {
match args { syn::Fields::Named(_) => {
DiagnosticDefArgs::Transparent => { quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), }
Some(forward_to_single_field_variant(ident, fields, quote! { code() }))
} }
DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { code, .. }) => { syn::Fields::Unnamed(_) => {
let code = &code.as_ref()?.0; quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), }
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::Unit => {
quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), }
}
})
}, },
); )
Some(quote! {
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
match self {
#(#code_pairs)*
_ => std::option::Option::None,
}
}
})
} }
pub(crate) fn gen_struct(&self) -> Option<TokenStream> { pub(crate) fn gen_struct(&self) -> Option<TokenStream> {

View File

@ -1,9 +1,10 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote}; use quote::quote;
use syn::{punctuated::Punctuated, DeriveInput, Token}; use syn::{punctuated::Punctuated, DeriveInput, Token};
use crate::code::Code; use crate::code::Code;
use crate::diagnostic_arg::DiagnosticArg; use crate::diagnostic_arg::DiagnosticArg;
use crate::forward::{Forward, WhichFn};
use crate::help::Help; use crate::help::Help;
use crate::severity::Severity; use crate::severity::Severity;
use crate::snippets::Snippets; use crate::snippets::Snippets;
@ -30,10 +31,29 @@ pub struct DiagnosticDef {
} }
pub enum DiagnosticDefArgs { pub enum DiagnosticDefArgs {
Transparent, Transparent(Forward),
Concrete(DiagnosticConcreteArgs), Concrete(DiagnosticConcreteArgs),
} }
impl DiagnosticDefArgs {
pub(crate) fn forward_or_override_enum(
&self,
variant: &syn::Ident,
which_fn: WhichFn,
mut f: impl FnMut(&DiagnosticConcreteArgs) -> Option<TokenStream>,
) -> Option<TokenStream> {
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)] #[derive(Default)]
pub struct DiagnosticConcreteArgs { pub struct DiagnosticConcreteArgs {
pub code: Option<Code>, pub code: Option<Code>,
@ -41,6 +61,7 @@ pub struct DiagnosticConcreteArgs {
pub help: Option<Help>, pub help: Option<Help>,
pub snippets: Option<Snippets>, pub snippets: Option<Snippets>,
pub url: Option<Url>, pub url: Option<Url>,
pub forward: Option<Forward>,
} }
impl DiagnosticConcreteArgs { impl DiagnosticConcreteArgs {
@ -54,11 +75,15 @@ impl DiagnosticConcreteArgs {
let mut severity = None; let mut severity = None;
let mut help = None; let mut help = None;
let mut url = None; let mut url = None;
let mut forward = None;
for arg in args { for arg in args {
match arg { match arg {
DiagnosticArg::Transparent => { DiagnosticArg::Transparent => {
return Err(syn::Error::new_spanned(attr, "transparent not allowed")); return Err(syn::Error::new_spanned(attr, "transparent not allowed"));
} }
DiagnosticArg::Forward(to_field) => {
forward = Some(to_field);
}
DiagnosticArg::Code(new_code) => { DiagnosticArg::Code(new_code) => {
// TODO: error on multiple? // TODO: error on multiple?
code = Some(new_code); code = Some(new_code);
@ -81,6 +106,7 @@ impl DiagnosticConcreteArgs {
severity, severity,
snippets, snippets,
url, url,
forward,
}; };
Ok(concrete) Ok(concrete)
} }
@ -92,14 +118,15 @@ impl DiagnosticDefArgs {
fields: &syn::Fields, fields: &syn::Fields,
attr: &syn::Attribute, attr: &syn::Attribute,
allow_transparent: bool, allow_transparent: bool,
) -> Result<Self, syn::Error> { ) -> syn::Result<Self> {
let args = let args =
attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)?; attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)?;
if allow_transparent if allow_transparent
&& args.len() == 1 && args.len() == 1
&& matches!(args.first(), Some(DiagnosticArg::Transparent)) && 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)) { } else if args.iter().any(|x| matches!(x, DiagnosticArg::Transparent)) {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
attr, attr,
@ -177,65 +204,55 @@ impl Diagnostic {
} => { } => {
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
match args { match args {
DiagnosticDefArgs::Transparent => { DiagnosticDefArgs::Transparent(forward) => {
if fields.iter().len() != 1 { let code_method = forward.gen_struct_method(WhichFn::Code);
return quote! { let help_method = forward.gen_struct_method(WhichFn::Help);
compile_error!("you can only use #[diagnostic(transparent)] on a struct with exactly one field"); 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);
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! { quote! {
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> { #code_method
#matcher #help_method
#field_name.code() #url_method
} #severity_method
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> { #snippets_method
#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) => { DiagnosticDefArgs::Concrete(concrete) => {
let code_body = concrete.code.as_ref().and_then(|x| x.gen_struct()); let forward = |which| {
let help_body = concrete.help.as_ref().and_then(|x| x.gen_struct(fields)); concrete
let sev_body = concrete.severity.as_ref().and_then(|x| x.gen_struct()); .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 let snip_body = concrete
.snippets .snippets
.as_ref() .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 let url_body = concrete
.url .url
.as_ref() .as_ref()
.and_then(|x| x.gen_struct(ident, fields)); .and_then(|x| x.gen_struct(ident, fields))
.or_else(|| forward(WhichFn::Url));
quote! { quote! {
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {

View File

@ -1,6 +1,7 @@
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
use crate::code::Code; use crate::code::Code;
use crate::forward::Forward;
use crate::help::Help; use crate::help::Help;
use crate::severity::Severity; use crate::severity::Severity;
use crate::url::Url; use crate::url::Url;
@ -11,6 +12,7 @@ pub enum DiagnosticArg {
Severity(Severity), Severity(Severity),
Help(Help), Help(Help),
Url(Url), Url(Url),
Forward(Forward),
} }
impl Parse for DiagnosticArg { impl Parse for DiagnosticArg {
@ -20,6 +22,8 @@ impl Parse for DiagnosticArg {
// consume the token // consume the token
let _: syn::Ident = input.parse()?; let _: syn::Ident = input.parse()?;
Ok(DiagnosticArg::Transparent) Ok(DiagnosticArg::Transparent)
} else if ident == "forward" {
Ok(DiagnosticArg::Forward(input.parse()?))
} else if ident == "code" { } else if ident == "code" {
Ok(DiagnosticArg::Code(input.parse()?)) Ok(DiagnosticArg::Code(input.parse()?))
} else if ident == "severity" { } else if ident == "severity" {

View File

@ -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<Self> {
let forward = input.parse::<syn::Ident>()?;
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<std::boxed::Box<dyn std::fmt::Display + 'a>>
},
Self::Help => quote! {
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>>
},
Self::Url => quote! {
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>>
},
Self::Severity => quote! {
fn severity(&self) -> std::option::Option<miette::Severity>
},
Self::Snippets => quote! {
fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>>
},
}
}
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<Self> {
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,
}
}
}
}
}

View File

@ -1,18 +1,18 @@
use std::collections::HashSet;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote}; use quote::quote;
use syn::{ use syn::{
parenthesized, parenthesized,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
spanned::Spanned,
Fields, Token, Fields, Token,
}; };
use crate::fmt::{self, Display};
use crate::{ use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
utils::forward_to_single_field_variant, utils::{display_pat_members, gen_all_variants_with},
};
use crate::{
fmt::{self, Display},
forward::WhichFn,
}; };
pub struct Help { pub struct Help {
@ -31,7 +31,6 @@ impl Parse for Help {
let args = if content.is_empty() { let args = if content.is_empty() {
TokenStream::new() TokenStream::new()
} else { } else {
content.parse::<Token![,]>()?;
fmt::parse_token_expr(&content, false)? fmt::parse_token_expr(&content, false)?
}; };
let display = Display { let display = Display {
@ -58,110 +57,28 @@ impl Parse for Help {
impl Help { impl Help {
pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let help_pairs = variants gen_all_variants_with(
.iter() variants,
.map( WhichFn::Help,
|DiagnosticDef { |ident, fields, DiagnosticConcreteArgs { help, .. }| {
ident, let (display_pat, display_members) = display_pat_members(fields);
fields, let display = &help.as_ref()?.display;
args, 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))),
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
} else {
Some(quote! {
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#[allow(unused_variables, deprecated)]
match self {
#(#help_pairs)*
_ => None,
}
}
})
}
} }
pub(crate) fn gen_struct(&self, fields: &Fields) -> Option<TokenStream> { pub(crate) fn gen_struct(&self, fields: &Fields) -> Option<TokenStream> {
let mut display = self.display.clone(); let (display_pat, display_members) = display_pat_members(fields);
let members: HashSet<syn::Member> = fields let (fmt, args) = self.display.expand_shorthand_cloned(&display_members);
.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! {},
};
Some(quote! { Some(quote! {
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> { fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#[allow(unused_variables, deprecated)] #[allow(unused_variables, deprecated)]
#fields_pat let Self #display_pat = self;
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))) std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
} }
}) })
} }

View File

@ -7,6 +7,7 @@ mod code;
mod diagnostic; mod diagnostic;
mod diagnostic_arg; mod diagnostic_arg;
mod fmt; mod fmt;
mod forward;
mod help; mod help;
mod severity; mod severity;
mod snippets; mod snippets;

View File

@ -7,8 +7,9 @@ use syn::{
}; };
use crate::{ use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
utils::forward_to_single_field_variant, forward::WhichFn,
utils::gen_all_variants_with,
}; };
pub struct Severity(pub syn::Ident); pub struct Severity(pub syn::Ident);
@ -60,42 +61,21 @@ fn get_severity(input: &str, span: Span) -> syn::Result<String> {
impl Severity { impl Severity {
pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let sev_pairs = variants gen_all_variants_with(
.iter() variants,
.map( WhichFn::Severity,
|DiagnosticDef { |ident, fields, DiagnosticConcreteArgs { severity, .. }| {
ident, fields, args let severity = &severity.as_ref()?.0;
}| { let fields = match fields {
match args { syn::Fields::Named(_) => quote! { { .. } },
DiagnosticDefArgs::Transparent => { syn::Fields::Unnamed(_) => quote! { (..) },
Some(forward_to_single_field_variant(ident, fields, quote!{ severity() })) syn::Fields::Unit => quote! {},
} };
DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { severity, .. }) => { Some(
let severity = &severity.as_ref()?.0; quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), },
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
} else {
Some(quote! {
fn severity(&self) -> std::option::Option<miette::Severity> {
match self {
#(#sev_pairs)*
_ => std::option::Option::None,
}
}
})
}
} }
pub(crate) fn gen_struct(&self) -> Option<TokenStream> { pub(crate) fn gen_struct(&self) -> Option<TokenStream> {

View File

@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
@ -10,11 +10,10 @@ use syn::{
}; };
use crate::{ use crate::{
diagnostic::DiagnosticConcreteArgs, fmt::Display, utils::forward_to_single_field_variant, diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
}; fmt::{self, Display},
use crate::{ forward::WhichFn,
diagnostic::{DiagnosticDef, DiagnosticDefArgs}, utils::{display_pat_members, gen_all_variants_with},
fmt,
}; };
pub struct Snippets(Vec<Snippet>); pub struct Snippets(Vec<Snippet>);
@ -57,7 +56,6 @@ impl Parse for SnippetAttr {
let args = if content.is_empty() { let args = if content.is_empty() {
TokenStream::new() TokenStream::new()
} else { } else {
content.parse::<Token![,]>()?;
fmt::parse_token_expr(&content, false)? fmt::parse_token_expr(&content, false)?
}; };
let display = Display { let display = Display {
@ -106,7 +104,6 @@ impl Parse for HighlightAttr {
let args = if content.is_empty() { let args = if content.is_empty() {
TokenStream::new() TokenStream::new()
} else { } else {
content.parse::<Token![,]>()?;
fmt::parse_token_expr(&content, false)? fmt::parse_token_expr(&content, false)?
}; };
let display = Display { let display = Display {
@ -210,28 +207,15 @@ impl Snippets {
} }
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> { pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
let (display_pat, display_members) = display_pat_members(fields);
let snippets = self.0.iter().map(|snippet| { let snippets = self.0.iter().map(|snippet| {
// snippet message // snippet message
let msg = if let Some(display) = &snippet.message { let msg = if let Some(display) = &snippet.message {
let members: HashSet<syn::Member> = fields let (fmt, args) = display.expand_shorthand_cloned(&display_members);
.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! { quote! {
message: std::option::Option::Some(format!(#fmt, #args)), message: {
std::option::Option::Some(format!(#fmt #args))
},
} }
} else { } else {
quote! { quote! {
@ -254,12 +238,11 @@ impl Snippets {
// Highlights // Highlights
let highlights = snippet.highlights.iter().map(|highlight| { let highlights = snippet.highlights.iter().map(|highlight| {
let Highlight { highlight, label } = 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! { quote! {
( (
std::option::Option::Some( std::option::Option::Some(format!(#fmt #args)),
format!(#fmt, #args)
),
self.#highlight.clone().into() self.#highlight.clone().into()
) )
} }
@ -288,6 +271,7 @@ impl Snippets {
Some(quote! { Some(quote! {
#[allow(unused_variables)] #[allow(unused_variables)]
fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> { fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> {
let Self #display_pat = self;
Some(Box::new(vec![ Some(Box::new(vec![
#(#snippets),* #(#snippets),*
].into_iter())) ].into_iter()))
@ -296,31 +280,16 @@ impl Snippets {
} }
pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let variant_arms = variants.iter().map(|variant| { gen_all_variants_with(
let DiagnosticDef { ident, fields, args: def_args } = variant; variants,
match def_args { WhichFn::Snippets,
DiagnosticDefArgs::Transparent => { |ident, fields, DiagnosticConcreteArgs { snippets, .. }| {
Some(forward_to_single_field_variant( let (display_pat, display_members) = display_pat_members(fields);
ident, snippets.as_ref().and_then(|snippets| {
fields,
quote! { snippets() },
))
}
DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { snippets, .. }) => {
snippets.as_ref().and_then(|snippets| {
let variant_snippets = snippets.0.iter().map(|snippet| { let variant_snippets = snippets.0.iter().map(|snippet| {
// snippet message // snippet message
let msg = if let Some(display) = &snippet.message { let msg = if let Some(display) = &snippet.message {
let members: HashSet<syn::Member> = variant.fields.iter().enumerate().map(|(i, field)| { let (fmt, args) = display.expand_shorthand_cloned(&display_members);
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! { quote! {
message: std::option::Option::Some(format!(#fmt, #args)), message: std::option::Option::Some(format!(#fmt, #args)),
} }
@ -390,40 +359,17 @@ impl Snippets {
} }
} }
}); });
let variant_name = variant.ident.clone(); let variant_name = ident.clone();
let members = variant.fields.iter().enumerate().map(|(i, field)| { match &fields {
field
.ident
.as_ref()
.cloned()
.unwrap_or_else(|| format_ident!("_{}", i))
});
match &variant.fields {
syn::Fields::Unit => None, syn::Fields::Unit => None,
syn::Fields::Named(_) => Some(quote! { _ => Some(quote! {
Self::#variant_name { #(#members),* } => std::option::Option::Some(std::boxed::Box::new(vec![ Self::#variant_name #display_pat => 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),* #(#variant_snippets),*
].into_iter())), ].into_iter())),
}), }),
} }
}) })
} },
} )
})
.flatten();
Some(quote! {
#[allow(unused_variables)]
fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> {
match self {
#(#variant_arms)*
_ => std::option::Option::None,
}
}
})
} }
} }

View File

@ -1,18 +1,18 @@
use std::collections::HashSet;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote}; use quote::quote;
use syn::{ use syn::{
parenthesized, parenthesized,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
spanned::Spanned,
Fields, Token, Fields, Token,
}; };
use crate::fmt::{self, Display};
use crate::{ use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs}, diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
utils::forward_to_single_field_variant, utils::{display_pat_members, gen_all_variants_with, gen_unused_pat},
};
use crate::{
fmt::{self, Display},
forward::WhichFn,
}; };
pub enum Url { pub enum Url {
@ -33,7 +33,6 @@ impl Parse for Url {
let args = if content.is_empty() { let args = if content.is_empty() {
TokenStream::new() TokenStream::new()
} else { } else {
content.parse::<Token![,]>()?;
fmt::parse_token_expr(&content, false)? fmt::parse_token_expr(&content, false)?
}; };
let display = Display { let display = Display {
@ -69,78 +68,37 @@ impl Url {
enum_name: &syn::Ident, enum_name: &syn::Ident,
variants: &[DiagnosticDef], variants: &[DiagnosticDef],
) -> Option<TokenStream> { ) -> Option<TokenStream> {
let url_pairs = variants.iter().map(|variant| { gen_all_variants_with(
let DiagnosticDef { ident, fields, args: def_args } = variant; variants,
match def_args { WhichFn::Url,
DiagnosticDefArgs::Transparent => { |ident, fields, DiagnosticConcreteArgs { url, .. }| {
Some(forward_to_single_field_variant( let (pat, fmt, args) = match url.as_ref()? {
ident, // fall through to `_ => None` below
fields, Url::Display(display) => {
quote! { url() }, let (display_pat, display_members) = display_pat_members(fields);
)) let (fmt, args) = display.expand_shorthand_cloned(&display_members);
} (display_pat, fmt.value(), args)
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<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();
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::<Vec<_>>();
if url_pairs.is_empty() {
None
} else {
Some(quote! {
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#[allow(unused_variables, deprecated)]
match self {
#(#url_pairs)*
_ => None,
} }
} 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( pub(crate) fn gen_struct(
@ -148,63 +106,31 @@ impl Url {
struct_name: &syn::Ident, struct_name: &syn::Ident,
fields: &Fields, fields: &Fields,
) -> Option<TokenStream> { ) -> Option<TokenStream> {
let members: HashSet<syn::Member> = fields let (pat, fmt, args) = match self {
.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 {
Url::Display(display) => { Url::Display(display) => {
let mut display = display.clone(); let (display_pat, display_members) = display_pat_members(fields);
display.expand_shorthand(&members); let (fmt, args) = display.expand_shorthand_cloned(&display_members);
let Display { fmt, args, .. } = display; (display_pat, fmt.value(), args)
(fmt.value(), args)
} }
Url::DocsRs => { Url::DocsRs => {
let pat = gen_unused_pat(fields);
let fmt = let fmt =
"https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into(); "https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into();
let item_path = format!("struct.{}.html", struct_name); let item_path = format!("struct.{}.html", struct_name);
let args = quote! { let args = quote! {
,
crate_name=env!("CARGO_PKG_NAME"), crate_name=env!("CARGO_PKG_NAME"),
crate_version=env!("CARGO_PKG_VERSION"), crate_version=env!("CARGO_PKG_VERSION"),
item_path=#item_path 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! { Some(quote! {
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> { fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#[allow(unused_variables, deprecated)] #[allow(unused_variables, deprecated)]
#fields_pat let Self #pat = self;
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))) std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
} }
}) })
} }

View File

@ -1,6 +1,9 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens}; use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream}; use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
};
pub(crate) enum MemberOrString { pub(crate) enum MemberOrString {
Member(syn::Member), Member(syn::Member),
@ -33,34 +36,103 @@ impl Parse for MemberOrString {
} }
} }
// bool here is whether to use curly braces use crate::{
pub fn single_field_name(fields: &syn::Fields) -> Option<(bool, &syn::Field)> { 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<TokenStream>,
) -> Option<TokenStream> {
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::<Vec<_>>();
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 { match fields {
syn::Fields::Named(f) if f.named.len() == 1 => f.named.first().map(|x| (true, x)), syn::Fields::Named(_) => quote! { { .. } },
syn::Fields::Unnamed(f) if f.unnamed.len() == 1 => f.unnamed.first().map(|x| (false, x)), syn::Fields::Unnamed(_) => quote! { ( .. ) },
_ => None, syn::Fields::Unit => quote! {},
} }
} }
// Returns a match arm /// Goes in the slot `let Self #pat = self;` or `match self { Self #pat => ... }`.
pub fn forward_to_single_field_variant( fn gen_fields_pat(fields: &syn::Fields) -> TokenStream {
ident: &syn::Ident, let member_idents = fields.iter().enumerate().map(|(i, field)| {
fields: &syn::Fields, field
method_call: TokenStream,
) -> TokenStream {
if let Some((curly, single_field)) = single_field_name(fields) {
let field_name = single_field
.ident .ident
.clone() .as_ref()
.unwrap_or_else(|| format_ident!("unnamed")); .cloned()
if curly { .unwrap_or_else(|| format_ident!("_{}", i))
quote! { Self::#ident { #field_name } => #field_name.#method_call, } });
} else { match fields {
quote! { Self::#ident(#field_name) => #field_name.#method_call, } syn::Fields::Named(_) => quote! {
} { #(#member_idents),* }
} else { },
quote! { syn::Fields::Unnamed(_) => quote! {
_ => compile_error!("miette: used `#[diagnostic(transparent)]` on variant without one single field"), ( #(#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<syn::Member>) {
let pat = gen_fields_pat(fields);
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();
(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::Member>,
) -> (syn::LitStr, TokenStream) {
let mut display = self.clone();
display.expand_shorthand(members);
let Display { fmt, args, .. } = display;
(fmt, args)
} }
} }

View File

@ -92,3 +92,38 @@ struct SingleFieldTests;
#[allow(dead_code)] #[allow(dead_code)]
#[doc(hidden)] #[doc(hidden)]
struct TransparentCombinations; 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;

View File

@ -322,7 +322,8 @@ const SNIPPET_TEXT: &str = "hello from miette";
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
#[error("welp")] #[error("welp")]
#[diagnostic( #[diagnostic(
code(foo::bar::baz), // code not necessary.
// code(foo::bar::baz),
url("https://example.com"), url("https://example.com"),
help("help"), help("help"),
severity(Warning) 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 // 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.url().unwrap().to_string(), "https://example.com");
assert_eq!(diag.help().unwrap().to_string(), "help"); assert_eq!(diag.help().unwrap().to_string(), "help");
assert_eq!(diag.severity().unwrap(), miette::Severity::Warning); assert_eq!(diag.severity().unwrap(), miette::Severity::Warning);
check_snippets(diag);
}
fn check_snippets(diag: &impl Diagnostic) {
type Snip = (Option<String>, usize, usize); type Snip = (Option<String>, usize, usize);
let snips: Vec<(Snip, Vec<Snip>)> = diag let snips: Vec<(Snip, Vec<Snip>)> = diag
.snippets() .snippets()
@ -394,7 +398,7 @@ fn test_transparent_enum_unnamed() {
let variant = Enum::FooVariant(ForwardsTo::new()); let variant = Enum::FooVariant(ForwardsTo::new());
check_snippets(&variant); check_all(&variant);
} }
#[test] #[test]
@ -416,7 +420,7 @@ fn test_transparent_enum_named() {
single_field: ForwardsTo::new(), single_field: ForwardsTo::new(),
}; };
check_snippets(&variant); check_all(&variant);
let bar_variant = Enum::BarVariant; let bar_variant = Enum::BarVariant;
assert_eq!( assert_eq!(
@ -436,7 +440,7 @@ fn test_transparent_struct_named() {
} }
// Also check the From impl here // Also check the From impl here
let variant: Struct = ForwardsTo::new().into(); let variant: Struct = ForwardsTo::new().into();
check_snippets(&variant); check_all(&variant);
} }
#[test] #[test]
@ -446,5 +450,113 @@ fn test_transparent_struct_unnamed() {
#[diagnostic(transparent)] #[diagnostic(transparent)]
struct Struct(#[from] ForwardsTo); struct Struct(#[from] ForwardsTo);
let variant = Struct(ForwardsTo::new()); 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 <ForwardsTo as Diagnostic>::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 <ForwardsTo as Diagnostic>::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 <ForwardsTo as Diagnostic>::snippets()
check_snippets(&variant); 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 <ForwardsTo as Diagnostic>::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"
)
}

View File

@ -196,7 +196,6 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
Ok(()) Ok(())
} }
#[test] #[test]
fn single_line_highlight_at_line_start() -> Result<(), MietteError> { fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -234,8 +233,8 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
try doing it better next time? try doing it better next time?
"# "#
.trim_start() .trim_start()
.to_string(); .to_string();
assert_eq!(expected, out); assert_eq!(expected, out);
Ok(()) Ok(())
} }