use std::ops::Deref;
use syn::{Attribute, Expr, Lit, Meta};
const DOC_ATTRIBUTE_TYPE: &str = "doc";
#[derive(Debug)]
pub(crate) struct CommentAttributes(pub(crate) Vec<String>);
impl CommentAttributes {
pub(crate) fn from_attributes(attributes: &[Attribute]) -> Self {
let mut docs = attributes
.iter()
.filter_map(|attr| {
if !matches!(attr.path().get_ident(), Some(ident) if ident == DOC_ATTRIBUTE_TYPE) {
return None;
}
if let Meta::NameValue(name_value) = &attr.meta
&& let Expr::Lit(ref doc_comment) = name_value.value
&& let Lit::Str(ref doc) = doc_comment.lit
{
let mut doc = doc.value();
doc.truncate(doc.trim_end().len());
return Some(doc);
}
None
})
.collect::<Vec<_>>();
let min_indent = docs
.iter()
.filter(|s| !s.is_empty())
.map(|s| s.len() - s.trim_start_matches(' ').len())
.min()
.unwrap_or(0);
for line in &mut docs {
if !line.is_empty() {
line.drain(..min_indent);
}
}
Self(docs)
}
pub(crate) fn as_formatted_string(&self) -> String {
self.join("\n")
}
}
impl Deref for CommentAttributes {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(test)]
mod tests {
use quote::quote;
use super::*;
fn parse_attrs(tokens: proc_macro2::TokenStream) -> Vec<Attribute> {
let item: syn::ItemStruct = syn::parse2(tokens).unwrap();
item.attrs
}
#[test]
fn test_comment_attributes_empty() {
let attrs = parse_attrs(quote! {
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
assert!(comments.is_empty());
}
#[test]
fn test_comment_attributes_single_doc() {
let attrs = parse_attrs(quote! {
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
assert_eq!(comments.len(), 1);
assert_eq!(comments[0], "This is a doc comment");
}
#[test]
fn test_comment_attributes_multiple_docs() {
let attrs = parse_attrs(quote! {
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
assert_eq!(comments.len(), 3);
assert_eq!(comments[0], "First line");
assert_eq!(comments[1], "Second line");
assert_eq!(comments[2], "Third line");
}
#[test]
fn test_comment_attributes_trims_whitespace() {
let attrs = parse_attrs(quote! {
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
assert_eq!(comments.len(), 1);
assert_eq!(comments[0], "Padded with spaces");
}
#[test]
fn test_comment_attributes_filters_non_doc() {
let attrs = parse_attrs(quote! {
#[derive(Debug)]
#[allow(dead_code)]
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
assert_eq!(comments.len(), 1);
assert_eq!(comments[0], "Doc comment");
}
#[test]
fn test_comment_attributes_as_formatted_string() {
let attrs = parse_attrs(quote! {
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
let formatted = comments.as_formatted_string();
assert_eq!(formatted, "First line\nSecond line");
}
#[test]
fn test_comment_attributes_deref() {
let attrs = parse_attrs(quote! {
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
let first: Option<&String> = comments.first();
assert_eq!(first.map(|s| s.as_str()), Some("Test"));
}
#[test]
fn test_comment_attributes_debug() {
let attrs = parse_attrs(quote! {
struct Foo;
});
let comments = CommentAttributes::from_attributes(&attrs);
let debug_str = format!("{comments:?}");
assert!(debug_str.contains("CommentAttributes"));
}
}