mirror of https://github.com/zkat/miette.git
feat(derive): format string support for help()
Fixes: https://github.com/zkat/miette/issues/7
This commit is contained in:
parent
3ed97050db
commit
8fbad1b1cd
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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 += ∫
|
||||
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))
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use diagnostic::Diagnostic;
|
|||
mod code;
|
||||
mod diagnostic;
|
||||
mod diagnostic_arg;
|
||||
mod fmt;
|
||||
mod help;
|
||||
mod severity;
|
||||
mod snippets;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in New Issue