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
`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),
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 {
Struct {
fields: syn::Fields,
ident: syn::Ident,
generics: syn::Generics,
code: Code,
@ -59,6 +60,7 @@ impl Diagnostic {
let snippets = Snippets::from_fields(&data_struct.fields)?;
let ident = input.ident.clone();
Diagnostic::Struct {
fields: data_struct.fields,
ident: input.ident,
generics: input.generics,
code: code.ok_or_else(|| {
@ -138,6 +140,7 @@ impl Diagnostic {
pub fn gen(&self) -> TokenStream {
match self {
Self::Struct {
fields,
ident,
generics,
code,
@ -147,7 +150,7 @@ impl Diagnostic {
} => {
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
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 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 quote::quote;
use quote::{format_ident, quote};
use syn::{
parenthesized,
parse::{Parse, ParseStream},
Token,
spanned::Spanned,
Fields, Token,
};
use crate::diagnostic::DiagnosticVariant;
use crate::fmt::{self, Display};
pub struct Help {
pub fmt: String,
pub args: Vec<syn::Expr>,
pub display: Display,
}
impl Parse for Help {
@ -21,42 +24,27 @@ impl Parse for Help {
if la.peek(syn::token::Paren) {
let content;
parenthesized!(content in input);
let mut fmt = None;
let mut args = Vec::new();
let punc = syn::punctuated::Punctuated::<syn::Expr, Token![,]>::parse_terminated(
&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.",
))
let fmt = content.parse()?;
let args = if content.is_empty() {
TokenStream::new()
} else {
Err(syn::Error::new(
ident.span(),
"help() format string is required",
))
}
content.parse::<Token![,]>()?;
fmt::parse_token_expr(&content, false)?
};
let display = Display {
fmt,
args,
has_bonus_display: false,
};
Ok(Help { display })
} else {
input.parse::<Token![=]>()?;
Ok(Help {
fmt: input.parse::<syn::LitStr>()?.value(),
args: Vec::new(),
display: Display {
fmt: input.parse()?,
args: TokenStream::new(),
has_bonus_display: false,
},
})
}
} else {
@ -64,6 +52,7 @@ impl Parse for Help {
}
}
}
impl Help {
pub(crate) fn gen_enum(variants: &[DiagnosticVariant]) -> Option<TokenStream> {
let help_pairs = variants
@ -76,18 +65,32 @@ impl Help {
ref fields,
..
}| {
let help = &help.as_ref().unwrap();
let fmt = &help.fmt;
let args = &help.args;
let mut display = help.as_ref().expect("already checked for Some").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;
match fields {
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(_) => {
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 =>
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 {
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,
@ -106,12 +110,48 @@ impl Help {
}
}
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
let fmt = &self.fmt;
let args = &self.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! {},
};
Some(quote! {
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 diagnostic;
mod diagnostic_arg;
mod fmt;
mod help;
mod severity;
mod snippets;

View File

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