Repository: shepmaster/snafu (rust)

Issue:
Use doc comments as default Display implementations
A lot of my errors look like this with snafu:
```rust
/// The Error type for splits files that couldn't be parsed by the FaceSplit
/// Parser.
#[derive(Debug, snafu::Snafu)]
pub enum ParseError {
    /// Expected the title, but didn't find it.
    #[snafu(display("Expected the title, but didn't find it."))]
    ExpectedTitle,
    /// Expected the goal, but didn't find it.
    #[snafu(display("Expected the goal, but didn't find it."))]
    ExpectedGoal,
    /// Expected the attempt count, but didn't find it.
    #[snafu(display("Expected the attempt count, but didn't find it."))]
    ExpectedAttemptCount,
    /// Expected the name of the segment, but didn't find it.
    #[snafu(display("Expected the name of the segment, but didn't find it."))]
    ExpectedSegmentName,
    /// Expected the split time, but didn't find it.
    #[snafu(display("Expected the split time, but didn't find it."))]
    ExpectedSplitTime,
    /// Expected the best segment time, but didn't find it.
    #[snafu(display("Expected the best segment time, but didn't find it."))]
    ExpectedBestSegmentTime,
}
```
As you can see all the display attributes are basically verbatim copied into the doc comments. It would be cool if Snafu used the doc comment as the display impl for a variant if there is no explicit snafu(display(...)) attribute.

Pull request:
Implement Doc Comments as Default Display Implementations
If there is no #[snafu(display(...))] attribute on a variant, but there is a doc comment, the doc comment is now chosen as the implementation of Display for the variant.

Resolves #106

Patch:
diff --git a/snafu-derive/src/lib.rs b/snafu-derive/src/lib.rs
index 46fe897..c9bb3d5 100644
--- a/snafu-derive/src/lib.rs
+++ b/snafu-derive/src/lib.rs
@@ -44,6 +44,7 @@ struct VariantInfo {
     backtrace_delegate: Option<Field>,
     user_fields: Vec<Field>,
     display_format: Option<UserInput>,
+    doc_comment: String,
     visibility: Option<UserInput>,
 }
 
@@ -157,6 +158,8 @@ fn parse_snafu_enum(
 
             let mut display_format = None;
             let mut visibility = None;
+            let mut doc_comment = String::new();
+            let mut reached_end_of_doc_comment = false;
 
             for attr in attributes_from_syn(variant.attrs)? {
                 match attr {
@@ -164,6 +167,22 @@ fn parse_snafu_enum(
                     SnafuAttribute::Visibility(v) => visibility = Some(v),
                     SnafuAttribute::Source(..) => { /* Report this isn't valid here? */ }
                     SnafuAttribute::Backtrace(..) => { /* Report this isn't valid here? */ }
+                    SnafuAttribute::DocComment(doc_comment_line) => {
+                        // We join all the doc comment attributes with a space,
+                        // but end once the summary of the doc comment is
+                        // complete, which is indicated by an empty line.
+                        if !reached_end_of_doc_comment {
+                            let trimmed = doc_comment_line.trim();
+                            if trimmed.is_empty() {
+                                reached_end_of_doc_comment = true;
+                            } else {
+                                if !doc_comment.is_empty() {
+                                    doc_comment.push_str(" ");
+                                }
+                                doc_comment.push_str(trimmed);
+                            }
+                        }
+                    }
                 }
             }
 
@@ -214,6 +233,7 @@ fn parse_snafu_enum(
                         },
                         SnafuAttribute::Visibility(_) => { /* Report this isn't valid here? */ }
                         SnafuAttribute::Display(_) => { /* Report this isn't valid here? */ }
+                        SnafuAttribute::DocComment(_) => { /* Just a regular doc comment. */ }
                     }
                 }
 
@@ -259,6 +279,7 @@ fn parse_snafu_enum(
                 backtrace_delegate,
                 user_fields,
                 display_format,
+                doc_comment,
                 visibility,
             })
         })
