]> git.lizzy.rs Git - rust.git/blob - src/libsyntax/parse/comments.rs
libsyntax: De-`@str` pathnames
[rust.git] / src / libsyntax / parse / comments.rs
1 // Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10
11 use ast;
12 use codemap::{BytePos, CharPos, CodeMap, Pos};
13 use diagnostic;
14 use parse::lexer::{is_whitespace, with_str_from, Reader};
15 use parse::lexer::{StringReader, bump, is_eof, nextch, TokenAndSpan};
16 use parse::lexer::{is_line_non_doc_comment, is_block_non_doc_comment};
17 use parse::lexer;
18 use parse::token;
19 use parse::token::{get_ident_interner};
20
21 use std::io;
22 use std::str;
23 use std::uint;
24
25 #[deriving(Clone, Eq)]
26 pub enum CommentStyle {
27     Isolated, // No code on either side of each line of the comment
28     Trailing, // Code exists to the left of the comment
29     Mixed, // Code before /* foo */ and after the comment
30     BlankLine, // Just a manual blank line "\n\n", for layout
31 }
32
33 #[deriving(Clone)]
34 pub struct Comment {
35     style: CommentStyle,
36     lines: ~[~str],
37     pos: BytePos
38 }
39
40 pub fn is_doc_comment(s: &str) -> bool {
41     (s.starts_with("///") && !is_line_non_doc_comment(s)) ||
42     s.starts_with("//!") ||
43     (s.starts_with("/**") && !is_block_non_doc_comment(s)) ||
44     s.starts_with("/*!")
45 }
46
47 pub fn doc_comment_style(comment: &str) -> ast::AttrStyle {
48     assert!(is_doc_comment(comment));
49     if comment.starts_with("//!") || comment.starts_with("/*!") {
50         ast::AttrInner
51     } else {
52         ast::AttrOuter
53     }
54 }
55
56 pub fn strip_doc_comment_decoration(comment: &str) -> ~str {
57     /// remove whitespace-only lines from the start/end of lines
58     fn vertical_trim(lines: ~[~str]) -> ~[~str] {
59         let mut i = 0u;
60         let mut j = lines.len();
61         // first line of all-stars should be omitted
62         if lines.len() > 0 && lines[0].chars().all(|c| c == '*') {
63             i += 1;
64         }
65         while i < j && lines[i].trim().is_empty() {
66             i += 1;
67         }
68         // like the first, a last line of all stars should be omitted
69         if j > i && lines[j - 1].chars().skip(1).all(|c| c == '*') {
70             j -= 1;
71         }
72         while j > i && lines[j - 1].trim().is_empty() {
73             j -= 1;
74         }
75         return lines.slice(i, j).to_owned();
76     }
77
78     /// remove a "[ \t]*\*" block from each line, if possible
79     fn horizontal_trim(lines: ~[~str]) -> ~[~str] {
80         let mut i = uint::MAX;
81         let mut can_trim = true;
82         let mut first = true;
83         for line in lines.iter() {
84             for (j, c) in line.chars().enumerate() {
85                 if j > i || !"* \t".contains_char(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.map(|line| line.slice(i + 1, line.len()).to_owned())
109         } else {
110             lines
111         }
112     }
113
114     // one-line comments lose their prefix
115     static ONLINERS: &'static [&'static str] = &["///!", "///", "//!", "//"];
116     for prefix in ONLINERS.iter() {
117         if comment.starts_with(*prefix) {
118             return comment.slice_from(prefix.len()).to_owned();
119         }
120     }
121
122     if comment.starts_with("/*") {
123         let lines = comment.slice(3u, comment.len() - 2u)
124             .lines_any()
125             .map(|s| s.to_owned())
126             .collect::<~[~str]>();
127
128         let lines = vertical_trim(lines);
129         let lines = horizontal_trim(lines);
130
131         return lines.connect("\n");
132     }
133
134     fail!("not a doc-comment: {}", comment);
135 }
136
137 fn read_to_eol(rdr: @StringReader) -> ~str {
138     let mut val = ~"";
139     while rdr.curr.get() != '\n' && !is_eof(rdr) {
140         val.push_char(rdr.curr.get());
141         bump(rdr);
142     }
143     if rdr.curr.get() == '\n' { bump(rdr); }
144     return val;
145 }
146
147 fn read_one_line_comment(rdr: @StringReader) -> ~str {
148     let val = read_to_eol(rdr);
149     assert!((val[0] == '/' as u8 && val[1] == '/' as u8) ||
150                  (val[0] == '#' as u8 && val[1] == '!' as u8));
151     return val;
152 }
153
154 fn consume_non_eol_whitespace(rdr: @StringReader) {
155     while is_whitespace(rdr.curr.get()) && rdr.curr.get() != '\n' &&
156             !is_eof(rdr) {
157         bump(rdr);
158     }
159 }
160
161 fn push_blank_line_comment(rdr: @StringReader, comments: &mut ~[Comment]) {
162     debug!(">>> blank-line comment");
163     let v: ~[~str] = ~[];
164     comments.push(Comment {
165         style: BlankLine,
166         lines: v,
167         pos: rdr.last_pos.get(),
168     });
169 }
170
171 fn consume_whitespace_counting_blank_lines(rdr: @StringReader,
172                                            comments: &mut ~[Comment]) {
173     while is_whitespace(rdr.curr.get()) && !is_eof(rdr) {
174         if rdr.col.get() == CharPos(0u) && rdr.curr.get() == '\n' {
175             push_blank_line_comment(rdr, &mut *comments);
176         }
177         bump(rdr);
178     }
179 }
180
181
182 fn read_shebang_comment(rdr: @StringReader, code_to_the_left: bool,
183                                             comments: &mut ~[Comment]) {
184     debug!(">>> shebang comment");
185     let p = rdr.last_pos.get();
186     debug!("<<< shebang comment");
187     comments.push(Comment {
188         style: if code_to_the_left { Trailing } else { Isolated },
189         lines: ~[read_one_line_comment(rdr)],
190         pos: p
191     });
192 }
193
194 fn read_line_comments(rdr: @StringReader, code_to_the_left: bool,
195                                           comments: &mut ~[Comment]) {
196     debug!(">>> line comments");
197     let p = rdr.last_pos.get();
198     let mut lines: ~[~str] = ~[];
199     while rdr.curr.get() == '/' && nextch(rdr) == '/' {
200         let line = read_one_line_comment(rdr);
201         debug!("{}", line);
202         if is_doc_comment(line) { // doc-comments are not put in comments
203             break;
204         }
205         lines.push(line);
206         consume_non_eol_whitespace(rdr);
207     }
208     debug!("<<< line comments");
209     if !lines.is_empty() {
210         comments.push(Comment {
211             style: if code_to_the_left { Trailing } else { Isolated },
212             lines: lines,
213             pos: p
214         });
215     }
216 }
217
218 // Returns None if the first col chars of s contain a non-whitespace char.
219 // Otherwise returns Some(k) where k is first char offset after that leading
220 // whitespace.  Note k may be outside bounds of s.
221 fn all_whitespace(s: &str, col: CharPos) -> Option<uint> {
222     let len = s.len();
223     let mut col = col.to_uint();
224     let mut cursor: uint = 0;
225     while col > 0 && cursor < len {
226         let r: str::CharRange = s.char_range_at(cursor);
227         if !r.ch.is_whitespace() {
228             return None;
229         }
230         cursor = r.next;
231         col -= 1;
232     }
233     return Some(cursor);
234 }
235
236 fn trim_whitespace_prefix_and_push_line(lines: &mut ~[~str],
237                                         s: ~str, col: CharPos) {
238     let len = s.len();
239     let s1 = match all_whitespace(s, col) {
240         Some(col) => {
241             if col < len {
242                 s.slice(col, len).to_owned()
243             } else {  ~"" }
244         }
245         None => s,
246     };
247     debug!("pushing line: {}", s1);
248     lines.push(s1);
249 }
250
251 fn read_block_comment(rdr: @StringReader,
252                       code_to_the_left: bool,
253                       comments: &mut ~[Comment]) {
254     debug!(">>> block comment");
255     let p = rdr.last_pos.get();
256     let mut lines: ~[~str] = ~[];
257     let col: CharPos = rdr.col.get();
258     bump(rdr);
259     bump(rdr);
260
261     let mut curr_line = ~"/*";
262
263     // doc-comments are not really comments, they are attributes
264     if rdr.curr.get() == '*' || rdr.curr.get() == '!' {
265         while !(rdr.curr.get() == '*' && nextch(rdr) == '/') && !is_eof(rdr) {
266             curr_line.push_char(rdr.curr.get());
267             bump(rdr);
268         }
269         if !is_eof(rdr) {
270             curr_line.push_str("*/");
271             bump(rdr);
272             bump(rdr);
273         }
274         if !is_block_non_doc_comment(curr_line) { return; }
275         assert!(!curr_line.contains_char('\n'));
276         lines.push(curr_line);
277     } else {
278         let mut level: int = 1;
279         while level > 0 {
280             debug!("=== block comment level {}", level);
281             if is_eof(rdr) {
282                 (rdr as @Reader).fatal(~"unterminated block comment");
283             }
284             if rdr.curr.get() == '\n' {
285                 trim_whitespace_prefix_and_push_line(&mut lines, curr_line,
286                                                      col);
287                 curr_line = ~"";
288                 bump(rdr);
289             } else {
290                 curr_line.push_char(rdr.curr.get());
291                 if rdr.curr.get() == '/' && nextch(rdr) == '*' {
292                     bump(rdr);
293                     bump(rdr);
294                     curr_line.push_char('*');
295                     level += 1;
296                 } else {
297                     if rdr.curr.get() == '*' && nextch(rdr) == '/' {
298                         bump(rdr);
299                         bump(rdr);
300                         curr_line.push_char('/');
301                         level -= 1;
302                     } else { bump(rdr); }
303                 }
304             }
305         }
306         if curr_line.len() != 0 {
307             trim_whitespace_prefix_and_push_line(&mut lines, curr_line, col);
308         }
309     }
310
311     let mut style = if code_to_the_left { Trailing } else { Isolated };
312     consume_non_eol_whitespace(rdr);
313     if !is_eof(rdr) && rdr.curr.get() != '\n' && lines.len() == 1u {
314         style = Mixed;
315     }
316     debug!("<<< block comment");
317     comments.push(Comment {style: style, lines: lines, pos: p});
318 }
319
320 fn peeking_at_comment(rdr: @StringReader) -> bool {
321     return ((rdr.curr.get() == '/' && nextch(rdr) == '/') ||
322          (rdr.curr.get() == '/' && nextch(rdr) == '*')) ||
323          (rdr.curr.get() == '#' && nextch(rdr) == '!');
324 }
325
326 fn consume_comment(rdr: @StringReader,
327                    code_to_the_left: bool,
328                    comments: &mut ~[Comment]) {
329     debug!(">>> consume comment");
330     if rdr.curr.get() == '/' && nextch(rdr) == '/' {
331         read_line_comments(rdr, code_to_the_left, comments);
332     } else if rdr.curr.get() == '/' && nextch(rdr) == '*' {
333         read_block_comment(rdr, code_to_the_left, comments);
334     } else if rdr.curr.get() == '#' && nextch(rdr) == '!' {
335         read_shebang_comment(rdr, code_to_the_left, comments);
336     } else { fail!(); }
337     debug!("<<< consume comment");
338 }
339
340 #[deriving(Clone)]
341 pub struct Literal {
342     lit: ~str,
343     pos: BytePos
344 }
345
346 // it appears this function is called only from pprust... that's
347 // probably not a good thing.
348 pub fn gather_comments_and_literals(span_diagnostic:
349                                         @diagnostic::SpanHandler,
350                                     path: ~str,
351                                     srdr: &mut io::Reader)
352                                  -> (~[Comment], ~[Literal]) {
353     let src = str::from_utf8_owned(srdr.read_to_end()).unwrap();
354     let cm = CodeMap::new();
355     let filemap = cm.new_filemap(path, src);
356     let rdr = lexer::new_low_level_string_reader(span_diagnostic, filemap);
357
358     let mut comments: ~[Comment] = ~[];
359     let mut literals: ~[Literal] = ~[];
360     let mut first_read: bool = true;
361     while !is_eof(rdr) {
362         loop {
363             let mut code_to_the_left = !first_read;
364             consume_non_eol_whitespace(rdr);
365             if rdr.curr.get() == '\n' {
366                 code_to_the_left = false;
367                 consume_whitespace_counting_blank_lines(rdr, &mut comments);
368             }
369             while peeking_at_comment(rdr) {
370                 consume_comment(rdr, code_to_the_left, &mut comments);
371                 consume_whitespace_counting_blank_lines(rdr, &mut comments);
372             }
373             break;
374         }
375
376
377         let bstart = rdr.last_pos.get();
378         rdr.next_token();
379         //discard, and look ahead; we're working with internal state
380         let TokenAndSpan {tok: tok, sp: sp} = rdr.peek();
381         if token::is_lit(&tok) {
382             with_str_from(rdr, bstart, |s| {
383                 debug!("tok lit: {}", s);
384                 literals.push(Literal {lit: s.to_owned(), pos: sp.lo});
385             })
386         } else {
387             debug!("tok: {}", token::to_str(get_ident_interner(), &tok));
388         }
389         first_read = false;
390     }
391
392     (comments, literals)
393 }
394
395 #[cfg(test)]
396 mod test {
397     use super::*;
398
399     #[test] fn test_block_doc_comment_1() {
400         let comment = "/**\n * Test \n **  Test\n *   Test\n*/";
401         let stripped = strip_doc_comment_decoration(comment);
402         assert_eq!(stripped, ~" Test \n*  Test\n   Test");
403     }
404
405     #[test] fn test_block_doc_comment_2() {
406         let comment = "/**\n * Test\n *  Test\n*/";
407         let stripped = strip_doc_comment_decoration(comment);
408         assert_eq!(stripped, ~" Test\n  Test");
409     }
410
411     #[test] fn test_block_doc_comment_3() {
412         let comment = "/**\n let a: *int;\n *a = 5;\n*/";
413         let stripped = strip_doc_comment_decoration(comment);
414         assert_eq!(stripped, ~" let a: *int;\n *a = 5;");
415     }
416
417     #[test] fn test_block_doc_comment_4() {
418         let comment = "/*******************\n test\n *********************/";
419         let stripped = strip_doc_comment_decoration(comment);
420         assert_eq!(stripped, ~" test");
421     }
422
423     #[test] fn test_line_doc_comment() {
424         let stripped = strip_doc_comment_decoration("/// test");
425         assert_eq!(stripped, ~" test");
426         let stripped = strip_doc_comment_decoration("///! test");
427         assert_eq!(stripped, ~" test");
428         let stripped = strip_doc_comment_decoration("// test");
429         assert_eq!(stripped, ~" test");
430         let stripped = strip_doc_comment_decoration("// test");
431         assert_eq!(stripped, ~" test");
432         let stripped = strip_doc_comment_decoration("///test");
433         assert_eq!(stripped, ~"test");
434         let stripped = strip_doc_comment_decoration("///!test");
435         assert_eq!(stripped, ~"test");
436         let stripped = strip_doc_comment_decoration("//test");
437         assert_eq!(stripped, ~"test");
438     }
439 }