Repository: dtolnay/thiserror
Pull request #39 (2019-11-10 02:27:43)

Support interpolating paths as if they had a Display impl
Based on https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md.


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index f861a56..2bdebfb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ thiserror-impl = { version = "=1.0.4", path = "impl" }
 
 [dev-dependencies]
 anyhow = "1.0"
+ref-cast = "1.0"
 rustversion = "1.0"
 trybuild = "1.0"
 
diff --git a/impl/src/attr.rs b/impl/src/attr.rs
index 96b8cd8..a361b89 100644
--- a/impl/src/attr.rs
+++ b/impl/src/attr.rs
@@ -20,6 +20,7 @@ pub struct Display<'a> {
     pub fmt: LitStr,
     pub args: TokenStream,
     pub was_shorthand: bool,
+    pub has_bonus_display: bool,
 }
 
 pub fn get(input: &[Attribute]) -> Result<Attrs> {
@@ -74,6 +75,7 @@ fn parse_display(attr: &Attribute) -> Result<Display> {
             fmt: input.parse()?,
             args: parse_token_expr(input, false)?,
             was_shorthand: false,
+            has_bonus_display: false,
         };
         display.expand_shorthand();
         Ok(display)
diff --git a/impl/src/expand.rs b/impl/src/expand.rs
index 5e092cd..3b821e7 100644
--- a/impl/src/expand.rs
+++ b/impl/src/expand.rs
@@ -77,10 +77,19 @@ fn impl_struct(input: Struct) -> TokenStream {
     });
 
     let display_impl = input.attrs.display.as_ref().map(|display| {
+        let use_as_display = if display.has_bonus_display {
+            Some(quote! {
+                #[allow(unused_imports)]
+                use thiserror::private::{DisplayAsDisplay, PathAsDisplay};
+            })
+        } else {
+            None
+        };
         let pat = fields_pat(&input.fields);
         quote! {
             impl #impl_generics std::fmt::Display for #ty #ty_generics #where_clause {
                 fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                    #use_as_display
                     #[allow(unused_variables)]
                     let Self #pat = self;
                     #display
@@ -215,6 +224,20 @@ fn impl_enum(input: Enum) -> TokenStream {
     };
 
     let display_impl = if input.has_display() {
+        let use_as_display = if input.variants.iter().any(|v| {
+            v.attrs
+                .display
+                .as_ref()
+                .expect(valid::CHECKED)
+                .has_bonus_display
+        }) {
+            Some(quote! {
+                #[allow(unused_imports)]
+                use thiserror::private::{DisplayAsDisplay, PathAsDisplay};
+            })
+        } else {
+            None
+        };
         let void_deref = if input.variants.is_empty() {
             Some(quote!(*))
         } else {
@@ -231,6 +254,7 @@ fn impl_enum(input: Enum) -> TokenStream {
         Some(quote! {
             impl #impl_generics std::fmt::Display for #ty #ty_generics #where_clause {
                 fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                    #use_as_display
                     #[allow(unused_variables)]
                     match #void_deref self {
                         #(#arms,)*
diff --git a/impl/src/fmt.rs b/impl/src/fmt.rs
index 1c394d3..2db79e8 100644
--- a/impl/src/fmt.rs
+++ b/impl/src/fmt.rs
@@ -15,6 +15,7 @@ impl Display<'_> {
         let mut read = fmt.as_str();
         let mut out = String::new();
         let mut args = TokenStream::new();
+        let mut has_bonus_display = false;
 
         while let Some(brace) = read.find('{') {
             out += &read[..=brace];
@@ -35,12 +36,17 @@ impl Display<'_> {
             };
             let ident = Ident::new(&var, span);
             args.extend(quote_spanned!(span=> , #ident));
+            if read.starts_with('}') {
+                has_bonus_display = true;
+                args.extend(quote_spanned!(span=> .as_display()));
+            }
         }
 
         out += read;
         self.fmt = LitStr::new(&out, self.fmt.span());
         self.args = args;
         self.was_shorthand = true;
+        self.has_bonus_display = has_bonus_display;
     }
 }
 
@@ -85,6 +91,7 @@ mod tests {
             fmt: LitStr::new(input, Span::call_site()),
             args: TokenStream::new(),
             was_shorthand: false,
+            has_bonus_display: false,
         };
         display.expand_shorthand();
         assert_eq!(fmt, display.fmt.value());
@@ -93,12 +100,12 @@ mod tests {
 
     #[test]
     fn test_expand() {
-        assert("error {var}", "error {}", ", var");
+        assert("error {var}", "error {}", ", var . as_display ( )");
         assert("fn main() {{ }}", "fn main() {{ }}", "");
         assert(
             "{v} {v:?} {0} {0:?}",
             "{} {:?} {} {:?}",
-            ", v , v , _0 , _0",
+            ", v . as_display ( ) , v , _0 . as_display ( ) , _0",
         );
     }
 }
diff --git a/src/display.rs b/src/display.rs
new file mode 100644
index 0000000..0eb0dd9
--- /dev/null
+++ b/src/display.rs
@@ -0,0 +1,28 @@
+use std::fmt::Display;
+use std::path::{self, Path, PathBuf};
+
+pub trait DisplayAsDisplay {
+    fn as_display(&self) -> Self;
+}
+
+impl<T: Display> DisplayAsDisplay for &T {
+    fn as_display(&self) -> Self {
+        self
+    }
+}
+
+pub trait PathAsDisplay {
+    fn as_display(&self) -> path::Display<'_>;
+}
+
+impl PathAsDisplay for Path {
+    fn as_display(&self) -> path::Display<'_> {
+        self.display()
+    }
+}
+
+impl PathAsDisplay for PathBuf {
+    fn as_display(&self) -> path::Display<'_> {
+        self.display()
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 0151518..b623e66 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -136,6 +136,7 @@
 //!   [`anyhow`]: https://github.com/dtolnay/anyhow
 
 mod aserror;
+mod display;
 
 pub use thiserror_impl::*;
 
@@ -143,4 +144,5 @@ pub use thiserror_impl::*;
 #[doc(hidden)]
 pub mod private {
     pub use crate::aserror::AsDynError;
+    pub use crate::display::{DisplayAsDisplay, PathAsDisplay};
 }


Test patch:
diff --git a/tests/test_path.rs b/tests/test_path.rs
new file mode 100644
index 0000000..a34a3d7
--- /dev/null
+++ b/tests/test_path.rs
@@ -0,0 +1,37 @@
+use ref_cast::RefCast;
+use std::fmt::Display;
+use std::path::{Path, PathBuf};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+#[error("failed to read '{file}'")]
+struct StructPathBuf {
+    file: PathBuf,
+}
+
+#[derive(Error, Debug, RefCast)]
+#[repr(C)]
+#[error("failed to read '{file}'")]
+struct StructPath {
+    file: Path,
+}
+
+#[derive(Error, Debug)]
+enum EnumPathBuf {
+    #[error("failed to read '{0}'")]
+    Read(PathBuf),
+}
+
+fn assert<T: Display>(expected: &str, value: T) {
+    assert_eq!(expected, value.to_string());
+}
+
+#[test]
+fn test_display() {
+    let path = Path::new("/thiserror");
+    let file = path.to_owned();
+    assert("failed to read '/thiserror'", StructPathBuf { file });
+    let file = path.to_owned();
+    assert("failed to read '/thiserror'", EnumPathBuf::Read(file));
+    assert("failed to read '/thiserror'", StructPath::ref_cast(path));
+}