feat(derive): format string support for help()

Fixes: https://github.com/zkat/miette/issues/7
This commit is contained in:
Kat Marchán 2021-08-18 19:55:25 -07:00
parent 3ed97050db
commit 8fbad1b1cd
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
6 changed files with 371 additions and 58 deletions

View File

@ -127,7 +127,8 @@ Error: Error[oops::my::bad]: oops it broke!
## License ## License
`miette` is released to the Rust community under the [MIT license](./LICENSE). `miette` is released to the Rust community under the [Apache license 2.0](./LICENSE).
It also includes some code taken from [`eyre`](https://github.com/yaahc/eyre), It also includes some code taken from [`eyre`](https://github.com/yaahc/eyre),
also [under the MIT license](https://github.com/yaahc/eyre#license). and some from [`thiserror`](https://github.com/dtolnay/thiserror), also under
the Apache License.

View File

@ -10,6 +10,7 @@ use crate::snippets::Snippets;
pub enum Diagnostic { pub enum Diagnostic {
Struct { Struct {
fields: syn::Fields,
ident: syn::Ident, ident: syn::Ident,
generics: syn::Generics, generics: syn::Generics,
code: Code, code: Code,
@ -59,6 +60,7 @@ impl Diagnostic {
let snippets = Snippets::from_fields(&data_struct.fields)?; let snippets = Snippets::from_fields(&data_struct.fields)?;
let ident = input.ident.clone(); let ident = input.ident.clone();
Diagnostic::Struct { Diagnostic::Struct {
fields: data_struct.fields,
ident: input.ident, ident: input.ident,
generics: input.generics, generics: input.generics,
code: code.ok_or_else(|| { code: code.ok_or_else(|| {
@ -138,6 +140,7 @@ impl Diagnostic {
pub fn gen(&self) -> TokenStream { pub fn gen(&self) -> TokenStream {
match self { match self {
Self::Struct { Self::Struct {
fields,
ident, ident,
generics, generics,
code, code,
@ -147,7 +150,7 @@ impl Diagnostic {
} => { } => {
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(); let code_body = code.gen_struct();
let help_body = help.as_ref().and_then(|x| x.gen_struct()); let help_body = help.as_ref().and_then(|x| x.gen_struct(fields));
let sev_body = severity.as_ref().and_then(|x| x.gen_struct()); let sev_body = severity.as_ref().and_then(|x| x.gen_struct());
let snip_body = snippets.as_ref().and_then(|x| x.gen_struct()); let snip_body = snippets.as_ref().and_then(|x| x.gen_struct());

235
miette-derive/src/fmt.rs Normal file
View File

@ -0,0 +1,235 @@
// NOTE: Most code in this file is taken straight from `thiserror`.
use std::collections::HashSet as Set;
use std::iter::FromIterator;
use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::ext::IdentExt;
use syn::parse::{ParseStream, Parser};
use syn::{Ident, Index, LitStr, Member, Result, Token, braced, bracketed, parenthesized};
#[derive(Clone)]
pub struct Display {
pub fmt: LitStr,
pub args: TokenStream,
pub has_bonus_display: bool,
}
impl ToTokens for Display {
fn to_tokens(&self, tokens: &mut TokenStream) {
let fmt = &self.fmt;
let args = &self.args;
tokens.extend(quote! {
write!(__formatter, #fmt #args)
});
}
}
impl Display {
// Transform `"error {var}"` to `"error {}", var`.
pub fn expand_shorthand(&mut self, members: &Set<Member>) {
let raw_args = self.args.clone();
let mut named_args = explicit_named_args.parse2(raw_args).unwrap();
let span = self.fmt.span();
let fmt = self.fmt.value();
let mut read = fmt.as_str();
let mut out = String::new();
let mut args = self.args.clone();
let mut has_bonus_display = false;
let mut has_trailing_comma = false;
if let Some(TokenTree::Punct(punct)) = args.clone().into_iter().last() {
if punct.as_char() == ',' {
has_trailing_comma = true;
}
}
while let Some(brace) = read.find('{') {
out += &read[..brace + 1];
read = &read[brace + 1..];
if read.starts_with('{') {
out.push('{');
read = &read[1..];
continue;
}
let next = match read.chars().next() {
Some(next) => next,
None => return,
};
let member = match next {
'0'..='9' => {
let int = take_int(&mut read);
let member = match int.parse::<u32>() {
Ok(index) => Member::Unnamed(Index { index, span }),
Err(_) => return,
};
if !members.contains(&member) {
out += &int;
continue;
}
member
}
'a'..='z' | 'A'..='Z' | '_' => {
let mut ident = take_ident(&mut read);
ident.set_span(span);
Member::Named(ident)
}
_ => continue,
};
let local = match &member {
Member::Unnamed(index) => format_ident!("_{}", index),
Member::Named(ident) => ident.clone(),
};
let mut formatvar = local.clone();
if formatvar.to_string().starts_with("r#") {
formatvar = format_ident!("r_{}", formatvar);
}
if formatvar.to_string().starts_with('_') {
// Work around leading underscore being rejected by 1.40 and
// older compilers. https://github.com/rust-lang/rust/pull/66847
formatvar = format_ident!("field_{}", formatvar);
}
out += &formatvar.to_string();
if !named_args.insert(formatvar.clone()) {
// Already specified in the format argument list.
continue;
}
if !has_trailing_comma {
args.extend(quote_spanned!(span=> ,));
}
args.extend(quote_spanned!(span=> #formatvar = #local));
if read.starts_with('}') && members.contains(&member) {
has_bonus_display = true;
// args.extend(quote_spanned!(span=> .as_display()));
}
has_trailing_comma = false;
}
out += read;
self.fmt = LitStr::new(&out, self.fmt.span());
self.args = args;
self.has_bonus_display = has_bonus_display;
}
}
fn explicit_named_args(input: ParseStream) -> Result<Set<Ident>> {
let mut named_args = Set::new();
while !input.is_empty() {
if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) {
input.parse::<Token![,]>()?;
let ident = input.call(Ident::parse_any)?;
input.parse::<Token![=]>()?;
named_args.insert(ident);
} else {
input.parse::<TokenTree>()?;
}
}
Ok(named_args)
}
fn take_int(read: &mut &str) -> String {
let mut int = String::new();
for (i, ch) in read.char_indices() {
match ch {
'0'..='9' => int.push(ch),
_ => {
*read = &read[i..];
break;
}
}
}
int
}
fn take_ident(read: &mut &str) -> Ident {
let mut ident = String::new();
let raw = read.starts_with("r#");
if raw {
ident.push_str("r#");
*read = &read[2..];
}
for (i, ch) in read.char_indices() {
match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch),
_ => {
*read = &read[i..];
break;
}
}
}
Ident::parse_any.parse_str(&ident).unwrap()
}
pub fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result<TokenStream> {
let mut tokens = Vec::new();
while !input.is_empty() {
if begin_expr && input.peek(Token![.]) {
if input.peek2(Ident) {
input.parse::<Token![.]>()?;
begin_expr = false;
continue;
}
if input.peek2(syn::LitInt) {
input.parse::<Token![.]>()?;
let int: Index = input.parse()?;
let ident = format_ident!("_{}", int.index, span = int.span);
tokens.push(TokenTree::Ident(ident));
begin_expr = false;
continue;
}
}
begin_expr = input.peek(Token![break])
|| input.peek(Token![continue])
|| input.peek(Token![if])
|| input.peek(Token![in])
|| input.peek(Token![match])
|| input.peek(Token![mut])
|| input.peek(Token![return])
|| input.peek(Token![while])
|| input.peek(Token![+])
|| input.peek(Token![&])
|| input.peek(Token![!])
|| input.peek(Token![^])
|| input.peek(Token![,])
|| input.peek(Token![/])
|| input.peek(Token![=])
|| input.peek(Token![>])
|| input.peek(Token![<])
|| input.peek(Token![|])
|| input.peek(Token![%])
|| input.peek(Token![;])
|| input.peek(Token![*])
|| input.peek(Token![-]);
let token: TokenTree = if input.peek(syn::token::Paren) {
let content;
let delimiter = parenthesized!(content in input);
let nested = parse_token_expr(&content, true)?;
let mut group = Group::new(Delimiter::Parenthesis, nested);
group.set_span(delimiter.span);
TokenTree::Group(group)
} else if input.peek(syn::token::Brace) {
let content;
let delimiter = braced!(content in input);
let nested = parse_token_expr(&content, true)?;
let mut group = Group::new(Delimiter::Brace, nested);
group.set_span(delimiter.span);
TokenTree::Group(group)
} else if input.peek(syn::token::Bracket) {
let content;
let delimiter = bracketed!(content in input);
let nested = parse_token_expr(&content, true)?;
let mut group = Group::new(Delimiter::Bracket, nested);
group.set_span(delimiter.span);
TokenTree::Group(group)
} else {
input.parse()?
};
tokens.push(token);
}
Ok(TokenStream::from_iter(tokens))
}

View File

@ -1,16 +1,19 @@
use std::collections::HashSet;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::{format_ident, quote};
use syn::{ use syn::{
parenthesized, parenthesized,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
Token, spanned::Spanned,
Fields, Token,
}; };
use crate::diagnostic::DiagnosticVariant; use crate::diagnostic::DiagnosticVariant;
use crate::fmt::{self, Display};
pub struct Help { pub struct Help {
pub fmt: String, pub display: Display,
pub args: Vec<syn::Expr>,
} }
impl Parse for Help { impl Parse for Help {
@ -21,42 +24,27 @@ impl Parse for Help {
if la.peek(syn::token::Paren) { if la.peek(syn::token::Paren) {
let content; let content;
parenthesized!(content in input); parenthesized!(content in input);
let mut fmt = None; let fmt = content.parse()?;
let mut args = Vec::new(); let args = if content.is_empty() {
let punc = syn::punctuated::Punctuated::<syn::Expr, Token![,]>::parse_terminated( TokenStream::new()
&content,
)?;
for (i, arg) in punc.into_iter().enumerate() {
if i == 0 {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(str),
..
}) = arg
{
fmt = Some(str.value());
}
} else {
args.push(arg);
}
}
if let Some(fmt) = fmt {
Ok(Help { fmt, args })
} else if !args.is_empty() {
Err(syn::Error::new(
ident.span(),
"The first arg to help() must be a literal format string.",
))
} else { } else {
Err(syn::Error::new( content.parse::<Token![,]>()?;
ident.span(), fmt::parse_token_expr(&content, false)?
"help() format string is required", };
)) let display = Display {
} fmt,
args,
has_bonus_display: false,
};
Ok(Help { display })
} else { } else {
input.parse::<Token![=]>()?; input.parse::<Token![=]>()?;
Ok(Help { Ok(Help {
fmt: input.parse::<syn::LitStr>()?.value(), display: Display {
args: Vec::new(), fmt: input.parse()?,
args: TokenStream::new(),
has_bonus_display: false,
},
}) })
} }
} else { } else {
@ -64,6 +52,7 @@ impl Parse for Help {
} }
} }
} }
impl Help { impl Help {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
let help_pairs = variants let help_pairs = variants
@ -76,18 +65,32 @@ impl Help {
ref fields, ref fields,
.. ..
}| { }| {
let help = &help.as_ref().unwrap(); let mut display = help.as_ref().expect("already checked for Some").display.clone();
let fmt = &help.fmt; let member_idents = fields.iter().enumerate().map(|(i, field)| {
let args = &help.args; 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;
match fields { match fields {
syn::Fields::Named(_) => { syn::Fields::Named(_) => {
quote! { Self::#ident{..} => 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))), }
} }
syn::Fields::Unnamed(_) => { syn::Fields::Unnamed(_) => {
quote! { Self::#ident(..) => 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))), }
} }
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))), },
} }
}, },
) )
@ -97,6 +100,7 @@ impl Help {
} else { } else {
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)]
match self { match self {
#(#help_pairs)* #(#help_pairs)*
_ => None, _ => None,
@ -106,12 +110,48 @@ impl Help {
} }
} }
pub(crate) fn gen_struct(&self) -> Option<TokenStream> { pub(crate) fn gen_struct(&self, fields: &Fields) -> Option<TokenStream> {
let fmt = &self.fmt; let mut display = self.display.clone();
let args = &self.args; 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! {},
};
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>> {
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))) #[allow(unused_variables, deprecated)]
#fields_pat
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args)))
} }
}) })
} }

View File

@ -6,6 +6,7 @@ use diagnostic::Diagnostic;
mod code; mod code;
mod diagnostic; mod diagnostic;
mod diagnostic_arg; mod diagnostic_arg;
mod fmt;
mod help; mod help;
mod severity; mod severity;
mod snippets; mod snippets;

View File

@ -136,25 +136,58 @@ fn list_help() {
fn fmt_help() { fn fmt_help() {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
#[error("welp")] #[error("welp")]
#[diagnostic( #[diagnostic(code(foo::bar::baz), help("{} x {0} x {:?}", 1, "2"))]
code(foo::bar::baz),
help("{} {}", 1, self.0),
)]
struct FooStruct(String); struct FooStruct(String);
assert_eq!( assert_eq!(
"1 hello".to_string(), "1 x hello x \"2\"".to_string(),
FooStruct("hello".into()).help().unwrap().to_string() FooStruct("hello".into()).help().unwrap().to_string()
); );
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
#[error("welp")] #[error("welp")]
enum FooEnum { #[diagnostic(code(foo::bar::baz), help("{} x {my_field} x {:?}", 1, "2"))]
#[diagnostic(code(foo::x), help("{} {}", 1, "bar"))] struct BarStruct {
X, my_field: String,
} }
assert_eq!("1 bar".to_string(), FooEnum::X.help().unwrap().to_string()); assert_eq!(
"1 x hello x \"2\"".to_string(),
BarStruct {
my_field: "hello".into()
}
.help()
.unwrap()
.to_string()
);
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
enum FooEnum {
#[diagnostic(code(foo::x), help("{} x {0} x {:?}", 1, "2"))]
X(String),
#[diagnostic(code(foo::x), help("{} x {len} x {:?}", 1, "2"))]
Y { len: usize },
#[diagnostic(code(foo::x), help("{} x {self:?} x {:?}", 1, "2"))]
Z
}
assert_eq!(
"1 x bar x \"2\"".to_string(),
FooEnum::X("bar".into()).help().unwrap().to_string()
);
assert_eq!(
"1 x 10 x \"2\"".to_string(),
FooEnum::Y { len: 10 }.help().unwrap().to_string()
);
assert_eq!(
"1 x Z x \"2\"".to_string(),
FooEnum::Z.help().unwrap().to_string()
);
} }
#[test] #[test]