]> git.lizzy.rs Git - rust.git/blob - src/libsyntax/parse/lexer/comments.rs
74fff3324eacf62e9cb6d5df08f489288cd87322
[rust.git] / src / libsyntax / parse / lexer / comments.rs
1 pub use CommentStyle::*;
2
3 use crate::ast;
4 use crate::source_map::SourceMap;
5 use crate::parse::lexer::{is_block_doc_comment, is_pattern_whitespace};
6 use crate::parse::lexer::{self, ParseSess, StringReader, TokenAndSpan};
7 use crate::print::pprust;
8
9 use syntax_pos::{BytePos, CharPos, Pos, FileName};
10 use log::debug;
11
12 use std::io::Read;
13 use std::usize;
14
15 #[derive(Clone, Copy, PartialEq, Debug)]
16 pub enum CommentStyle {
17     /// No code on either side of each line of the comment
18     Isolated,
19     /// Code exists to the left of the comment
20     Trailing,
21     /// Code before /* foo */ and after the comment
22     Mixed,
23     /// Just a manual blank line "\n\n", for layout
24     BlankLine,
25 }
26
27 #[derive(Clone)]
28 pub struct Comment {
29     pub style: CommentStyle,
30     pub lines: Vec<String>,
31     pub pos: BytePos,
32 }
33
34 fn is_doc_comment(s: &str) -> bool {
35     (s.starts_with("///") && super::is_doc_comment(s)) || s.starts_with("//!") ||
36     (s.starts_with("/**") && is_block_doc_comment(s)) || s.starts_with("/*!")
37 }
38
39 pub fn doc_comment_style(comment: &str) -> ast::AttrStyle {
40     assert!(is_doc_comment(comment));
41     if comment.starts_with("//!") || comment.starts_with("/*!") {
42         ast::AttrStyle::Inner
43     } else {
44         ast::AttrStyle::Outer
45     }
46 }
47
48 pub fn strip_doc_comment_decoration(comment: &str) -> String {
49     /// remove whitespace-only lines from the start/end of lines
50     fn vertical_trim(lines: Vec<String>) -> Vec<String> {
51         let mut i = 0;
52         let mut j = lines.len();
53         // first line of all-stars should be omitted
54         if !lines.is_empty() && lines[0].chars().all(|c| c == '*') {
55             i += 1;
56         }
57
58         while i < j && lines[i].trim().is_empty() {
59             i += 1;
60         }
61         // like the first, a last line of all stars should be omitted
62         if j > i &&
63            lines[j - 1]
64                .chars()
65                .skip(1)
66                .all(|c| c == '*') {
67             j -= 1;
68         }
69
70         while j > i && lines[j - 1].trim().is_empty() {
71             j -= 1;
72         }
73
74         lines[i..j].to_vec()
75     }
76
77     /// remove a "[ \t]*\*" block from each line, if possible
78     fn horizontal_trim(lines: Vec<String>) -> Vec<String> {
79         let mut i = usize::MAX;
80         let mut can_trim = true;
81         let mut first = true;
82
83         for line in &lines {
84             for (j, c) in line.chars().enumerate() {
85                 if j > i || !"* \t".contains(c) {
86                     can_trim = false;
87                     break;
88                 }
89                 if c == '*' {
90                     if first {
91                         i = j;
92                         first = false;
93                     } else if i != j {
94                         can_trim = false;
95                     }
96                     break;
97                 }
98             }
99             if i >= line.len() {
100                 can_trim = false;
101             }
102             if !can_trim {
103                 break;
104             }
105         }
106
107         if can_trim {
108             lines.iter()
109                  .map(|line| (&line[i + 1..line.len()]).to_string())
110                  .collect()
111         } else {
112             lines
113         }
114     }
115
116     // one-line comments lose their prefix
117     const ONELINERS: &[&str] = &["///!", "///", "//!", "//"];
118
119     for prefix in ONELINERS {
120         if comment.starts_with(*prefix) {
121             return (&comment[prefix.len()..]).to_string();
122         }
123     }
124
125     if comment.starts_with("/*") {
126         let lines = comment[3..comment.len() - 2]
127                         .lines()
128                         .map(|s| s.to_string())
129                         .collect::<Vec<String>>();
130
131         let lines = vertical_trim(lines);
132         let lines = horizontal_trim(lines);
133
134         return lines.join("\n");
135     }
136
137     panic!("not a doc-comment: {}", comment);
138 }
139
140 fn push_blank_line_comment(rdr: &StringReader<'_>, comments: &mut Vec<Comment>) {
141     debug!(">>> blank-line comment");
142     comments.push(Comment {
143         style: BlankLine,
144         lines: Vec::new(),
145         pos: rdr.pos,
146     });
147 }
148
149 fn consume_whitespace_counting_blank_lines(
150     rdr: &mut StringReader<'_>,
151     comments: &mut Vec<Comment>
152 ) {
153     while is_pattern_whitespace(rdr.ch) && !rdr.is_eof() {
154         if rdr.ch_is('\n') {
155             push_blank_line_comment(rdr, &mut *comments);
156         }
157         rdr.bump();
158     }
159 }
160
161 fn read_shebang_comment(rdr: &mut StringReader<'_>,
162                         code_to_the_left: bool,
163                         comments: &mut Vec<Comment>) {
164     debug!(">>> shebang comment");
165     let p = rdr.pos;
166     debug!("<<< shebang comment");
167     comments.push(Comment {
168         style: if code_to_the_left { Trailing } else { Isolated },
169         lines: vec![rdr.read_one_line_comment()],
170         pos: p,
171     });
172 }
173
174 fn read_line_comments(rdr: &mut StringReader<'_>,
175                       code_to_the_left: bool,
176                       comments: &mut Vec<Comment>) {
177     debug!(">>> line comments");
178     let p = rdr.pos;
179     let mut lines: Vec<String> = Vec::new();
180     while rdr.ch_is('/') && rdr.nextch_is('/') {
181         let line = rdr.read_one_line_comment();
182         debug!("{}", line);
183         // Doc comments are not put in comments.
184         if is_doc_comment(&line[..]) {
185             break;
186         }
187         lines.push(line);
188         rdr.consume_non_eol_whitespace();
189     }
190     debug!("<<< line comments");
191     if !lines.is_empty() {
192         comments.push(Comment {
193             style: if code_to_the_left { Trailing } else { Isolated },
194             lines,
195             pos: p,
196         });
197     }
198 }
199
200 /// Returns `None` if the first `col` chars of `s` contain a non-whitespace char.
201 /// Otherwise returns `Some(k)` where `k` is first char offset after that leading
202 /// whitespace. Note that `k` may be outside bounds of `s`.
203 fn all_whitespace(s: &str, col: CharPos) -> Option<usize> {
204     let mut idx = 0;
205     for (i, ch) in s.char_indices().take(col.to_usize()) {
206         if !ch.is_whitespace() {
207             return None;
208         }
209         idx = i + ch.len_utf8();
210     }
211     Some(idx)
212 }
213
214 fn trim_whitespace_prefix_and_push_line(lines: &mut Vec<String>, s: String, col: CharPos) {
215     let len = s.len();
216     let s1 = match all_whitespace(&s[..], col) {
217         Some(col) => {
218             if col < len {
219                 s[col..len].to_string()
220             } else {
221                 String::new()
222             }
223         }
224         None => s,
225     };
226     debug!("pushing line: {}", s1);
227     lines.push(s1);
228 }
229
230 fn read_block_comment(rdr: &mut StringReader<'_>,
231                       code_to_the_left: bool,
232                       comments: &mut Vec<Comment>) {
233     debug!(">>> block comment");
234     let p = rdr.pos;
235     let mut lines: Vec<String> = Vec::new();
236
237     // Count the number of chars since the start of the line by rescanning.
238     let src_index = rdr.src_index(rdr.source_file.line_begin_pos(rdr.pos));
239     let end_src_index = rdr.src_index(rdr.pos);
240     assert!(src_index <= end_src_index,
241         "src_index={}, end_src_index={}, line_begin_pos={}",
242         src_index, end_src_index, rdr.source_file.line_begin_pos(rdr.pos).to_u32());
243
244     let col = CharPos(rdr.src[src_index..end_src_index].chars().count());
245
246     rdr.bump();
247     rdr.bump();
248
249     let mut curr_line = String::from("/*");
250
251     // doc-comments are not really comments, they are attributes
252     if (rdr.ch_is('*') && !rdr.nextch_is('*')) || rdr.ch_is('!') {
253         while !(rdr.ch_is('*') && rdr.nextch_is('/')) && !rdr.is_eof() {
254             curr_line.push(rdr.ch.unwrap());
255             rdr.bump();
256         }
257         if !rdr.is_eof() {
258             curr_line.push_str("*/");
259             rdr.bump();
260             rdr.bump();
261         }
262         if is_block_doc_comment(&curr_line[..]) {
263             return;
264         }
265         assert!(!curr_line.contains('\n'));
266         lines.push(curr_line);
267     } else {
268         let mut level: isize = 1;
269         while level > 0 {
270             debug!("=== block comment level {}", level);
271             if rdr.is_eof() {
272                 rdr.fatal("unterminated block comment").raise();
273             }
274             if rdr.ch_is('\n') {
275                 trim_whitespace_prefix_and_push_line(&mut lines, curr_line, col);
276                 curr_line = String::new();
277                 rdr.bump();
278             } else {
279                 curr_line.push(rdr.ch.unwrap());
280                 if rdr.ch_is('/') && rdr.nextch_is('*') {
281                     rdr.bump();
282                     rdr.bump();
283                     curr_line.push('*');
284                     level += 1;
285                 } else {
286                     if rdr.ch_is('*') && rdr.nextch_is('/') {
287                         rdr.bump();
288                         rdr.bump();
289                         curr_line.push('/');
290                         level -= 1;
291                     } else {
292                         rdr.bump();
293                     }
294                 }
295             }
296         }
297         if !curr_line.is_empty() {
298             trim_whitespace_prefix_and_push_line(&mut lines, curr_line, col);
299         }
300     }
301
302     let mut style = if code_to_the_left {
303         Trailing
304     } else {
305         Isolated
306     };
307     rdr.consume_non_eol_whitespace();
308     if !rdr.is_eof() && !rdr.ch_is('\n') && lines.len() == 1 {
309         style = Mixed;
310     }
311     debug!("<<< block comment");
312     comments.push(Comment {
313         style,
314         lines,
315         pos: p,
316     });
317 }
318
319
320 fn consume_comment(rdr: &mut StringReader<'_>,
321                    comments: &mut Vec<Comment>,
322                    code_to_the_left: &mut bool,
323                    anything_to_the_left: &mut bool) {
324     debug!(">>> consume comment");
325     if rdr.ch_is('/') && rdr.nextch_is('/') {
326         read_line_comments(rdr, *code_to_the_left, comments);
327         *code_to_the_left = false;
328         *anything_to_the_left = false;
329     } else if rdr.ch_is('/') && rdr.nextch_is('*') {
330         read_block_comment(rdr, *code_to_the_left, comments);
331         *anything_to_the_left = true;
332     } else if rdr.ch_is('#') && rdr.nextch_is('!') {
333         read_shebang_comment(rdr, *code_to_the_left, comments);
334         *code_to_the_left = false;
335         *anything_to_the_left = false;
336     } else {
337         panic!();
338     }
339     debug!("<<< consume comment");
340 }
341
342 #[derive(Clone)]
343 pub struct Literal {
344     pub lit: String,
345     pub pos: BytePos,
346 }
347
348 // it appears this function is called only from pprust... that's
349 // probably not a good thing.
350 pub fn gather_comments_and_literals(sess: &ParseSess, path: FileName, srdr: &mut dyn Read)
351     -> (Vec<Comment>, Vec<Literal>)
352 {
353     let mut src = String::new();
354     srdr.read_to_string(&mut src).unwrap();
355     let cm = SourceMap::new(sess.source_map().path_mapping().clone());
356     let source_file = cm.new_source_file(path, src);
357     let mut rdr = lexer::StringReader::new_raw(sess, source_file, None);
358
359     let mut comments: Vec<Comment> = Vec::new();
360     let mut literals: Vec<Literal> = Vec::new();
361     let mut code_to_the_left = false; // Only code
362     let mut anything_to_the_left = false; // Code or comments
363
364     while !rdr.is_eof() {
365         loop {
366             // Eat all the whitespace and count blank lines.
367             rdr.consume_non_eol_whitespace();
368             if rdr.ch_is('\n') {
369                 if anything_to_the_left {
370                     rdr.bump(); // The line is not blank, do not count.
371                 }
372                 consume_whitespace_counting_blank_lines(&mut rdr, &mut comments);
373                 code_to_the_left = false;
374                 anything_to_the_left = false;
375             }
376             // Eat one comment group
377             if rdr.peeking_at_comment() {
378                 consume_comment(&mut rdr, &mut comments,
379                                 &mut code_to_the_left, &mut anything_to_the_left);
380             } else {
381                 break
382             }
383         }
384
385         let bstart = rdr.pos;
386         rdr.next_token();
387         // discard, and look ahead; we're working with internal state
388         let TokenAndSpan { tok, sp } = rdr.peek();
389         if tok.is_lit() {
390             rdr.with_str_from(bstart, |s| {
391                 debug!("tok lit: {}", s);
392                 literals.push(Literal {
393                     lit: s.to_string(),
394                     pos: sp.lo(),
395                 });
396             })
397         } else {
398             debug!("tok: {}", pprust::token_to_string(&tok));
399         }
400         code_to_the_left = true;
401         anything_to_the_left = true;
402     }
403
404     (comments, literals)
405 }
406
407 #[cfg(test)]
408 mod tests {
409     use super::*;
410
411     #[test]
412     fn test_block_doc_comment_1() {
413         let comment = "/**\n * Test \n **  Test\n *   Test\n*/";
414         let stripped = strip_doc_comment_decoration(comment);
415         assert_eq!(stripped, " Test \n*  Test\n   Test");
416     }
417
418     #[test]
419     fn test_block_doc_comment_2() {
420         let comment = "/**\n * Test\n *  Test\n*/";
421         let stripped = strip_doc_comment_decoration(comment);
422         assert_eq!(stripped, " Test\n  Test");
423     }
424
425     #[test]
426     fn test_block_doc_comment_3() {
427         let comment = "/**\n let a: *i32;\n *a = 5;\n*/";
428         let stripped = strip_doc_comment_decoration(comment);
429         assert_eq!(stripped, " let a: *i32;\n *a = 5;");
430     }
431
432     #[test]
433     fn test_block_doc_comment_4() {
434         let comment = "/*******************\n test\n *********************/";
435         let stripped = strip_doc_comment_decoration(comment);
436         assert_eq!(stripped, " test");
437     }
438
439     #[test]
440     fn test_line_doc_comment() {
441         let stripped = strip_doc_comment_decoration("/// test");
442         assert_eq!(stripped, " test");
443         let stripped = strip_doc_comment_decoration("///! test");
444         assert_eq!(stripped, " test");
445         let stripped = strip_doc_comment_decoration("// test");
446         assert_eq!(stripped, " test");
447         let stripped = strip_doc_comment_decoration("// test");
448         assert_eq!(stripped, " test");
449         let stripped = strip_doc_comment_decoration("///test");
450         assert_eq!(stripped, "test");
451         let stripped = strip_doc_comment_decoration("///!test");
452         assert_eq!(stripped, "test");
453         let stripped = strip_doc_comment_decoration("//test");
454         assert_eq!(stripped, "test");
455     }
456 }