clap_derive/utils/
doc_comments.rs

1//! The preprocessing we apply to doc comments.
2//!
3//! #[derive(Parser)] works in terms of "paragraphs". Paragraph is a sequence of
4//! non-empty adjacent lines, delimited by sequences of blank (whitespace only) lines.
5
6use std::iter;
7
8pub(crate) fn extract_doc_comment(attrs: &[syn::Attribute]) -> Vec<String> {
9    // multiline comments (`/** ... */`) may have LFs (`\n`) in them,
10    // we need to split so we could handle the lines correctly
11    //
12    // we also need to remove leading and trailing blank lines
13    let mut lines: Vec<_> = attrs
14        .iter()
15        .filter(|attr| attr.path().is_ident("doc"))
16        .filter_map(|attr| {
17            // non #[doc = "..."] attributes are not our concern
18            // we leave them for rustc to handle
19            match &attr.meta {
20                syn::Meta::NameValue(syn::MetaNameValue {
21                    value:
22                        syn::Expr::Lit(syn::ExprLit {
23                            lit: syn::Lit::Str(s),
24                            ..
25                        }),
26                    ..
27                }) => Some(s.value()),
28                _ => None,
29            }
30        })
31        .skip_while(|s| is_blank(s))
32        .flat_map(|s| {
33            let lines = s
34                .split('\n')
35                .map(|s| {
36                    // remove one leading space no matter what
37                    let s = s.strip_prefix(' ').unwrap_or(s);
38                    s.to_owned()
39                })
40                .collect::<Vec<_>>();
41            lines
42        })
43        .collect();
44
45    while let Some(true) = lines.last().map(|s| is_blank(s)) {
46        lines.pop();
47    }
48
49    lines
50}
51
52pub(crate) fn format_doc_comment(
53    lines: &[String],
54    preprocess: bool,
55    force_long: bool,
56) -> (Option<String>, Option<String>) {
57    if let Some(first_blank) = lines.iter().position(|s| is_blank(s)) {
58        let (short, long) = if preprocess {
59            let paragraphs = split_paragraphs(lines);
60            let short = paragraphs[0].clone();
61            let long = paragraphs.join("\n\n");
62            (remove_period(short), long)
63        } else {
64            let short = lines[..first_blank].join("\n");
65            let long = lines.join("\n");
66            (short, long)
67        };
68
69        (Some(short), Some(long))
70    } else {
71        let (short, long) = if preprocess {
72            let short = merge_lines(lines);
73            let long = force_long.then(|| short.clone());
74            let short = remove_period(short);
75            (short, long)
76        } else {
77            let short = lines.join("\n");
78            let long = force_long.then(|| short.clone());
79            (short, long)
80        };
81
82        (Some(short), long)
83    }
84}
85
86fn split_paragraphs(lines: &[String]) -> Vec<String> {
87    let mut last_line = 0;
88    iter::from_fn(|| {
89        let slice = &lines[last_line..];
90        let start = slice.iter().position(|s| !is_blank(s)).unwrap_or(0);
91
92        let slice = &slice[start..];
93        let len = slice
94            .iter()
95            .position(|s| is_blank(s))
96            .unwrap_or(slice.len());
97
98        last_line += start + len;
99
100        if len != 0 {
101            Some(merge_lines(&slice[..len]))
102        } else {
103            None
104        }
105    })
106    .collect()
107}
108
109fn remove_period(mut s: String) -> String {
110    if s.ends_with('.') && !s.ends_with("..") {
111        s.pop();
112    }
113    s
114}
115
116fn is_blank(s: &str) -> bool {
117    s.trim().is_empty()
118}
119
120fn merge_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> String {
121    lines
122        .into_iter()
123        .map(|s| s.as_ref().trim().to_owned())
124        .collect::<Vec<_>>()
125        .join(" ")
126}