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