rustc_ast/util/
comments.rs

1use rustc_span::{BytePos, Symbol};
2
3use crate::token::CommentKind;
4
5#[cfg(test)]
6mod tests;
7
8#[derive(Clone, Copy, PartialEq, Debug)]
9pub enum CommentStyle {
10    /// No code on either side of each line of the comment
11    Isolated,
12    /// Code exists to the left of the comment
13    Trailing,
14    /// Code before /* foo */ and after the comment
15    Mixed,
16    /// Just a manual blank line "\n\n", for layout
17    BlankLine,
18}
19
20#[derive(Clone)]
21pub struct Comment {
22    pub style: CommentStyle,
23    pub lines: Vec<String>,
24    pub pos: BytePos,
25}
26
27/// A fast conservative estimate on whether the string can contain documentation links.
28/// A pair of square brackets `[]` must exist in the string, but we only search for the
29/// opening bracket because brackets always go in pairs in practice.
30#[inline]
31pub fn may_have_doc_links(s: &str) -> bool {
32    s.contains('[')
33}
34
35/// Makes a doc string more presentable to users.
36/// Used by rustdoc and perhaps other tools, but not by rustc.
37pub fn beautify_doc_string(data: Symbol, kind: CommentKind) -> Symbol {
38    fn get_vertical_trim(lines: &[&str]) -> Option<(usize, usize)> {
39        let mut i = 0;
40        let mut j = lines.len();
41        // first line of all-stars should be omitted
42        if lines.first().is_some_and(|line| line.chars().all(|c| c == '*')) {
43            i += 1;
44        }
45
46        // like the first, a last line of all stars should be omitted
47        if j > i && !lines[j - 1].is_empty() && lines[j - 1].chars().all(|c| c == '*') {
48            j -= 1;
49        }
50
51        if i != 0 || j != lines.len() { Some((i, j)) } else { None }
52    }
53
54    fn get_horizontal_trim(lines: &[&str], kind: CommentKind) -> Option<String> {
55        let mut i = usize::MAX;
56        let mut first = true;
57
58        // In case we have doc comments like `/**` or `/*!`, we want to remove stars if they are
59        // present. However, we first need to strip the empty lines so they don't get in the middle
60        // when we try to compute the "horizontal trim".
61        let lines = match kind {
62            CommentKind::Block => {
63                // Whatever happens, we skip the first line.
64                let mut i = lines
65                    .first()
66                    .map(|l| if l.trim_start().starts_with('*') { 0 } else { 1 })
67                    .unwrap_or(0);
68                let mut j = lines.len();
69
70                while i < j && lines[i].trim().is_empty() {
71                    i += 1;
72                }
73                while j > i && lines[j - 1].trim().is_empty() {
74                    j -= 1;
75                }
76                &lines[i..j]
77            }
78            CommentKind::Line => lines,
79        };
80
81        for line in lines {
82            for (j, c) in line.chars().enumerate() {
83                if j > i || !"* \t".contains(c) {
84                    return None;
85                }
86                if c == '*' {
87                    if first {
88                        i = j;
89                        first = false;
90                    } else if i != j {
91                        return None;
92                    }
93                    break;
94                }
95            }
96            if i >= line.len() {
97                return None;
98            }
99        }
100        Some(lines.first()?[..i].to_string())
101    }
102
103    let data_s = data.as_str();
104    if data_s.contains('\n') {
105        let mut lines = data_s.lines().collect::<Vec<&str>>();
106        let mut changes = false;
107        let lines = if let Some((i, j)) = get_vertical_trim(&lines) {
108            changes = true;
109            // remove whitespace-only lines from the start/end of lines
110            &mut lines[i..j]
111        } else {
112            &mut lines
113        };
114        if let Some(horizontal) = get_horizontal_trim(lines, kind) {
115            changes = true;
116            // remove a "[ \t]*\*" block from each line, if possible
117            for line in lines.iter_mut() {
118                if let Some(tmp) = line.strip_prefix(&horizontal) {
119                    *line = tmp;
120                    if kind == CommentKind::Block
121                        && (*line == "*" || line.starts_with("* ") || line.starts_with("**"))
122                    {
123                        *line = &line[1..];
124                    }
125                }
126            }
127        }
128        if changes {
129            return Symbol::intern(&lines.join("\n"));
130        }
131    }
132    data
133}