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::{
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<TokenStream> {
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<std::boxed::Box<dyn std::fmt::Display + 'a>> {
match self {
#(#code_pairs)*
_ => std::option::Option::None,
}
}
})
)
}
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {

View File

@ -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<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)]
pub struct DiagnosticConcreteArgs {
pub code: Option<Code>,
@ -41,6 +61,7 @@ pub struct DiagnosticConcreteArgs {
pub help: Option<Help>,
pub snippets: Option<Snippets>,
pub url: Option<Url>,
pub forward: Option<Forward>,
}
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<Self, syn::Error> {
) -> syn::Result<Self> {
let args =
attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)?;
if allow_transparent
&& args.len() == 1
&& matches!(args.first(), Some(DiagnosticArg::Transparent))
{
return Ok(Self::Transparent);
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<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#matcher
#field_name.code()
}
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#matcher
#field_name.help()
}
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#matcher
#field_name.url()
}
fn severity(&self) -> std::option::Option<miette::Severity> {
#matcher
#field_name.severity()
}
fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> {
#matcher
#field_name.snippets()
}
#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 {

View File

@ -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" {

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 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::<Token![,]>()?;
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<TokenStream> {
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<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,
}
}
})
}
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<TokenStream> {
let mut display = self.display.clone();
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 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<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#[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)))
}
})
}

View File

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

View File

@ -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<String> {
impl Severity {
pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
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::<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,
}
}
})
}
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<TokenStream> {

View File

@ -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<Snippet>);
@ -57,7 +56,6 @@ impl Parse for SnippetAttr {
let args = if content.is_empty() {
TokenStream::new()
} else {
content.parse::<Token![,]>()?;
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::<Token![,]>()?;
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<TokenStream> {
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<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 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<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> {
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<TokenStream> {
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<syn::Member> = variant.fields.iter().enumerate().map(|(i, field)| {
if let Some(ident) = field.ident.as_ref().cloned() {
syn::Member::Named(ident)
} else {
syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
}
}).collect();
let mut display = display.clone();
display.expand_shorthand(&members);
let Display { fmt, args, .. } = display;
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<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 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::<Token![,]>()?;
fmt::parse_token_expr(&content, false)?
};
let display = Display {
@ -69,78 +68,37 @@ impl Url {
enum_name: &syn::Ident,
variants: &[DiagnosticDef],
) -> Option<TokenStream> {
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<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,
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<TokenStream> {
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 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<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#[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)))
}
})
}

View File

@ -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<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 {
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<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)]
#[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;

View File

@ -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<String>, usize, usize);
let snips: Vec<(Snip, Vec<Snip>)> = 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 <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);
}
#[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(())
}
#[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(())
}