utoipa_gen/
doc_comment.rs

1use syn::{Attribute, Expr, Lit, Meta};
2
3const DOC_ATTRIBUTE_TYPE: &str = "doc";
4
5/// CommentAttributes holds Vec of parsed doc comments
6#[cfg_attr(feature = "debug", derive(Debug))]
7pub(crate) struct CommentAttributes(pub(crate) Vec<String>);
8
9impl CommentAttributes {
10    /// Creates new [`CommentAttributes`] instance from [`Attribute`] slice filtering out all
11    /// other attributes which are not `doc` comments
12    pub(crate) fn from_attributes(attributes: &[Attribute]) -> Self {
13        let mut docs = attributes
14            .iter()
15            .filter_map(|attr| {
16                if !matches!(attr.path().get_ident(), Some(ident) if ident == DOC_ATTRIBUTE_TYPE) {
17                    return None;
18                }
19                // ignore `#[doc(hidden)]` and similar tags.
20                if let Meta::NameValue(name_value) = &attr.meta {
21                    if let Expr::Lit(ref doc_comment) = name_value.value {
22                        if let Lit::Str(ref doc) = doc_comment.lit {
23                            let mut doc = doc.value();
24                            // NB. Only trim trailing whitespaces. Leading whitespaces are handled
25                            // below.
26                            doc.truncate(doc.trim_end().len());
27                            return Some(doc);
28                        }
29                    }
30                }
31                None
32            })
33            .collect::<Vec<_>>();
34        // Calculate the minimum indentation of all non-empty lines and strip them.
35        // This can get rid of typical single space after doc comment start `///`, but not messing
36        // up indentation of markdown list or code.
37        let min_indent = docs
38            .iter()
39            .filter(|s| !s.is_empty())
40            // Only recognize ASCII space, not unicode multi-bytes ones.
41            // `str::trim_ascii_start` requires 1.80 which is greater than our MSRV yet.
42            .map(|s| s.len() - s.trim_start_matches(' ').len())
43            .min()
44            .unwrap_or(0);
45        for line in &mut docs {
46            if !line.is_empty() {
47                line.drain(..min_indent);
48            }
49        }
50        Self(docs)
51    }
52
53    pub(crate) fn is_empty(&self) -> bool {
54        self.0.is_empty()
55    }
56
57    /// Returns found `doc comments` as formatted `String` joining them all with `\n` _(new line)_.
58    pub(crate) fn as_formatted_string(&self) -> String {
59        self.0.join("\n")
60    }
61}