]> git.lizzy.rs Git - rust.git/blob - src/comment.rs
Refactor indent and width into Shape struct
[rust.git] / src / comment.rs
1 // Copyright 2015 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 // Formatting and tools for comments.
12
13 use std::{self, iter};
14
15 use syntax::codemap::Span;
16
17 use {Indent, Shape};
18 use config::Config;
19 use rewrite::RewriteContext;
20 use string::{StringFormat, rewrite_string};
21 use utils::wrap_str;
22
23 fn is_custom_comment(comment: &str) -> bool {
24     if !comment.starts_with("//") {
25         false
26     } else {
27         if let Some(c) = comment.chars().nth(2) {
28             !c.is_alphanumeric() && !c.is_whitespace()
29         } else {
30             false
31         }
32     }
33 }
34
35 pub fn rewrite_comment(orig: &str,
36                        block_style: bool,
37                        shape: Shape,
38                        config: &Config)
39                        -> Option<String> {
40     // If there are lines without a starting sigil, we won't format them correctly
41     // so in that case we won't even re-align (if !config.normalize_comments) and
42     // we should stop now.
43     let num_bare_lines = orig.lines()
44         .map(|line| line.trim())
45         .filter(|l| !(l.starts_with('*') || l.starts_with("//") || l.starts_with("/*")))
46         .count();
47     if num_bare_lines > 0 && !config.normalize_comments {
48         return Some(orig.to_owned());
49     }
50
51     if !config.normalize_comments && !config.wrap_comments {
52         return light_rewrite_comment(orig, shape.indent, config);
53     }
54
55     let (opener, closer, line_start) =
56         if block_style {
57             ("/* ", " */", " * ")
58         } else if !config.normalize_comments {
59             if orig.starts_with("/**") && !orig.starts_with("/**/") {
60                 ("/** ", " **/", " ** ")
61             } else if orig.starts_with("/*!") {
62                 ("/*! ", " */", " * ")
63             } else if orig.starts_with("/*") {
64                 ("/* ", " */", " * ")
65             } else if orig.starts_with("///") {
66                 ("/// ", "", "/// ")
67             } else if orig.starts_with("//!") {
68                 ("//! ", "", "//! ")
69             } else {
70                 ("// ", "", "// ")
71             }
72         } else if orig.starts_with("///") ||
73                   (orig.starts_with("/**") && !orig.starts_with("/**/")) {
74             ("/// ", "", "/// ")
75         } else if orig.starts_with("//!") || orig.starts_with("/*!") {
76             ("//! ", "", "//! ")
77         } else if is_custom_comment(orig) {
78             if orig.chars().nth(3) == Some(' ') {
79                 (&orig[0..4], "", &orig[0..4])
80             } else {
81                 (&orig[0..3], "", &orig[0..3])
82             }
83         } else {
84             ("// ", "", "// ")
85         };
86
87     let max_chars = shape.width.checked_sub(closer.len() + opener.len()).unwrap_or(1);
88     let indent_str = shape.indent.to_string(config);
89     let fmt = StringFormat {
90         opener: "",
91         closer: "",
92         line_start: line_start,
93         line_end: "",
94         shape: Shape::legacy(max_chars, shape.indent + (opener.len() - line_start.len())),
95         trim_end: true,
96         config: config,
97     };
98
99     let line_breaks = orig.trim_right().chars().filter(|&c| c == '\n').count();
100     let lines = orig.lines()
101         .enumerate()
102         .map(|(i, mut line)| {
103             line = line.trim();
104             // Drop old closer.
105             if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
106                 line = &line[..(line.len() - 2)].trim_right();
107             }
108
109             line
110         })
111         .map(left_trim_comment_line)
112         .map(|line| if orig.starts_with("/*") && line_breaks == 0 {
113             line.trim_left()
114         } else {
115             line
116         });
117
118     let mut result = opener.to_owned();
119     for line in lines {
120         if result == opener {
121             if line.is_empty() {
122                 continue;
123             }
124         } else {
125             result.push('\n');
126             result.push_str(&indent_str);
127             result.push_str(line_start);
128         }
129
130         if config.wrap_comments && line.len() > max_chars {
131             let rewrite = rewrite_string(line, &fmt).unwrap_or(line.to_owned());
132             result.push_str(&rewrite);
133         } else {
134             if line.is_empty() {
135                 // Remove space if this is an empty comment or a doc comment.
136                 result.pop();
137             }
138             result.push_str(line);
139         }
140     }
141
142     result.push_str(closer);
143     if result == opener {
144         // Trailing space.
145         result.pop();
146     }
147
148     Some(result)
149 }
150
151 /// Trims whitespace and aligns to indent, but otherwise does not change comments.
152 fn light_rewrite_comment(orig: &str, offset: Indent, config: &Config) -> Option<String> {
153     let lines: Vec<&str> = orig.lines()
154         .map(|l| {
155             // This is basically just l.trim(), but in the case that a line starts
156             // with `*` we want to leave one space before it, so it aligns with the
157             // `*` in `/*`.
158             let first_non_whitespace = l.find(|c| !char::is_whitespace(c));
159             if let Some(fnw) = first_non_whitespace {
160                     if l.as_bytes()[fnw] == '*' as u8 && fnw > 0 {
161                         &l[fnw - 1..]
162                     } else {
163                         &l[fnw..]
164                     }
165                 } else {
166                     ""
167                 }
168                 .trim_right()
169         })
170         .collect();
171     Some(lines.join(&format!("\n{}", offset.to_string(config))))
172 }
173
174 /// Trims comment characters and possibly a single space from the left of a string.
175 /// Does not trim all whitespace.
176 fn left_trim_comment_line(line: &str) -> &str {
177     if line.starts_with("//! ") || line.starts_with("/// ") || line.starts_with("/*! ") ||
178        line.starts_with("/** ") {
179         &line[4..]
180     } else if is_custom_comment(line) {
181         if line.len() > 3 && line.chars().nth(3) == Some(' ') {
182             &line[4..]
183         } else {
184             &line[3..]
185         }
186     } else if line.starts_with("/* ") || line.starts_with("// ") || line.starts_with("//!") ||
187               line.starts_with("///") ||
188               line.starts_with("** ") || line.starts_with("/*!") ||
189               (line.starts_with("/**") && !line.starts_with("/**/")) {
190         &line[3..]
191     } else if line.starts_with("/*") || line.starts_with("* ") || line.starts_with("//") ||
192               line.starts_with("**") {
193         &line[2..]
194     } else if line.starts_with('*') {
195         &line[1..]
196     } else {
197         line
198     }
199 }
200
201 pub trait FindUncommented {
202     fn find_uncommented(&self, pat: &str) -> Option<usize>;
203 }
204
205 impl FindUncommented for str {
206     fn find_uncommented(&self, pat: &str) -> Option<usize> {
207         let mut needle_iter = pat.chars();
208         for (kind, (i, b)) in CharClasses::new(self.char_indices()) {
209             match needle_iter.next() {
210                 None => {
211                     return Some(i - pat.len());
212                 }
213                 Some(c) => {
214                     match kind {
215                         FullCodeCharKind::Normal if b == c => {}
216                         _ => {
217                             needle_iter = pat.chars();
218                         }
219                     }
220                 }
221             }
222         }
223
224         // Handle case where the pattern is a suffix of the search string
225         match needle_iter.next() {
226             Some(_) => None,
227             None => Some(self.len() - pat.len()),
228         }
229     }
230 }
231
232 // Returns the first byte position after the first comment. The given string
233 // is expected to be prefixed by a comment, including delimiters.
234 // Good: "/* /* inner */ outer */ code();"
235 // Bad:  "code(); // hello\n world!"
236 pub fn find_comment_end(s: &str) -> Option<usize> {
237     let mut iter = CharClasses::new(s.char_indices());
238     for (kind, (i, _c)) in &mut iter {
239         if kind == FullCodeCharKind::Normal {
240             return Some(i);
241         }
242     }
243
244     // Handle case where the comment ends at the end of s.
245     if iter.status == CharClassesStatus::Normal {
246         Some(s.len())
247     } else {
248         None
249     }
250 }
251
252 /// Returns true if text contains any comment.
253 pub fn contains_comment(text: &str) -> bool {
254     CharClasses::new(text.chars()).any(|(kind, _)| kind.is_comment())
255 }
256
257 struct CharClasses<T>
258     where T: Iterator,
259           T::Item: RichChar
260 {
261     base: iter::Peekable<T>,
262     status: CharClassesStatus,
263 }
264
265 trait RichChar {
266     fn get_char(&self) -> char;
267 }
268
269 impl RichChar for char {
270     fn get_char(&self) -> char {
271         *self
272     }
273 }
274
275 impl RichChar for (usize, char) {
276     fn get_char(&self) -> char {
277         self.1
278     }
279 }
280
281 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
282 enum CharClassesStatus {
283     Normal,
284     LitString,
285     LitStringEscape,
286     LitChar,
287     LitCharEscape,
288     // The u32 is the nesting deepness of the comment
289     BlockComment(u32),
290     // Status when the '/' has been consumed, but not yet the '*', deepness is
291     // the new deepness (after the comment opening).
292     BlockCommentOpening(u32),
293     // Status when the '*' has been consumed, but not yet the '/', deepness is
294     // the new deepness (after the comment closing).
295     BlockCommentClosing(u32),
296     LineComment,
297 }
298
299 /// Distinguish between functional part of code and comments
300 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
301 pub enum CodeCharKind {
302     Normal,
303     Comment,
304 }
305
306 /// Distinguish between functional part of code and comments,
307 /// describing opening and closing of comments for ease when chunking
308 /// code from tagged characters
309 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
310 enum FullCodeCharKind {
311     Normal,
312     /// The first character of a comment, there is only one for a comment (always '/')
313     StartComment,
314     /// Any character inside a comment including the second character of comment
315     /// marks ("//", "/*")
316     InComment,
317     /// Last character of a comment, '\n' for a line comment, '/' for a block comment.
318     EndComment,
319 }
320
321 impl FullCodeCharKind {
322     fn is_comment(&self) -> bool {
323         match *self {
324             FullCodeCharKind::Normal => false,
325             FullCodeCharKind::StartComment |
326             FullCodeCharKind::InComment |
327             FullCodeCharKind::EndComment => true,
328         }
329     }
330
331     fn to_codecharkind(&self) -> CodeCharKind {
332         if self.is_comment() {
333             CodeCharKind::Comment
334         } else {
335             CodeCharKind::Normal
336         }
337     }
338 }
339
340 impl<T> CharClasses<T>
341     where T: Iterator,
342           T::Item: RichChar
343 {
344     fn new(base: T) -> CharClasses<T> {
345         CharClasses {
346             base: base.peekable(),
347             status: CharClassesStatus::Normal,
348         }
349     }
350 }
351
352 impl<T> Iterator for CharClasses<T>
353     where T: Iterator,
354           T::Item: RichChar
355 {
356     type Item = (FullCodeCharKind, T::Item);
357
358     fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> {
359         let item = try_opt!(self.base.next());
360         let chr = item.get_char();
361         self.status = match self.status {
362             CharClassesStatus::LitString => {
363                 match chr {
364                     '"' => CharClassesStatus::Normal,
365                     '\\' => CharClassesStatus::LitStringEscape,
366                     _ => CharClassesStatus::LitString,
367                 }
368             }
369             CharClassesStatus::LitStringEscape => CharClassesStatus::LitString,
370             CharClassesStatus::LitChar => {
371                 match chr {
372                     '\\' => CharClassesStatus::LitCharEscape,
373                     '\'' => CharClassesStatus::Normal,
374                     _ => CharClassesStatus::LitChar,
375                 }
376             }
377             CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar,
378             CharClassesStatus::Normal => {
379                 match chr {
380                     '"' => CharClassesStatus::LitString,
381                     '\'' => CharClassesStatus::LitChar,
382                     '/' => {
383                         match self.base.peek() {
384                             Some(next) if next.get_char() == '*' => {
385                                 self.status = CharClassesStatus::BlockCommentOpening(1);
386                                 return Some((FullCodeCharKind::StartComment, item));
387                             }
388                             Some(next) if next.get_char() == '/' => {
389                                 self.status = CharClassesStatus::LineComment;
390                                 return Some((FullCodeCharKind::StartComment, item));
391                             }
392                             _ => CharClassesStatus::Normal,
393                         }
394                     }
395                     _ => CharClassesStatus::Normal,
396                 }
397             }
398             CharClassesStatus::BlockComment(deepness) => {
399                 assert!(deepness != 0);
400                 self.status = match self.base.peek() {
401                     Some(next) if next.get_char() == '/' && chr == '*' => {
402                         CharClassesStatus::BlockCommentClosing(deepness - 1)
403                     }
404                     Some(next) if next.get_char() == '*' && chr == '/' => {
405                         CharClassesStatus::BlockCommentOpening(deepness + 1)
406                     }
407                     _ => CharClassesStatus::BlockComment(deepness),
408                 };
409                 return Some((FullCodeCharKind::InComment, item));
410             }
411             CharClassesStatus::BlockCommentOpening(deepness) => {
412                 assert_eq!(chr, '*');
413                 self.status = CharClassesStatus::BlockComment(deepness);
414                 return Some((FullCodeCharKind::InComment, item));
415             }
416             CharClassesStatus::BlockCommentClosing(deepness) => {
417                 assert_eq!(chr, '/');
418                 if deepness == 0 {
419                     self.status = CharClassesStatus::Normal;
420                     return Some((FullCodeCharKind::EndComment, item));
421                 } else {
422                     self.status = CharClassesStatus::BlockComment(deepness);
423                     return Some((FullCodeCharKind::InComment, item));
424                 }
425             }
426             CharClassesStatus::LineComment => {
427                 match chr {
428                     '\n' => {
429                         self.status = CharClassesStatus::Normal;
430                         return Some((FullCodeCharKind::EndComment, item));
431                     }
432                     _ => {
433                         self.status = CharClassesStatus::LineComment;
434                         return Some((FullCodeCharKind::InComment, item));
435                     }
436                 }
437             }
438         };
439         Some((FullCodeCharKind::Normal, item))
440     }
441 }
442
443 /// Iterator over functional and commented parts of a string. Any part of a string is either
444 /// functional code, either *one* block comment, either *one* line comment. Whitespace between
445 /// comments is functional code. Line comments contain their ending newlines.
446 struct UngroupedCommentCodeSlices<'a> {
447     slice: &'a str,
448     iter: iter::Peekable<CharClasses<std::str::CharIndices<'a>>>,
449 }
450
451 impl<'a> UngroupedCommentCodeSlices<'a> {
452     fn new(code: &'a str) -> UngroupedCommentCodeSlices<'a> {
453         UngroupedCommentCodeSlices {
454             slice: code,
455             iter: CharClasses::new(code.char_indices()).peekable(),
456         }
457     }
458 }
459
460 impl<'a> Iterator for UngroupedCommentCodeSlices<'a> {
461     type Item = (CodeCharKind, usize, &'a str);
462
463     fn next(&mut self) -> Option<Self::Item> {
464         let (kind, (start_idx, _)) = try_opt!(self.iter.next());
465         match kind {
466             FullCodeCharKind::Normal => {
467                 // Consume all the Normal code
468                 while let Some(&(FullCodeCharKind::Normal, (_, _))) = self.iter.peek() {
469                     let _ = self.iter.next();
470                 }
471             }
472             FullCodeCharKind::StartComment => {
473                 // Consume the whole comment
474                 while let Some((FullCodeCharKind::InComment, (_, _))) = self.iter.next() {}
475             }
476             _ => panic!(),
477         }
478         let slice = match self.iter.peek() {
479             Some(&(_, (end_idx, _))) => &self.slice[start_idx..end_idx],
480             None => &self.slice[start_idx..],
481         };
482         Some((if kind.is_comment() {
483                   CodeCharKind::Comment
484               } else {
485                   CodeCharKind::Normal
486               },
487               start_idx,
488               slice))
489     }
490 }
491
492
493
494
495 /// Iterator over an alternating sequence of functional and commented parts of
496 /// a string. The first item is always a, possibly zero length, subslice of
497 /// functional text. Line style comments contain their ending newlines.
498 pub struct CommentCodeSlices<'a> {
499     slice: &'a str,
500     last_slice_kind: CodeCharKind,
501     last_slice_end: usize,
502 }
503
504 impl<'a> CommentCodeSlices<'a> {
505     pub fn new(slice: &'a str) -> CommentCodeSlices<'a> {
506         CommentCodeSlices {
507             slice: slice,
508             last_slice_kind: CodeCharKind::Comment,
509             last_slice_end: 0,
510         }
511     }
512 }
513
514 impl<'a> Iterator for CommentCodeSlices<'a> {
515     type Item = (CodeCharKind, usize, &'a str);
516
517     fn next(&mut self) -> Option<Self::Item> {
518         if self.last_slice_end == self.slice.len() {
519             return None;
520         }
521
522         let mut sub_slice_end = self.last_slice_end;
523         let mut first_whitespace = None;
524         let subslice = &self.slice[self.last_slice_end..];
525         let mut iter = CharClasses::new(subslice.char_indices());
526
527         for (kind, (i, c)) in &mut iter {
528             let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal &&
529                                        &subslice[..2] == "//" &&
530                                        [' ', '\t'].contains(&c);
531
532             if is_comment_connector && first_whitespace.is_none() {
533                 first_whitespace = Some(i);
534             }
535
536             if kind.to_codecharkind() == self.last_slice_kind && !is_comment_connector {
537                 let last_index = match first_whitespace {
538                     Some(j) => j,
539                     None => i,
540                 };
541                 sub_slice_end = self.last_slice_end + last_index;
542                 break;
543             }
544
545             if !is_comment_connector {
546                 first_whitespace = None;
547             }
548         }
549
550         if let (None, true) = (iter.next(), sub_slice_end == self.last_slice_end) {
551             // This was the last subslice.
552             sub_slice_end = match first_whitespace {
553                 Some(i) => self.last_slice_end + i,
554                 None => self.slice.len(),
555             };
556         }
557
558         let kind = match self.last_slice_kind {
559             CodeCharKind::Comment => CodeCharKind::Normal,
560             CodeCharKind::Normal => CodeCharKind::Comment,
561         };
562         let res = (kind, self.last_slice_end, &self.slice[self.last_slice_end..sub_slice_end]);
563         self.last_slice_end = sub_slice_end;
564         self.last_slice_kind = kind;
565
566         Some(res)
567     }
568 }
569
570 /// Checks is `new` didn't miss any comment from `span`, if it removed any, return previous text
571 /// (if it fits in the width/offset, else return None), else return `new`
572 pub fn recover_comment_removed(new: String,
573                                span: Span,
574                                context: &RewriteContext,
575                                shape: Shape)
576                                -> Option<String> {
577     let snippet = context.snippet(span);
578     if changed_comment_content(&snippet, &new) {
579         // We missed some comments
580         // Keep previous formatting if it satisfies the constrains
581         wrap_str(snippet, context.config.max_width, shape)
582     } else {
583         Some(new)
584     }
585 }
586
587 /// Return true if the two strings of code have the same payload of comments.
588 /// The payload of comments is everything in the string except:
589 ///     - actual code (not comments)
590 ///     - comment start/end marks
591 ///     - whitespace
592 ///     - '*' at the beginning of lines in block comments
593 fn changed_comment_content(orig: &str, new: &str) -> bool {
594     // Cannot write this as a fn since we cannot return types containing closures
595     let code_comment_content = |code| {
596         let slices = UngroupedCommentCodeSlices::new(code);
597         slices.filter(|&(ref kind, _, _)| *kind == CodeCharKind::Comment)
598             .flat_map(|(_, _, s)| CommentReducer::new(s))
599     };
600     let res = code_comment_content(orig).ne(code_comment_content(new));
601     debug!("comment::changed_comment_content: {}\norig: '{}'\nnew: '{}'\nraw_old: {}\nraw_new: {}",
602            res,
603            orig,
604            new,
605            code_comment_content(orig).collect::<String>(),
606            code_comment_content(new).collect::<String>());
607     res
608 }
609
610
611 /// Iterator over the 'payload' characters of a comment.
612 /// It skips whitespace, comment start/end marks, and '*' at the beginning of lines.
613 /// The comment must be one comment, ie not more than one start mark (no multiple line comments,
614 /// for example).
615 struct CommentReducer<'a> {
616     is_block: bool,
617     at_start_line: bool,
618     iter: std::str::Chars<'a>,
619 }
620
621 impl<'a> CommentReducer<'a> {
622     fn new(comment: &'a str) -> CommentReducer<'a> {
623         let is_block = comment.starts_with("/*");
624         let comment = remove_comment_header(comment);
625         CommentReducer {
626             is_block: is_block,
627             at_start_line: false, // There are no supplementary '*' on the first line
628             iter: comment.chars(),
629         }
630     }
631 }
632
633 impl<'a> Iterator for CommentReducer<'a> {
634     type Item = char;
635     fn next(&mut self) -> Option<Self::Item> {
636         loop {
637             let mut c = try_opt!(self.iter.next());
638             if self.is_block && self.at_start_line {
639                 while c.is_whitespace() {
640                     c = try_opt!(self.iter.next());
641                 }
642                 // Ignore leading '*'
643                 if c == '*' {
644                     c = try_opt!(self.iter.next());
645                 }
646             } else {
647                 if c == '\n' {
648                     self.at_start_line = true;
649                 }
650             }
651             if !c.is_whitespace() {
652                 return Some(c);
653             }
654         }
655     }
656 }
657
658
659 fn remove_comment_header(comment: &str) -> &str {
660     if comment.starts_with("///") || comment.starts_with("//!") {
661         &comment[3..]
662     } else if comment.starts_with("//") {
663         &comment[2..]
664     } else if (comment.starts_with("/**") && !comment.starts_with("/**/")) ||
665               comment.starts_with("/*!") {
666         &comment[3..comment.len() - 2]
667     } else {
668         assert!(comment.starts_with("/*"),
669                 format!("string '{}' is not a comment", comment));
670         &comment[2..comment.len() - 2]
671     }
672 }
673
674 #[cfg(test)]
675 mod test {
676     use super::{CharClasses, CodeCharKind, FullCodeCharKind, contains_comment, rewrite_comment,
677                 FindUncommented, CommentCodeSlices};
678     use {Indent, Shape};
679
680     #[test]
681     fn char_classes() {
682         let mut iter = CharClasses::new("//\n\n".chars());
683
684         assert_eq!((FullCodeCharKind::StartComment, '/'), iter.next().unwrap());
685         assert_eq!((FullCodeCharKind::InComment, '/'), iter.next().unwrap());
686         assert_eq!((FullCodeCharKind::EndComment, '\n'), iter.next().unwrap());
687         assert_eq!((FullCodeCharKind::Normal, '\n'), iter.next().unwrap());
688         assert_eq!(None, iter.next());
689     }
690
691     #[test]
692     fn comment_code_slices() {
693         let input = "code(); /* test */ 1 + 1";
694         let mut iter = CommentCodeSlices::new(input);
695
696         assert_eq!((CodeCharKind::Normal, 0, "code(); "), iter.next().unwrap());
697         assert_eq!((CodeCharKind::Comment, 8, "/* test */"),
698                    iter.next().unwrap());
699         assert_eq!((CodeCharKind::Normal, 18, " 1 + 1"), iter.next().unwrap());
700         assert_eq!(None, iter.next());
701     }
702
703     #[test]
704     fn comment_code_slices_two() {
705         let input = "// comment\n    test();";
706         let mut iter = CommentCodeSlices::new(input);
707
708         assert_eq!((CodeCharKind::Normal, 0, ""), iter.next().unwrap());
709         assert_eq!((CodeCharKind::Comment, 0, "// comment\n"),
710                    iter.next().unwrap());
711         assert_eq!((CodeCharKind::Normal, 11, "    test();"),
712                    iter.next().unwrap());
713         assert_eq!(None, iter.next());
714     }
715
716     #[test]
717     fn comment_code_slices_three() {
718         let input = "1 // comment\n    // comment2\n\n";
719         let mut iter = CommentCodeSlices::new(input);
720
721         assert_eq!((CodeCharKind::Normal, 0, "1 "), iter.next().unwrap());
722         assert_eq!((CodeCharKind::Comment, 2, "// comment\n    // comment2\n"),
723                    iter.next().unwrap());
724         assert_eq!((CodeCharKind::Normal, 29, "\n"), iter.next().unwrap());
725         assert_eq!(None, iter.next());
726     }
727
728     #[test]
729     #[cfg_attr(rustfmt, rustfmt_skip)]
730     fn format_comments() {
731         let mut config: ::config::Config = Default::default();
732         config.wrap_comments = true;
733         config.normalize_comments = true;
734
735         let comment = rewrite_comment(" //test",
736                                       true,
737                                       Shape::legacy(100, Indent::new(0, 100)),
738                                       &config).unwrap();
739         assert_eq!("/* test */", comment);
740
741         let comment = rewrite_comment("// comment on a",
742                                       false,
743                                       Shape::legacy(10, Indent::empty()),
744                                       &config).unwrap();
745         assert_eq!("// comment\n// on a", comment);
746
747         let comment = rewrite_comment("//  A multi line comment\n             // between args.",
748                                       false,
749                                       Shape::legacy(60, Indent::new(0, 12)),
750                                       &config).unwrap();
751         assert_eq!("//  A multi line comment\n            // between args.", comment);
752
753         let input = "// comment";
754         let expected =
755             "/* comment */";
756         let comment = rewrite_comment(input,
757                                       true,
758                                       Shape::legacy(9, Indent::new(0, 69)),
759                                       &config).unwrap();
760         assert_eq!(expected, comment);
761
762         let comment = rewrite_comment("/*   trimmed    */",
763                                       true,
764                                       Shape::legacy(100, Indent::new(0, 100)),
765                                       &config).unwrap();
766         assert_eq!("/* trimmed */", comment);
767     }
768
769     // This is probably intended to be a non-test fn, but it is not used. I'm
770     // keeping it around unless it helps us test stuff.
771     fn uncommented(text: &str) -> String {
772         CharClasses::new(text.chars())
773             .filter_map(|(s, c)| match s {
774                 FullCodeCharKind::Normal => Some(c),
775                 _ => None,
776             })
777             .collect()
778     }
779
780     #[test]
781     fn test_uncommented() {
782         assert_eq!(&uncommented("abc/*...*/"), "abc");
783         assert_eq!(&uncommented("// .... /* \n../* /* *** / */ */a/* // */c\n"),
784                    "..ac\n");
785         assert_eq!(&uncommented("abc \" /* */\" qsdf"), "abc \" /* */\" qsdf");
786     }
787
788     #[test]
789     fn test_contains_comment() {
790         assert_eq!(contains_comment("abc"), false);
791         assert_eq!(contains_comment("abc // qsdf"), true);
792         assert_eq!(contains_comment("abc /* kqsdf"), true);
793         assert_eq!(contains_comment("abc \" /* */\" qsdf"), false);
794     }
795
796     #[test]
797     fn test_find_uncommented() {
798         fn check(haystack: &str, needle: &str, expected: Option<usize>) {
799             assert_eq!(expected, haystack.find_uncommented(needle));
800         }
801
802         check("/*/ */test", "test", Some(6));
803         check("//test\ntest", "test", Some(7));
804         check("/* comment only */", "whatever", None);
805         check("/* comment */ some text /* more commentary */ result",
806               "result",
807               Some(46));
808         check("sup // sup", "p", Some(2));
809         check("sup", "x", None);
810         check(r#"π? /**/ π is nice!"#, r#"π is nice"#, Some(9));
811         check("/*sup yo? \n sup*/ sup", "p", Some(20));
812         check("hel/*lohello*/lo", "hello", None);
813         check("acb", "ab", None);
814         check(",/*A*/ ", ",", Some(0));
815         check("abc", "abc", Some(0));
816         check("/* abc */", "abc", None);
817         check("/**/abc/* */", "abc", Some(4));
818         check("\"/* abc */\"", "abc", Some(4));
819         check("\"/* abc", "abc", Some(4));
820     }
821 }