@@ -297,6 +318,7 @@ fn parse_snafu_struct(
                 }
             }
             SnafuAttribute::Backtrace(..) => { /* Report this isn't valid here? */ }
+            SnafuAttribute::DocComment(_) => { /* Just a regular doc comment. */ }
         }
     }
 
@@ -498,6 +520,7 @@ enum SnafuAttribute {
     Visibility(UserInput),
     Source(Vec<Source>),
     Backtrace(Backtrace),
+    DocComment(String),
 }
 
 impl SnafuAttribute {
@@ -569,19 +592,35 @@ impl syn::parse::Parse for SnafuAttributeBody {
     }
 }
 
+struct DocComment(SnafuAttribute);
+
+impl syn::parse::Parse for DocComment {
+    fn parse(input: syn::parse::ParseStream) -> SynResult<Self> {
+        use syn::token::Eq;
+        use syn::LitStr;
+
+        let _: Eq = input.parse()?;
+        let doc: LitStr = input.parse()?;
+
+        Ok(DocComment(SnafuAttribute::DocComment(doc.value())))
+    }
+}
+
 fn attributes_from_syn(attrs: Vec<syn::Attribute>) -> MultiSynResult<Vec<SnafuAttribute>> {
     use syn::parse2;
 
     let mut ours = Vec::new();
     let mut errs = Vec::new();
 
-    let parsed_attrs = attrs
-        .into_iter()
-        .filter(|attr| attr.path.is_ident("snafu"))
-        .map(|attr| {
-            let body: SnafuAttributeBody = parse2(attr.tts)?;
-            Ok(body.0)
-        });
+    let parsed_attrs = attrs.into_iter().flat_map(|attr| {
+        if attr.path.is_ident("snafu") {
+            Some(parse2::<SnafuAttributeBody>(attr.tts).map(|body| body.0))
+        } else if attr.path.is_ident("doc") {
+            Some(parse2::<DocComment>(attr.tts).map(|comment| vec![comment.0]))
+        } else {
+            None
+        }
+    });
 
     for attr in parsed_attrs {
         match attr {
@@ -868,11 +907,15 @@ impl<'a> DisplayImpl<'a> {
                     ref source_field,
                     ref backtrace_field,
                     ref display_format,
+                    ref doc_comment,
                     ..
                 } = *variant;
 
                 let format = match (display_format, source_field) {
                     (&Some(ref v), _) => quote! { #v },
+                    (&None, _) if !doc_comment.is_empty() => {
+                        quote! { #doc_comment }
+                    }
                     (&None, &Some(ref f)) => {
                         let field_name = &f.name;
                         quote! { concat!(stringify!(#variant_name), ": {}"), #field_name }


Test patch:
diff --git a/tests/doc_comment.rs b/tests/doc_comment.rs
new file mode 100644
index 0000000..38a9623
--- /dev/null
+++ b/tests/doc_comment.rs
@@ -0,0 +1,40 @@
+extern crate snafu;
+
+use snafu::Snafu;
+
+#[derive(Debug, Snafu)]
+enum Error {
+    /// No user available.
+    /// You may need to specify one.
+    ///
+    /// Here is a more detailed description.
+    MissingUser,
+    /// This is just a doc comment.
+    #[snafu(display("This is {}", stronger))]
+    Stronger { stronger: &'static str },
+}
+
+#[test]
+fn implements_error() {
+    fn check<T: std::error::Error>() {}
+    check::<Error>();
+}
+
+#[test]
+fn uses_doc_comment() {
+    assert_eq!(
+        Error::MissingUser.to_string(),
+        "No user available. You may need to specify one.",
+    );
+}
+
+#[test]
+fn display_is_stronger_than_doc_comment() {
+    assert_eq!(
+        Error::Stronger {
+            stronger: "always stronger!"
+        }
+        .to_string(),
+        "This is always stronger!",
+    );
+}