diff --git a/README.md b/README.md index 806800a..f886891 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index 8e9a47a..6c14467 100644 --- a/miette-derive/src/diagnostic.rs +++ b/miette-derive/src/diagnostic.rs @@ -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()); diff --git a/miette-derive/src/fmt.rs b/miette-derive/src/fmt.rs new file mode 100644 index 0000000..d076e76 --- /dev/null +++ b/miette-derive/src/fmt.rs @@ -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) { + 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::() { + 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> { + let mut named_args = Set::new(); + + while !input.is_empty() { + if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) { + input.parse::()?; + let ident = input.call(Ident::parse_any)?; + input.parse::()?; + named_args.insert(ident); + } else { + input.parse::()?; + } + } + + 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 { + let mut tokens = Vec::new(); + while !input.is_empty() { + if begin_expr && input.peek(Token![.]) { + if input.peek2(Ident) { + input.parse::()?; + begin_expr = false; + continue; + } + if input.peek2(syn::LitInt) { + input.parse::()?; + 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)) +} diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs index 10dde3e..3d7ecf9 100644 --- a/miette-derive/src/help.rs +++ b/miette-derive/src/help.rs @@ -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, + 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::::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::()?; + fmt::parse_token_expr(&content, false)? + }; + let display = Display { + fmt, + args, + has_bonus_display: false, + }; + Ok(Help { display }) } else { input.parse::()?; Ok(Help { - fmt: input.parse::()?.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 { 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 = 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> { + #[allow(unused_variables, deprecated)] match self { #(#help_pairs)* _ => None, @@ -106,12 +110,48 @@ impl Help { } } - pub(crate) fn gen_struct(&self) -> Option { - let fmt = &self.fmt; - let args = &self.args; + pub(crate) fn gen_struct(&self, fields: &Fields) -> Option { + let mut display = self.display.clone(); + let members: HashSet = 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::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))) } }) } diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index 72f2f64..709ed57 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -6,6 +6,7 @@ use diagnostic::Diagnostic; mod code; mod diagnostic; mod diagnostic_arg; +mod fmt; mod help; mod severity; mod snippets; diff --git a/tests/derive.rs b/tests/derive.rs index 95a37e6..af3b2f3 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -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]