feat(derive): Add `#[diagnostic(transparent,forward)]` (#36)

Fixes: https://github.com/zkat/miette/issues/16
This commit is contained in:
Cormac Relf 2021-08-30 05:33:16 +10:00 committed by GitHub
parent 2009ab238c
commit 53f5d6d1d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 675 additions and 281 deletions

View File

@ -6,7 +6,10 @@ use syn::{
Token, Token,
}; };
use crate::diagnostic::DiagnosticVariant; use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
utils::forward_to_single_field_variant,
};
#[derive(Debug)] #[derive(Debug)]
pub struct Code(pub String); pub struct Code(pub String);
@ -44,23 +47,31 @@ impl Parse for Code {
} }
impl Code { impl Code {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let code_pairs = variants.iter().map( let code_pairs = variants.iter().map(
|DiagnosticVariant { |DiagnosticDef {
ref ident, ident,
ref code, fields,
ref fields, args,
..
}| { }| {
let code = &code.0; match args {
match fields { DiagnosticDefArgs::Transparent => {
syn::Fields::Named(_) => { forward_to_single_field_variant(ident, fields, quote! { code() })
quote! { Self::#ident { .. } => std::boxed::Box::new(#code), }
} }
syn::Fields::Unnamed(_) => { DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { code, .. }) => {
quote! { Self::#ident(..) => std::boxed::Box::new(#code), } let code = &code.0;
match fields {
syn::Fields::Named(_) => {
quote! { Self::#ident { .. } => std::boxed::Box::new(#code), }
}
syn::Fields::Unnamed(_) => {
quote! { Self::#ident(..) => std::boxed::Box::new(#code), }
}
syn::Fields::Unit => {
quote! { Self::#ident => std::boxed::Box::new(#code), }
}
}
} }
syn::Fields::Unit => quote! { Self::#ident => std::boxed::Box::new(#code), },
} }
}, },
); );

View File

@ -1,5 +1,5 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::{format_ident, quote};
use syn::{punctuated::Punctuated, DeriveInput, Token}; use syn::{punctuated::Punctuated, DeriveInput, Token};
use crate::code::Code; use crate::code::Code;
@ -11,25 +11,30 @@ use crate::url::Url;
pub enum Diagnostic { pub enum Diagnostic {
Struct { Struct {
fields: syn::Fields,
ident: syn::Ident,
generics: syn::Generics, generics: syn::Generics,
code: Code, ident: syn::Ident,
severity: Option<Severity>, fields: syn::Fields,
help: Option<Help>, args: DiagnosticDefArgs,
snippets: Option<Snippets>,
url: Option<Url>,
}, },
Enum { Enum {
ident: syn::Ident, ident: syn::Ident,
generics: syn::Generics, generics: syn::Generics,
variants: Vec<DiagnosticVariant>, variants: Vec<DiagnosticDef>,
}, },
} }
pub struct DiagnosticVariant { pub struct DiagnosticDef {
pub ident: syn::Ident, pub ident: syn::Ident,
pub fields: syn::Fields, pub fields: syn::Fields,
pub args: DiagnosticDefArgs,
}
pub enum DiagnosticDefArgs {
Transparent,
Concrete(DiagnosticConcreteArgs),
}
pub struct DiagnosticConcreteArgs {
pub code: Code, pub code: Code,
pub severity: Option<Severity>, pub severity: Option<Severity>,
pub help: Option<Help>, pub help: Option<Help>,
@ -37,46 +42,94 @@ pub struct DiagnosticVariant {
pub url: Option<Url>, pub url: Option<Url>,
} }
impl DiagnosticConcreteArgs {
fn parse(
ident: &syn::Ident,
fields: &syn::Fields,
attr: &syn::Attribute,
args: impl Iterator<Item = DiagnosticArg>,
) -> Result<Self, syn::Error> {
let mut code = None;
let mut severity = None;
let mut help = None;
let mut url = None;
for arg in args {
match arg {
DiagnosticArg::Transparent => {
return Err(syn::Error::new_spanned(attr, "transparent not allowed"));
}
DiagnosticArg::Code(new_code) => {
// TODO: error on multiple?
code = Some(new_code);
}
DiagnosticArg::Severity(sev) => {
severity = Some(sev);
}
DiagnosticArg::Help(hl) => {
help = Some(hl);
}
DiagnosticArg::Url(u) => {
url = Some(u);
}
}
}
let snippets = Snippets::from_fields(fields)?;
let concrete = DiagnosticConcreteArgs {
code: code
.ok_or_else(|| syn::Error::new(ident.span(), "Diagnostic code is required."))?,
help,
severity,
snippets,
url,
};
Ok(concrete)
}
}
impl DiagnosticDefArgs {
fn parse(
ident: &syn::Ident,
fields: &syn::Fields,
attr: &syn::Attribute,
allow_transparent: bool,
) -> Result<Self, syn::Error> {
let args =
attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)?;
if allow_transparent
&& args.len() == 1
&& matches!(args.first(), Some(DiagnosticArg::Transparent))
{
return Ok(Self::Transparent);
} else if args.iter().any(|x| matches!(x, DiagnosticArg::Transparent)) {
return Err(syn::Error::new_spanned(
attr,
if allow_transparent {
"diagnostic(transparent) not allowed in combination with other args"
} else {
"diagnostic(transparent) not allowed here"
},
));
}
let args = args
.into_iter()
.filter(|x| !matches!(x, DiagnosticArg::Transparent));
let concrete = DiagnosticConcreteArgs::parse(ident, fields, attr, args)?;
Ok(DiagnosticDefArgs::Concrete(concrete))
}
}
impl Diagnostic { impl Diagnostic {
pub fn from_derive_input(input: DeriveInput) -> Result<Self, syn::Error> { pub fn from_derive_input(input: DeriveInput) -> Result<Self, syn::Error> {
Ok(match input.data { Ok(match input.data {
syn::Data::Struct(data_struct) => { syn::Data::Struct(data_struct) => {
if let Some(attr) = input.attrs.iter().find(|x| x.path.is_ident("diagnostic")) { if let Some(attr) = input.attrs.iter().find(|x| x.path.is_ident("diagnostic")) {
let args = attr.parse_args_with( let args =
Punctuated::<DiagnosticArg, Token![,]>::parse_terminated, DiagnosticDefArgs::parse(&input.ident, &data_struct.fields, attr, true)?;
)?;
let mut code = None;
let mut severity = None;
let mut help = None;
let mut url = None;
for arg in args {
match arg {
DiagnosticArg::Code(new_code) => {
// TODO: error on multiple?
code = Some(new_code);
}
DiagnosticArg::Url(u) => {
url = Some(u);
}
DiagnosticArg::Severity(sev) => {
severity = Some(sev);
}
DiagnosticArg::Help(hl) => help = Some(hl),
}
}
let snippets = Snippets::from_fields(&data_struct.fields)?;
let ident = input.ident.clone();
Diagnostic::Struct { Diagnostic::Struct {
fields: data_struct.fields, fields: data_struct.fields,
ident: input.ident, ident: input.ident,
generics: input.generics, generics: input.generics,
code: code.ok_or_else(|| { args,
syn::Error::new(ident.span(), "Diagnostic code is required.")
})?,
help,
severity,
snippets,
url,
} }
} else { } else {
// Also handle when there's multiple `#[diagnostic]` attrs? // Also handle when there's multiple `#[diagnostic]` attrs?
@ -90,42 +143,11 @@ impl Diagnostic {
let mut vars = Vec::new(); let mut vars = Vec::new();
for var in variants { for var in variants {
if let Some(attr) = var.attrs.iter().find(|x| x.path.is_ident("diagnostic")) { if let Some(attr) = var.attrs.iter().find(|x| x.path.is_ident("diagnostic")) {
let args = attr.parse_args_with( let args = DiagnosticDefArgs::parse(&var.ident, &var.fields, attr, true)?;
Punctuated::<DiagnosticArg, Token![,]>::parse_terminated, vars.push(DiagnosticDef {
)?;
let mut code = None;
let mut severity = None;
let mut help = None;
let mut url = None;
for arg in args {
match arg {
DiagnosticArg::Code(new_code) => {
// TODO: error on multiple?
code = Some(new_code);
}
DiagnosticArg::Severity(sev) => {
severity = Some(sev);
}
DiagnosticArg::Help(hl) => {
help = Some(hl);
}
DiagnosticArg::Url(u) => {
url = Some(u);
}
}
}
let snippets = Snippets::from_fields(&var.fields)?;
let ident = input.ident.clone();
vars.push(DiagnosticVariant {
ident: var.ident, ident: var.ident,
fields: var.fields, fields: var.fields,
code: code.ok_or_else(|| { args,
syn::Error::new(ident.span(), "Diagnostic code is required.")
})?,
help,
severity,
snippets,
url,
}); });
} else { } else {
// Also handle when there's multiple `#[diagnostic]` attrs? // Also handle when there's multiple `#[diagnostic]` attrs?
@ -153,29 +175,82 @@ impl Diagnostic {
pub fn gen(&self) -> TokenStream { pub fn gen(&self) -> TokenStream {
match self { match self {
Self::Struct { Self::Struct {
fields,
ident, ident,
fields,
generics, generics,
code, args,
severity,
help,
snippets,
url,
} => { } => {
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
let code_body = code.gen_struct(); match args {
let help_body = help.as_ref().and_then(|x| x.gen_struct(fields)); DiagnosticDefArgs::Transparent => {
let sev_body = severity.as_ref().and_then(|x| x.gen_struct()); if fields.iter().len() != 1 {
let snip_body = snippets.as_ref().and_then(|x| x.gen_struct(fields)); return quote! {
let url_body = url.as_ref().and_then(|x| x.gen_struct(ident, fields)); compile_error!("you can only use #[diagnostic(transparent)] on a struct with exactly one field");
};
}
let field = fields
.iter()
.next()
.expect("MIETTE BUG: thought we knew we had exactly one field");
let field_name = field
.ident
.clone()
.unwrap_or_else(|| format_ident!("unnamed"));
let matcher = match fields {
syn::Fields::Named(_) => quote! { let Self { #field_name } = self; },
syn::Fields::Unnamed(_) => quote! { let Self(#field_name) = self; },
syn::Fields::Unit => {
unreachable!("MIETTE BUG: thought we knew we had exactly one field")
}
};
quote! { quote! {
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
#code_body fn code<'a>(&'a self) -> std::boxed::Box<dyn std::fmt::Display + 'a> {
#help_body #matcher
#sev_body #field_name.code()
#snip_body }
#url_body fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#matcher
#field_name.help()
}
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#matcher
#field_name.url()
}
fn severity(&self) -> std::option::Option<miette::Severity> {
#matcher
#field_name.severity()
}
fn snippets(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::DiagnosticSnippet> + '_>> {
#matcher
#field_name.snippets()
}
}
}
}
DiagnosticDefArgs::Concrete(concrete) => {
let code_body = concrete.code.gen_struct();
let help_body = concrete.help.as_ref().and_then(|x| x.gen_struct(fields));
let sev_body = concrete.severity.as_ref().and_then(|x| x.gen_struct());
let snip_body = concrete
.snippets
.as_ref()
.and_then(|x| x.gen_struct(fields));
let url_body = concrete
.url
.as_ref()
.and_then(|x| x.gen_struct(ident, fields));
quote! {
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
#code_body
#help_body
#sev_body
#snip_body
#url_body
}
}
} }
} }
} }

View File

@ -6,6 +6,7 @@ use crate::severity::Severity;
use crate::url::Url; use crate::url::Url;
pub enum DiagnosticArg { pub enum DiagnosticArg {
Transparent,
Code(Code), Code(Code),
Severity(Severity), Severity(Severity),
Help(Help), Help(Help),
@ -15,7 +16,11 @@ pub enum DiagnosticArg {
impl Parse for DiagnosticArg { impl Parse for DiagnosticArg {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
let ident = input.fork().parse::<syn::Ident>()?; let ident = input.fork().parse::<syn::Ident>()?;
if ident == "code" { if ident == "transparent" {
// consume the token
let _: syn::Ident = input.parse()?;
Ok(DiagnosticArg::Transparent)
} else if ident == "code" {
Ok(DiagnosticArg::Code(input.parse()?)) Ok(DiagnosticArg::Code(input.parse()?))
} else if ident == "severity" { } else if ident == "severity" {
Ok(DiagnosticArg::Severity(input.parse()?)) Ok(DiagnosticArg::Severity(input.parse()?))

View File

@ -9,8 +9,11 @@ use syn::{
Fields, Token, Fields, Token,
}; };
use crate::diagnostic::DiagnosticVariant;
use crate::fmt::{self, Display}; use crate::fmt::{self, Display};
use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
utils::forward_to_single_field_variant,
};
pub struct Help { pub struct Help {
pub display: Display, pub display: Display,
@ -54,46 +57,53 @@ impl Parse for Help {
} }
impl Help { impl Help {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let help_pairs = variants let help_pairs = variants
.iter() .iter()
.filter(|v| v.help.is_some())
.map( .map(
|DiagnosticVariant { |DiagnosticDef {
ref ident, ident,
ref help, fields,
ref fields, args,
.. ..
}| { }| {
let mut display = help.as_ref().expect("already checked for Some").display.clone(); match args {
let member_idents = fields.iter().enumerate().map(|(i, field)| { DiagnosticDefArgs::Transparent => {
field Some(forward_to_single_field_variant(ident, fields, quote!{ help() } ))
.ident }
.as_ref() DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { help, .. }) => {
.cloned() let mut display = help.as_ref()?.display.clone();
.unwrap_or_else(|| format_ident!("_{}", i)) let member_idents = fields.iter().enumerate().map(|(i, field)| {
}); field
let members: HashSet<syn::Member> = fields.iter().enumerate().map(|(i, field)| { .ident
if let Some(ident) = field.ident.as_ref().cloned() { .as_ref()
syn::Member::Named(ident) .cloned()
} else { .unwrap_or_else(|| format_ident!("_{}", i))
syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() }) });
} let members: HashSet<syn::Member> = fields.iter().enumerate().map(|(i, field)| {
}).collect(); if let Some(ident) = field.ident.as_ref().cloned() {
display.expand_shorthand(&members); syn::Member::Named(ident)
let Display { fmt, args, .. } = display; } else {
match fields { syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
syn::Fields::Named(_) => { }
quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), } }).collect();
} display.expand_shorthand(&members);
syn::Fields::Unnamed(_) => { let Display { fmt, args, .. } = display;
quote! { Self::#ident( #(#member_idents),* ) => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), } Some(match fields {
} syn::Fields::Named(_) => {
syn::Fields::Unit => quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
quote! { Self::#ident => 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<_>>(); .collect::<Vec<_>>();
if help_pairs.is_empty() { if help_pairs.is_empty() {
None None

View File

@ -6,7 +6,10 @@ use syn::{
Token, Token,
}; };
use crate::diagnostic::DiagnosticVariant; use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
utils::forward_to_single_field_variant,
};
pub struct Severity(pub syn::Ident); pub struct Severity(pub syn::Ident);
@ -56,23 +59,30 @@ fn get_severity(input: &str, span: Span) -> syn::Result<String> {
} }
impl Severity { impl Severity {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let sev_pairs = variants let sev_pairs = variants
.iter() .iter()
.filter(|v| v.severity.is_some())
.map( .map(
|DiagnosticVariant { |DiagnosticDef {
ident, severity, fields, .. ident, fields, args
}| { }| {
let severity = &severity.as_ref().unwrap().0; match args {
let fields = match fields { DiagnosticDefArgs::Transparent => {
syn::Fields::Named(_) => quote! { { .. } }, Some(forward_to_single_field_variant(ident, fields, quote!{ severity() }))
syn::Fields::Unnamed(_) => quote! { (..) }, }
syn::Fields::Unit => quote!{}, DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { severity, .. }) => {
}; 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<_>>(); .collect::<Vec<_>>();
if sev_pairs.is_empty() { if sev_pairs.is_empty() {
None None

View File

@ -9,8 +9,13 @@ use syn::{
Token, Token,
}; };
use crate::fmt; use crate::{
use crate::{diagnostic::DiagnosticVariant, fmt::Display}; diagnostic::DiagnosticConcreteArgs, fmt::Display, utils::forward_to_single_field_variant,
};
use crate::{
diagnostic::{DiagnosticDef, DiagnosticDefArgs},
fmt,
};
pub struct Snippets(Vec<Snippet>); pub struct Snippets(Vec<Snippet>);
@ -290,114 +295,127 @@ impl Snippets {
}) })
} }
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
let variant_arms = variants.iter().map(|variant| { let variant_arms = variants.iter().map(|variant| {
variant.snippets.as_ref().map(|snippets| { let DiagnosticDef { ident, fields, args: def_args } = variant;
let variant_snippets = snippets.0.iter().map(|snippet| { match def_args {
// snippet message DiagnosticDefArgs::Transparent => {
let msg = if let Some(display) = &snippet.message { Some(forward_to_single_field_variant(
let members: HashSet<syn::Member> = variant.fields.iter().enumerate().map(|(i, field)| { ident,
if let Some(ident) = field.ident.as_ref().cloned() { fields,
syn::Member::Named(ident) quote! { snippets() },
} else { ))
syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
}
}).collect();
let mut display = display.clone();
display.expand_shorthand(&members);
let Display { fmt, args, .. } = display;
quote! {
message: std::option::Option::Some(format!(#fmt, #args)),
}
} else {
quote! {
message: std::option::Option::None,
}
};
// Source field
let src_ident = match &snippet.source {
syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
let src_ident = quote! {
// TODO: I don't like this. Think about it more and maybe improve protocol?
source: #src_ident,
};
// Context
let context = match &snippet.snippet {
syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
let context = quote! {
context: #context.clone().into(),
};
// Highlights
let highlights = snippet.highlights.iter().map(|highlight| {
let Highlight { highlight, label } = highlight;
let m = match highlight {
syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
if let Some(Display { fmt, args, ..}) = label {
quote! {
(
std::option::Option::Some(format!(#fmt, #args)),
#m.clone().into()
)
}
} else {
quote! {
(std::option::Option::None, #m.clone().into())
}
}
});
let highlights = quote! {
highlights: std::option::Option::Some(vec![
#(#highlights),*
]),
};
// Generate the snippet itself
quote! {
miette::DiagnosticSnippet {
#msg
#src_ident
#context
#highlights
}
}
});
let variant_name = variant.ident.clone();
let members = variant.fields.iter().enumerate().map(|(i, field)| {
field
.ident
.as_ref()
.cloned()
.unwrap_or_else(|| format_ident!("_{}", i))
});
match &variant.fields {
syn::Fields::Unit => None,
syn::Fields::Named(_) => Some(quote! {
Self::#variant_name { #(#members),* } => std::option::Option::Some(std::boxed::Box::new(vec![
#(#variant_snippets),*
].into_iter())),
}),
syn::Fields::Unnamed(_) => Some(quote! {
Self::#variant_name(#(#members),*) => std::option::Option::Some(Box::new(vec![
#(#variant_snippets),*
].into_iter())),
}),
} }
}) DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { snippets, .. }) => {
}); snippets.as_ref().and_then(|snippets| {
let variant_snippets = snippets.0.iter().map(|snippet| {
// snippet message
let msg = if let Some(display) = &snippet.message {
let members: HashSet<syn::Member> = variant.fields.iter().enumerate().map(|(i, field)| {
if let Some(ident) = field.ident.as_ref().cloned() {
syn::Member::Named(ident)
} else {
syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
}
}).collect();
let mut display = display.clone();
display.expand_shorthand(&members);
let Display { fmt, args, .. } = display;
quote! {
message: std::option::Option::Some(format!(#fmt, #args)),
}
} else {
quote! {
message: std::option::Option::None,
}
};
// Source field
let src_ident = match &snippet.source {
syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
let src_ident = quote! {
// TODO: I don't like this. Think about it more and maybe improve protocol?
source: #src_ident,
};
// Context
let context = match &snippet.snippet {
syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
let context = quote! {
context: #context.clone().into(),
};
// Highlights
let highlights = snippet.highlights.iter().map(|highlight| {
let Highlight { highlight, label } = highlight;
let m = match highlight {
syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
if let Some(Display { fmt, args, ..}) = label {
quote! {
(
std::option::Option::Some(format!(#fmt, #args)),
#m.clone().into()
)
}
} else {
quote! {
(std::option::Option::None, #m.clone().into())
}
}
});
let highlights = quote! {
highlights: std::option::Option::Some(vec![
#(#highlights),*
]),
};
// Generate the snippet itself
quote! {
miette::DiagnosticSnippet {
#msg
#src_ident
#context
#highlights
}
}
});
let variant_name = variant.ident.clone();
let members = variant.fields.iter().enumerate().map(|(i, field)| {
field
.ident
.as_ref()
.cloned()
.unwrap_or_else(|| format_ident!("_{}", i))
});
match &variant.fields {
syn::Fields::Unit => None,
syn::Fields::Named(_) => Some(quote! {
Self::#variant_name { #(#members),* } => std::option::Option::Some(std::boxed::Box::new(vec![
#(#variant_snippets),*
].into_iter())),
}),
syn::Fields::Unnamed(_) => Some(quote! {
Self::#variant_name(#(#members),*) => std::option::Option::Some(Box::new(vec![
#(#variant_snippets),*
].into_iter())),
}),
}
})
}
}
})
.flatten();
Some(quote! { 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> + '_>> {

View File

@ -9,8 +9,11 @@ use syn::{
Fields, Token, Fields, Token,
}; };
use crate::diagnostic::DiagnosticVariant;
use crate::fmt::{self, Display}; use crate::fmt::{self, Display};
use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef, DiagnosticDefArgs},
utils::forward_to_single_field_variant,
};
pub enum Url { pub enum Url {
Display(Display), Display(Display),
@ -64,18 +67,19 @@ impl Parse for Url {
impl Url { impl Url {
pub(crate) fn gen_enum( pub(crate) fn gen_enum(
enum_name: &syn::Ident, enum_name: &syn::Ident,
variants: &[DiagnosticVariant], variants: &[DiagnosticDef],
) -> Option<TokenStream> { ) -> Option<TokenStream> {
let url_pairs = variants let url_pairs = variants.iter().map(|variant| {
.iter() let DiagnosticDef { ident, fields, args: def_args } = variant;
.filter(|v| v.url.is_some()) match def_args {
.map( DiagnosticDefArgs::Transparent => {
|DiagnosticVariant { Some(forward_to_single_field_variant(
ref ident, ident,
ref url, fields,
ref fields, quote! { url() },
.. ))
}| { }
DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { ref url, .. }) => {
let member_idents = fields.iter().enumerate().map(|(i, field)| { let member_idents = fields.iter().enumerate().map(|(i, field)| {
field field
.ident .ident
@ -90,7 +94,8 @@ impl Url {
syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() }) syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
} }
}).collect(); }).collect();
let (fmt, args) = match url.as_ref().expect("MIETTE BUG: we already checked for `Some`") { let (fmt, args) = match url.as_ref()? {
// fall through to `_ => None` below
Url::Display(display) => { Url::Display(display) => {
let mut display = display.clone(); let mut display = display.clone();
display.expand_shorthand(&members); display.expand_shorthand(&members);
@ -108,7 +113,7 @@ impl Url {
(fmt, args) (fmt, args)
} }
}; };
match fields { Some(match fields {
syn::Fields::Named(_) => { syn::Fields::Named(_) => {
quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), } quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
} }
@ -117,10 +122,12 @@ impl Url {
} }
syn::Fields::Unit => syn::Fields::Unit =>
quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }, quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), },
} })
}, }
) }
.collect::<Vec<_>>(); })
.flatten()
.collect::<Vec<_>>();
if url_pairs.is_empty() { if url_pairs.is_empty() {
None None
} else { } else {

View File

@ -1,5 +1,5 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::ToTokens; use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
pub(crate) enum MemberOrString { pub(crate) enum MemberOrString {
@ -32,3 +32,35 @@ impl Parse for MemberOrString {
} }
} }
} }
// bool here is whether to use curly braces
pub fn single_field_name(fields: &syn::Fields) -> Option<(bool, &syn::Field)> {
match fields {
syn::Fields::Named(f) if f.named.len() == 1 => f.named.first().map(|x| (true, x)),
syn::Fields::Unnamed(f) if f.unnamed.len() == 1 => f.unnamed.first().map(|x| (false, x)),
_ => None,
}
}
// Returns a match arm
pub fn forward_to_single_field_variant(
ident: &syn::Ident,
fields: &syn::Fields,
method_call: TokenStream,
) -> TokenStream {
if let Some((curly, single_field)) = single_field_name(fields) {
let field_name = single_field
.ident
.clone()
.unwrap_or_else(|| format_ident!("unnamed"));
if curly {
quote! { Self::#ident { #field_name } => #field_name.#method_call, }
} else {
quote! { Self::#ident(#field_name) => #field_name.#method_call, }
}
} else {
quote! {
_ => compile_error!("miette: used `#[diagnostic(transparent)]` on variant without one single field"),
}
}
}

94
src/compile_test.rs Normal file
View File

@ -0,0 +1,94 @@
//! A hacky but perfectly good method of adding compile_fail doctests. You can't do this in a
//! regular tests/blah.rs file.
/// ```compile_fail
/// use thiserror::Error;
/// use miette_derive::Diagnostic;
/// #[derive(Debug, Diagnostic, Error)]
/// #[error("welp")]
/// #[diagnostic(code(foo::bar::baz))]
/// struct Foo {}
///
/// #[derive(Debug, Diagnostic, Error)]
/// enum Variants {
/// #[error("no")]
/// #[diagnostic(transparent)]
/// One,
/// }
/// ```
///
/// ```compile_fail
/// use thiserror::Error;
/// use miette_derive::Diagnostic;
/// #[derive(Debug, Diagnostic, Error)]
/// #[error("welp")]
/// #[diagnostic(code(foo::bar::baz))]
/// struct Foo {}
///
/// #[derive(Debug, Diagnostic, Error)]
/// enum Variants {
/// #[error("no")]
/// #[diagnostic(transparent)]
/// One {
/// one: Foo,
/// two: u32,
/// },
/// }
/// ```
///
/// ```compile_fail
/// use thiserror::Error;
/// use miette_derive::Diagnostic;
/// #[derive(Debug, Diagnostic, Error)]
/// #[error("welp")]
/// #[diagnostic(code(foo::bar::baz))]
/// struct Foo {}
///
/// #[derive(Debug, Diagnostic, Error)]
/// enum Variants {
/// #[error("no")]
/// #[diagnostic(transparent)]
/// One(Foo, u32),
/// }
/// ```
///
#[allow(dead_code)]
#[doc(hidden)]
struct SingleFieldTests;
/// Directly on a struct with any other arg
///
/// ```compile_fail
/// use thiserror::Error;
/// use miette_derive::Diagnostic;
/// #[derive(Debug, Diagnostic, Error)]
/// #[error("welp")]
/// #[diagnostic(code(foo::bar::baz))]
/// struct Foo {}
/// #[derive(Debug, Diagnostic, Error)]
/// #[error("welp")]
/// #[diagnostic(transparent, code(invalid::combo))]
/// struct Bar(Foo);
/// ```
///
/// With any other arg to diagnostic()
///
/// ```compile_fail
/// use thiserror::Error;
/// use miette_derive::Diagnostic;
/// #[derive(Debug, Diagnostic, Error)]
/// #[error("welp")]
/// #[diagnostic(code(foo::bar::baz))]
/// struct Foo {}
///
/// #[derive(Debug, Diagnostic, Error)]
/// enum Variants {
/// #[error("no")]
/// #[diagnostic(transparent, code(invalid::combo))]
/// One(Foo),
/// }
/// ```
///
#[allow(dead_code)]
#[doc(hidden)]
struct TransparentCombinations;

View File

@ -15,3 +15,6 @@ mod printer;
mod protocol; mod protocol;
mod source_impls; mod source_impls;
mod utils; mod utils;
#[cfg(doctest)]
mod compile_test;

View File

@ -310,3 +310,132 @@ fn url_docsrs() {
Foo {}.url().unwrap().to_string() Foo {}.url().unwrap().to_string()
); );
} }
const SNIPPET_TEXT: &str = "hello from miette";
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic(
code(foo::bar::baz),
url("https://example.com"),
help("help"),
severity(Warning)
)]
struct ForwardsTo {
src: String,
#[snippet(src, message("snippet text"))]
snip: miette::SourceSpan,
#[highlight(snip, label("highlight text"))]
highlight: miette::SourceSpan,
}
impl ForwardsTo {
fn new() -> Self {
ForwardsTo {
src: SNIPPET_TEXT.into(),
snip: SourceSpan::new(0.into(), SNIPPET_TEXT.len().into()),
highlight: SourceSpan::new(11.into(), 6.into()),
}
}
}
fn check_snippets(diag: &impl Diagnostic) {
// check Diagnostic impl forwards all these methods
assert_eq!(diag.code().to_string(), "foo::bar::baz");
assert_eq!(diag.url().unwrap().to_string(), "https://example.com");
assert_eq!(diag.help().unwrap().to_string(), "help");
assert_eq!(diag.severity().unwrap(), miette::Severity::Warning);
type Snip = (Option<String>, usize, usize);
let snips: Vec<(Snip, Vec<Snip>)> = diag
.snippets()
.unwrap()
.map(
|miette::DiagnosticSnippet {
message,
context,
highlights,
..
}| {
(
(message, context.offset(), context.len()),
highlights
.into_iter()
.flatten()
.map(|(msg, span)| (msg, span.offset(), span.len()))
.collect(),
)
},
)
.collect();
assert_eq!(
&snips[..],
&[(
(Some("snippet text".into()), 0, SNIPPET_TEXT.len()),
vec![(Some("highlight text".into()), 11, 6)]
)]
);
}
#[test]
fn test_transparent_enum_unnamed() {
#[derive(Debug, Diagnostic, Error)]
enum Enum {
#[error("enum")]
#[diagnostic(transparent)]
FooVariant(#[from] ForwardsTo),
}
let variant = Enum::FooVariant(ForwardsTo::new());
check_snippets(&variant);
}
#[test]
fn test_transparent_enum_named() {
#[derive(Debug, Diagnostic, Error)]
enum Enum {
#[error("enum")]
#[diagnostic(transparent)]
FooVariant {
#[from]
single_field: ForwardsTo,
},
#[error("foo")]
#[diagnostic(code(foo::bar::bar_variant))]
BarVariant,
}
let variant = Enum::FooVariant {
single_field: ForwardsTo::new(),
};
check_snippets(&variant);
let bar_variant = Enum::BarVariant;
assert_eq!(bar_variant.code().to_string(), "foo::bar::bar_variant");
}
#[test]
fn test_transparent_struct_named() {
#[derive(Debug, Diagnostic, Error)]
#[error(transparent)]
#[diagnostic(transparent)]
struct Struct {
#[from]
single_field: ForwardsTo,
}
// Also check the From impl here
let variant: Struct = ForwardsTo::new().into();
check_snippets(&variant);
}
#[test]
fn test_transparent_struct_unnamed() {
#[derive(Debug, Diagnostic, Error)]
#[error(transparent)]
#[diagnostic(transparent)]
struct Struct(#[from] ForwardsTo);
let variant = Struct(ForwardsTo::new());
check_snippets(&variant);
}