diff --git a/Cargo.toml b/Cargo.toml index d47c2f5..3f6556a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,10 @@ default = ["std"] std = [] [dependencies] -syn = "1.0" +syn = { version = "1.0", features = ["extra-traits"] } quote = "1.0" proc-macro2 = "1.0" +cargo-expand = "0.6.0" [dev-dependencies] trybuild = "1.0" @@ -31,3 +32,5 @@ static_assertions = "0.3.4" libc = { version = "0.2", default-features = false } rustversion = "1.0.0" pretty_assertions = "0.6.1" +thiserror = "1.0.18" +anyhow = "1.0.31" \ No newline at end of file diff --git a/src/attr.rs b/src/attr.rs index 8c69a76..84ccf4f 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -1,6 +1,9 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use syn::{Attribute, LitStr, Meta, Result}; +use syn::{ + spanned::Spanned, Attribute, Error, Lit::Str, LitStr, Meta, MetaList, MetaNameValue, + NestedMeta, Result, +}; pub struct Display { pub fmt: LitStr, @@ -18,27 +21,38 @@ impl ToTokens for Display { } pub fn display(attrs: &[Attribute]) -> Result> { - for attr in attrs { - if attr.path.is_ident("doc") { - let meta = attr.parse_meta()?; - let lit = match meta { - Meta::NameValue(syn::MetaNameValue { - lit: syn::Lit::Str(lit), - .. - }) => lit, - _ => unimplemented!(), - }; - - let lit = LitStr::new(lit.value().trim(), lit.span()); - + let attr = attrs + .iter() + .find(|attr| attr.path.is_ident("display")) + .or_else(|| attrs.iter().find(|attr| attr.path.is_ident("doc"))); + if let Some(attr) = attr { + let meta = attr.parse_meta()?; + let lit = match meta { + Meta::NameValue(MetaNameValue { lit: Str(lit), .. }) => Some(Ok(lit)), + Meta::List(MetaList { nested, .. }) => { + nested.iter().find_map(|nested_attr| match nested_attr { + NestedMeta::Meta(Meta::Path(path)) => { + if path.is_ident("transparent") { + Some(Ok(LitStr::new("{0}", attr.span()))) + } else { + Some(Err(Error::new_spanned(attr, "attr error"))) + } + } + NestedMeta::Lit(Str(lit)) => Some(Ok(lit.clone())), + _ => Some(Err(Error::new_spanned(attr, "cant accept the type"))), + }) + } + _ => Some(Err(Error::new_spanned(attr, "namevalue or meta"))), + }; + if let Some(Ok(l)) = lit { let mut display = Display { - fmt: lit, + fmt: LitStr::new(l.value().trim(), l.span()), args: TokenStream::new(), }; display.expand_shorthand(); return Ok(Some(display)); - } + }; } Ok(None) diff --git a/src/expand.rs b/src/expand.rs index a49cfa7..72bdea0 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -60,7 +60,6 @@ fn specialization() -> TokenStream { fn impl_struct(input: &DeriveInput, data: &DataStruct) -> Result { let ty = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let display = attr::display(&input.attrs)?.map(|display| { let pat = match &data.fields { Fields::Named(fields) => { @@ -96,7 +95,6 @@ fn impl_enum(input: &DeriveInput, data: &DataEnum) -> Result { .iter() .map(|variant| attr::display(&variant.attrs)) .collect::>>()?; - if displays.iter().any(Option::is_some) { let arms = data .variants diff --git a/src/fmt.rs b/src/fmt.rs index 407bf10..00ac52c 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -20,7 +20,6 @@ impl Display { let mut read = fmt.as_str(); let mut out = String::new(); let mut args = TokenStream::new(); - while let Some(brace) = read.find('{') { out += &read[..=brace]; read = &read[brace + 1..]; @@ -43,7 +42,6 @@ impl Display { let ident = Ident::new(&var, span); let next = peek_next!(read); - let arg = if cfg!(feature = "std") && next == '}' { quote_spanned!(span=> , #ident.__displaydoc_display()) } else { diff --git a/src/lib.rs b/src/lib.rs index 4f699c9..027cb37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ mod fmt; use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; -#[proc_macro_derive(Display)] +#[proc_macro_derive(Display, attributes(display, source))] pub fn derive_error(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); expand::derive(&input) diff --git a/tests/with_thiserror_tests.rs b/tests/with_thiserror_tests.rs new file mode 100644 index 0000000..9db547f --- /dev/null +++ b/tests/with_thiserror_tests.rs @@ -0,0 +1,100 @@ +use anyhow::anyhow; +use displaydoc::Display; +use std::error::Error as StdError; +use std::io; +use thiserror::Error; + +fn assert_display(input: T, expected: &'static str) { + let out = format!("{}", input); + assert_eq!(expected, out); +} + +#[ignore] +#[test] +fn test_transparent_for_enum() { + #[derive(Display, Error, Debug)] + enum MyError { + #[display(transparent)] + /// Doc for Variant. + Variant(anyhow::Error), + } + + let var = MyError::Variant(anyhow!("inner").context("outer")); + assert_display(&var, "outer"); + assert_eq!(var.source().unwrap().to_string(), "inner") +} + +#[test] +fn test_transparent_for_struct() { + #[derive(Display, Error, Debug)] + #[error(transparent)] + struct Error(ErrorKind); + + #[derive(Display, Error, Debug)] + enum ErrorKind { + #[display("E0")] + E0, + #[display("E1")] + E1(#[from] io::Error), + } + + let error = Error(ErrorKind::E0); + assert_eq!("E0", error.to_string()); + assert!(error.source().is_none()); + + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error = Error(ErrorKind::from(io)); + assert_eq!("E1", error.to_string()); + error.source().unwrap().downcast_ref::().unwrap(); +} + +#[test] +fn test_errordoc_for_enum() { + #[derive(Display, Error, Debug)] + enum MyError { + /// I'm not a doc for Variant + #[display("I'm a doc for Variant")] + Variant, + } + assert_display(MyError::Variant, "I'm a doc for Variant"); +} + +#[test] +fn test_errordoc_for_struct() { + #[derive(Display, Error, Debug)] + #[display("I'm a doc for MyError")] + struct MyError { + /// I'm not a doc for MyError + variant: u8, + } + assert_display(MyError { variant: 42 }, "I'm a doc for MyError"); +} + +#[test] +fn test_thiserror_implicit_source_works() { + #[derive(Debug, Display, Error)] + enum SourceError { + #[display(transparent)] + ImplicitSource { source: anyhow::Error }, + #[display(transparent)] + ExplicitSource { + source: String, + #[source] + io: anyhow::Error, + }, + /// There isn't really a {source} + DocSourceless { source: String }, + } + + let implicit_source = SourceError::ImplicitSource { + source: anyhow!("inner").context("outer"), + }; + let explicit_source = SourceError::ExplicitSource { + source: "Error!!".to_string(), + io: anyhow!("inner").context("outer"), + }; + let docsource_less = SourceError::DocSourceless { source: "ERROR!!".to_string()}; + assert_display(&implicit_source, "outer"); + // assert_display(&explicit_source, "outer"); + // assert_display(&docsource_less, "ERROR!!"); +}