]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_ast/src/util/comments.rs
Fix invalid array access in `beautify_doc_string`
[rust.git] / compiler / rustc_ast / src / util / comments.rs
1 use crate::token::CommentKind;
2 use rustc_span::source_map::SourceMap;
3 use rustc_span::{BytePos, CharPos, FileName, Pos, Symbol};
4
5 #[cfg(test)]
6 mod tests;
7
8 #[derive(Clone, Copy, PartialEq, Debug)]
9 pub 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)]
21 pub struct Comment {
22     pub style: CommentStyle,
23     pub lines: Vec<String>,
24     pub pos: BytePos,
25 }
26
27 /// Makes a doc string more presentable to users.
28 /// Used by rustdoc and perhaps other tools, but not by rustc.
29 pub fn beautify_doc_string(data: Symbol, kind: CommentKind) -> Symbol {
30     fn get_vertical_trim(lines: &[&str]) -> Option<(usize, usize)> {
31         let mut i = 0;
32         let mut j = lines.len();
33         // first line of all-stars should be omitted
34         if !lines.is_empty() && lines[0].chars().all(|c| c == '*') {
35             i += 1;
36         }
37
38         // like the first, a last line of all stars should be omitted
39         if j > i && !lines[j - 1].is_empty() && lines[j - 1].chars().all(|c| c == '*') {
40             j -= 1;
41         }
42
43         if i != 0 || j != lines.len() { Some((i, j)) } else { None }
44     }
45
46     fn get_horizontal_trim<'a>(lines: &'a [&str], kind: CommentKind) -> Option<String> {
47         let mut i = usize::MAX;
48         let mut first = true;
49
50         // In case we have doc comments like `/**` or `/*!`, we want to remove stars if they are
51         // present. However, we first need to strip the empty lines so they don't get in the middle
52         // when we try to compute the "horizontal trim".
53         let lines = if kind == CommentKind::Block {
54             // Whatever happens, we skip the first line.
55             let mut i = lines
56                 .get(0)
57                 .map(|l| if l.trim_start().starts_with('*') { 0 } else { 1 })
58                 .unwrap_or(0);
59             let mut j = lines.len();
60
61             while i < j && lines[i].trim().is_empty() {
62                 i += 1;
63             }
64             while j > i && lines[j - 1].trim().is_empty() {
65                 j -= 1;
66             }
67             &lines[i..j]
68         } else {
69             lines
70         };
71
72         for line in lines {
73             for (j, c) in line.chars().enumerate() {
74                 if j > i || !"* \t".contains(c) {
75                     return None;
76                 }
77                 if c == '*' {
78                     if first {
79                         i = j;
80                         first = false;
81                     } else if i != j {
82                         return None;
83                     }
84                     break;
85                 }
86             }
87             if i >= line.len() {
88                 return None;
89             }
90         }
91         if lines.is_empty() { None } else { Some(lines[0][..i].into()) }
92     }
93
94     let data_s = data.as_str();
95     if data_s.contains('\n') {
96         let mut lines = data_s.lines().collect::<Vec<&str>>();
97         let mut changes = false;
98         let lines = if let Some((i, j)) = get_vertical_trim(&lines) {
99             changes = true;
100             // remove whitespace-only lines from the start/end of lines
101             &mut lines[i..j]
102         } else {
103             &mut lines
104         };
105         if let Some(horizontal) = get_horizontal_trim(&lines, kind) {
106             changes = true;
107             // remove a "[ \t]*\*" block from each line, if possible
108             for line in lines.iter_mut() {
109                 if let Some(tmp) = line.strip_prefix(&horizontal) {
110                     *line = tmp;
111                     if kind == CommentKind::Block
112                         && (*line == "*" || line.starts_with("* ") || line.starts_with("**"))
113                     {
114                         *line = &line[1..];
115                     }
116                 }
117             }
118         }
119         if changes {
120             return Symbol::intern(&lines.join("\n"));
121         }
122     }
123     data
124 }
125
126 /// Returns `None` if the first `col` chars of `s` contain a non-whitespace char.
127 /// Otherwise returns `Some(k)` where `k` is first char offset after that leading
128 /// whitespace. Note that `k` may be outside bounds of `s`.
129 fn all_whitespace(s: &str, col: CharPos) -> Option<usize> {
130     let mut idx = 0;
131     for (i, ch) in s.char_indices().take(col.to_usize()) {
132         if !ch.is_whitespace() {
133             return None;
134         }
135         idx = i + ch.len_utf8();
136     }
137     Some(idx)
138 }
139
140 fn trim_whitespace_prefix(s: &str, col: CharPos) -> &str {
141     let len = s.len();
142     match all_whitespace(&s, col) {
143         Some(col) => {
144             if col < len {
145                 &s[col..]
146             } else {
147                 ""
148             }
149         }
150         None => s,
151     }
152 }
153
154 fn split_block_comment_into_lines(text: &str, col: CharPos) -> Vec<String> {
155     let mut res: Vec<String> = vec![];
156     let mut lines = text.lines();
157     // just push the first line
158     res.extend(lines.next().map(|it| it.to_string()));
159     // for other lines, strip common whitespace prefix
160     for line in lines {
161         res.push(trim_whitespace_prefix(line, col).to_string())
162     }
163     res
164 }
165
166 // it appears this function is called only from pprust... that's
167 // probably not a good thing.
168 pub fn gather_comments(sm: &SourceMap, path: FileName, src: String) -> Vec<Comment> {
169     let sm = SourceMap::new(sm.path_mapping().clone());
170     let source_file = sm.new_source_file(path, src);
171     let text = (*source_file.src.as_ref().unwrap()).clone();
172
173     let text: &str = text.as_str();
174     let start_bpos = source_file.start_pos;
175     let mut pos = 0;
176     let mut comments: Vec<Comment> = Vec::new();
177     let mut code_to_the_left = false;
178
179     if let Some(shebang_len) = rustc_lexer::strip_shebang(text) {
180         comments.push(Comment {
181             style: CommentStyle::Isolated,
182             lines: vec![text[..shebang_len].to_string()],
183             pos: start_bpos,
184         });
185         pos += shebang_len;
186     }
187
188     for token in rustc_lexer::tokenize(&text[pos..]) {
189         let token_text = &text[pos..pos + token.len];
190         match token.kind {
191             rustc_lexer::TokenKind::Whitespace => {
192                 if let Some(mut idx) = token_text.find('\n') {
193                     code_to_the_left = false;
194                     while let Some(next_newline) = &token_text[idx + 1..].find('\n') {
195                         idx += 1 + next_newline;
196                         comments.push(Comment {
197                             style: CommentStyle::BlankLine,
198                             lines: vec![],
199                             pos: start_bpos + BytePos((pos + idx) as u32),
200                         });
201                     }
202                 }
203             }
204             rustc_lexer::TokenKind::BlockComment { doc_style, .. } => {
205                 if doc_style.is_none() {
206                     let code_to_the_right =
207                         !matches!(text[pos + token.len..].chars().next(), Some('\r' | '\n'));
208                     let style = match (code_to_the_left, code_to_the_right) {
209                         (_, true) => CommentStyle::Mixed,
210                         (false, false) => CommentStyle::Isolated,
211                         (true, false) => CommentStyle::Trailing,
212                     };
213
214                     // Count the number of chars since the start of the line by rescanning.
215                     let pos_in_file = start_bpos + BytePos(pos as u32);
216                     let line_begin_in_file = source_file.line_begin_pos(pos_in_file);
217                     let line_begin_pos = (line_begin_in_file - start_bpos).to_usize();
218                     let col = CharPos(text[line_begin_pos..pos].chars().count());
219
220                     let lines = split_block_comment_into_lines(token_text, col);
221                     comments.push(Comment { style, lines, pos: pos_in_file })
222                 }
223             }
224             rustc_lexer::TokenKind::LineComment { doc_style } => {
225                 if doc_style.is_none() {
226                     comments.push(Comment {
227                         style: if code_to_the_left {
228                             CommentStyle::Trailing
229                         } else {
230                             CommentStyle::Isolated
231                         },
232                         lines: vec![token_text.to_string()],
233                         pos: start_bpos + BytePos(pos as u32),
234                     })
235                 }
236             }
237             _ => {
238                 code_to_the_left = true;
239             }
240         }
241         pos += token.len;
242     }
243
244     comments
245 }