]> git.lizzy.rs Git - rust.git/blob - rustfmt-core/src/comment.rs
Preserve trailing whitespaces only for doc comment
[rust.git] / rustfmt-core / 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 config::Config;
18 use rewrite::RewriteContext;
19 use shape::{Indent, Shape};
20 use string::{rewrite_string, StringFormat};
21 use utils::{count_newlines, first_line_width, last_line_width};
22
23 fn is_custom_comment(comment: &str) -> bool {
24     if !comment.starts_with("//") {
25         false
26     } else if let Some(c) = comment.chars().nth(2) {
27         !c.is_alphanumeric() && !c.is_whitespace()
28     } else {
29         false
30     }
31 }
32
33 #[derive(Copy, Clone, PartialEq, Eq)]
34 pub enum CommentStyle<'a> {
35     DoubleSlash,
36     TripleSlash,
37     Doc,
38     SingleBullet,
39     DoubleBullet,
40     Exclamation,
41     Custom(&'a str),
42 }
43
44 fn custom_opener(s: &str) -> &str {
45     s.lines().next().map_or("", |first_line| {
46         first_line
47             .find(' ')
48             .map_or(first_line, |space_index| &first_line[0..space_index + 1])
49     })
50 }
51
52 impl<'a> CommentStyle<'a> {
53     pub fn opener(&self) -> &'a str {
54         match *self {
55             CommentStyle::DoubleSlash => "// ",
56             CommentStyle::TripleSlash => "/// ",
57             CommentStyle::Doc => "//! ",
58             CommentStyle::SingleBullet => "/* ",
59             CommentStyle::DoubleBullet => "/** ",
60             CommentStyle::Exclamation => "/*! ",
61             CommentStyle::Custom(opener) => opener,
62         }
63     }
64
65     pub fn closer(&self) -> &'a str {
66         match *self {
67             CommentStyle::DoubleSlash
68             | CommentStyle::TripleSlash
69             | CommentStyle::Custom(..)
70             | CommentStyle::Doc => "",
71             CommentStyle::DoubleBullet => " **/",
72             CommentStyle::SingleBullet | CommentStyle::Exclamation => " */",
73         }
74     }
75
76     pub fn line_start(&self) -> &'a str {
77         match *self {
78             CommentStyle::DoubleSlash => "// ",
79             CommentStyle::TripleSlash => "/// ",
80             CommentStyle::Doc => "//! ",
81             CommentStyle::SingleBullet | CommentStyle::Exclamation => " * ",
82             CommentStyle::DoubleBullet => " ** ",
83             CommentStyle::Custom(opener) => opener,
84         }
85     }
86
87     pub fn to_str_tuplet(&self) -> (&'a str, &'a str, &'a str) {
88         (self.opener(), self.closer(), self.line_start())
89     }
90
91     pub fn line_with_same_comment_style(&self, line: &str, normalize_comments: bool) -> bool {
92         match *self {
93             CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => {
94                 line.trim_left().starts_with(self.line_start().trim_left())
95                     || comment_style(line, normalize_comments) == *self
96             }
97             CommentStyle::DoubleBullet | CommentStyle::SingleBullet | CommentStyle::Exclamation => {
98                 line.trim_left().starts_with(self.closer().trim_left())
99                     || line.trim_left().starts_with(self.line_start().trim_left())
100                     || comment_style(line, normalize_comments) == *self
101             }
102             CommentStyle::Custom(opener) => line.trim_left().starts_with(opener.trim_right()),
103         }
104     }
105 }
106
107 fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle {
108     if !normalize_comments {
109         if orig.starts_with("/**") && !orig.starts_with("/**/") {
110             CommentStyle::DoubleBullet
111         } else if orig.starts_with("/*!") {
112             CommentStyle::Exclamation
113         } else if orig.starts_with("/*") {
114             CommentStyle::SingleBullet
115         } else if orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/') {
116             CommentStyle::TripleSlash
117         } else if orig.starts_with("//!") {
118             CommentStyle::Doc
119         } else if is_custom_comment(orig) {
120             CommentStyle::Custom(custom_opener(orig))
121         } else {
122             CommentStyle::DoubleSlash
123         }
124     } else if (orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/'))
125         || (orig.starts_with("/**") && !orig.starts_with("/**/"))
126     {
127         CommentStyle::TripleSlash
128     } else if orig.starts_with("//!") || orig.starts_with("/*!") {
129         CommentStyle::Doc
130     } else if is_custom_comment(orig) {
131         CommentStyle::Custom(custom_opener(orig))
132     } else {
133         CommentStyle::DoubleSlash
134     }
135 }
136
137 /// Combine `prev_str` and `next_str` into a single `String`. `span` may contain
138 /// comments between two strings. If there are such comments, then that will be
139 /// recovered. If `allow_extend` is true and there is no comment between the two
140 /// strings, then they will be put on a single line as long as doing so does not
141 /// exceed max width.
142 pub fn combine_strs_with_missing_comments(
143     context: &RewriteContext,
144     prev_str: &str,
145     next_str: &str,
146     span: Span,
147     shape: Shape,
148     allow_extend: bool,
149 ) -> Option<String> {
150     let mut allow_one_line = !prev_str.contains('\n') && !next_str.contains('\n');
151     let first_sep = if prev_str.is_empty() || next_str.is_empty() {
152         ""
153     } else {
154         " "
155     };
156     let mut one_line_width =
157         last_line_width(prev_str) + first_line_width(next_str) + first_sep.len();
158
159     let indent_str = shape.indent.to_string(context.config);
160     let missing_comment = rewrite_missing_comment(span, shape, context)?;
161
162     if missing_comment.is_empty() {
163         if allow_extend && prev_str.len() + first_sep.len() + next_str.len() <= shape.width {
164             return Some(format!("{}{}{}", prev_str, first_sep, next_str));
165         } else {
166             let sep = if prev_str.is_empty() {
167                 String::new()
168             } else {
169                 String::from("\n") + &indent_str
170             };
171             return Some(format!("{}{}{}", prev_str, sep, next_str));
172         }
173     }
174
175     // We have a missing comment between the first expression and the second expression.
176
177     // Peek the the original source code and find out whether there is a newline between the first
178     // expression and the second expression or the missing comment. We will preserve the original
179     // layout whenever possible.
180     let original_snippet = context.snippet(span);
181     let prefer_same_line = if let Some(pos) = original_snippet.chars().position(|c| c == '/') {
182         !original_snippet[..pos].contains('\n')
183     } else {
184         !original_snippet.contains('\n')
185     };
186
187     one_line_width -= first_sep.len();
188     let first_sep = if prev_str.is_empty() || missing_comment.is_empty() {
189         String::new()
190     } else {
191         let one_line_width = last_line_width(prev_str) + first_line_width(&missing_comment) + 1;
192         if prefer_same_line && one_line_width <= shape.width {
193             String::from(" ")
194         } else {
195             format!("\n{}", indent_str)
196         }
197     };
198     let second_sep = if missing_comment.is_empty() || next_str.is_empty() {
199         String::new()
200     } else if missing_comment.starts_with("//") {
201         format!("\n{}", indent_str)
202     } else {
203         one_line_width += missing_comment.len() + first_sep.len() + 1;
204         allow_one_line &= !missing_comment.starts_with("//") && !missing_comment.contains('\n');
205         if prefer_same_line && allow_one_line && one_line_width <= shape.width {
206             String::from(" ")
207         } else {
208             format!("\n{}", indent_str)
209         }
210     };
211     Some(format!(
212         "{}{}{}{}{}",
213         prev_str, first_sep, missing_comment, second_sep, next_str,
214     ))
215 }
216
217 pub fn rewrite_doc_comment(orig: &str, shape: Shape, config: &Config) -> Option<String> {
218     _rewrite_comment(orig, false, shape, config, true)
219 }
220
221 pub fn rewrite_comment(
222     orig: &str,
223     block_style: bool,
224     shape: Shape,
225     config: &Config,
226 ) -> Option<String> {
227     _rewrite_comment(orig, block_style, shape, config, false)
228 }
229
230 fn _rewrite_comment(
231     orig: &str,
232     block_style: bool,
233     shape: Shape,
234     config: &Config,
235     is_doc_comment: bool,
236 ) -> Option<String> {
237     // If there are lines without a starting sigil, we won't format them correctly
238     // so in that case we won't even re-align (if !config.normalize_comments()) and
239     // we should stop now.
240     let num_bare_lines = orig.lines()
241         .map(|line| line.trim())
242         .filter(|l| !(l.starts_with('*') || l.starts_with("//") || l.starts_with("/*")))
243         .count();
244     if num_bare_lines > 0 && !config.normalize_comments() {
245         return Some(orig.to_owned());
246     }
247     if !config.normalize_comments() && !config.wrap_comments() {
248         return light_rewrite_comment(orig, shape.indent, config, is_doc_comment);
249     }
250
251     identify_comment(orig, block_style, shape, config)
252 }
253
254 fn identify_comment(
255     orig: &str,
256     block_style: bool,
257     shape: Shape,
258     config: &Config,
259 ) -> Option<String> {
260     let style = comment_style(orig, false);
261     let first_group = orig.lines()
262         .take_while(|l| style.line_with_same_comment_style(l, false))
263         .collect::<Vec<_>>()
264         .join("\n");
265     let rest = orig.lines()
266         .skip(first_group.lines().count())
267         .collect::<Vec<_>>()
268         .join("\n");
269
270     let first_group_str = rewrite_comment_inner(&first_group, block_style, style, shape, config)?;
271     if rest.is_empty() {
272         Some(first_group_str)
273     } else {
274         identify_comment(&rest, block_style, shape, config).map(|rest_str| {
275             format!(
276                 "{}\n{}{}",
277                 first_group_str,
278                 shape.indent.to_string(config),
279                 rest_str
280             )
281         })
282     }
283 }
284
285 fn rewrite_comment_inner(
286     orig: &str,
287     block_style: bool,
288     style: CommentStyle,
289     shape: Shape,
290     config: &Config,
291 ) -> Option<String> {
292     let (opener, closer, line_start) = if block_style {
293         CommentStyle::SingleBullet.to_str_tuplet()
294     } else {
295         comment_style(orig, config.normalize_comments()).to_str_tuplet()
296     };
297
298     let max_chars = shape
299         .width
300         .checked_sub(closer.len() + opener.len())
301         .unwrap_or(1);
302     let indent_str = shape.indent.to_string(config);
303     let fmt_indent = shape.indent + (opener.len() - line_start.len());
304     let mut fmt = StringFormat {
305         opener: "",
306         closer: "",
307         line_start,
308         line_end: "",
309         shape: Shape::legacy(max_chars, fmt_indent),
310         trim_end: true,
311         config,
312     };
313
314     let line_breaks = count_newlines(orig.trim_right());
315     let lines = orig.lines()
316         .enumerate()
317         .map(|(i, mut line)| {
318             line = line.trim();
319             // Drop old closer.
320             if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
321                 line = line[..(line.len() - 2)].trim_right();
322             }
323
324             line
325         })
326         .map(|s| left_trim_comment_line(s, &style))
327         .map(|(line, has_leading_whitespace)| {
328             if orig.starts_with("/*") && line_breaks == 0 {
329                 (
330                     line.trim_left(),
331                     has_leading_whitespace || config.normalize_comments(),
332                 )
333             } else {
334                 (line, has_leading_whitespace || config.normalize_comments())
335             }
336         });
337
338     let mut result = String::with_capacity(orig.len() * 2);
339     result.push_str(opener);
340     let mut code_block_buffer = String::with_capacity(128);
341     let mut is_prev_line_multi_line = false;
342     let mut inside_code_block = false;
343     let comment_line_separator = format!("\n{}{}", indent_str, line_start);
344     let join_code_block_with_comment_line_separator = |s: &str| {
345         let mut result = String::with_capacity(s.len() + 128);
346         let mut iter = s.lines().peekable();
347         while let Some(line) = iter.next() {
348             result.push_str(line);
349             result.push_str(match iter.peek() {
350                 Some(next_line) if next_line.is_empty() => comment_line_separator.trim_right(),
351                 Some(..) => &comment_line_separator,
352                 None => "",
353             });
354         }
355         result
356     };
357
358     for (i, (line, has_leading_whitespace)) in lines.enumerate() {
359         let is_last = i == count_newlines(orig);
360
361         if inside_code_block {
362             if line.starts_with("```") {
363                 inside_code_block = false;
364                 result.push_str(&comment_line_separator);
365                 let code_block = ::format_code_block(&code_block_buffer, config)
366                     .unwrap_or_else(|| code_block_buffer.to_owned());
367                 result.push_str(&join_code_block_with_comment_line_separator(&code_block));
368                 code_block_buffer.clear();
369                 result.push_str(&comment_line_separator);
370                 result.push_str(line);
371             } else {
372                 code_block_buffer.push_str(line);
373                 code_block_buffer.push('\n');
374             }
375
376             continue;
377         } else {
378             inside_code_block = line.starts_with("```");
379
380             if result == opener {
381                 let force_leading_whitespace = opener == "/* " && count_newlines(orig) == 0;
382                 if !has_leading_whitespace && !force_leading_whitespace && result.ends_with(' ') {
383                     result.pop();
384                 }
385                 if line.is_empty() {
386                     continue;
387                 }
388             } else if is_prev_line_multi_line && !line.is_empty() {
389                 result.push(' ')
390             } else if is_last && !closer.is_empty() && line.is_empty() {
391                 result.push('\n');
392                 result.push_str(&indent_str);
393             } else {
394                 result.push_str(&comment_line_separator);
395                 if !has_leading_whitespace && result.ends_with(' ') {
396                     result.pop();
397                 }
398             }
399         }
400
401         if config.wrap_comments() && line.len() > fmt.shape.width && !has_url(line) {
402             match rewrite_string(line, &fmt, Some(max_chars)) {
403                 Some(ref s) => {
404                     is_prev_line_multi_line = s.contains('\n');
405                     result.push_str(s);
406                 }
407                 None if is_prev_line_multi_line => {
408                     // We failed to put the current `line` next to the previous `line`.
409                     // Remove the trailing space, then start rewrite on the next line.
410                     result.pop();
411                     result.push_str(&comment_line_separator);
412                     fmt.shape = Shape::legacy(max_chars, fmt_indent);
413                     match rewrite_string(line, &fmt, Some(max_chars)) {
414                         Some(ref s) => {
415                             is_prev_line_multi_line = s.contains('\n');
416                             result.push_str(s);
417                         }
418                         None => {
419                             is_prev_line_multi_line = false;
420                             result.push_str(line);
421                         }
422                     }
423                 }
424                 None => {
425                     is_prev_line_multi_line = false;
426                     result.push_str(line);
427                 }
428             }
429
430             fmt.shape = if is_prev_line_multi_line {
431                 // 1 = " "
432                 let offset = 1 + last_line_width(&result) - line_start.len();
433                 Shape {
434                     width: max_chars.checked_sub(offset).unwrap_or(0),
435                     indent: fmt_indent,
436                     offset: fmt.shape.offset + offset,
437                 }
438             } else {
439                 Shape::legacy(max_chars, fmt_indent)
440             };
441         } else {
442             if line.is_empty() && result.ends_with(' ') && !is_last {
443                 // Remove space if this is an empty comment or a doc comment.
444                 result.pop();
445             }
446             result.push_str(line);
447             fmt.shape = Shape::legacy(max_chars, fmt_indent);
448             is_prev_line_multi_line = false;
449         }
450     }
451
452     result.push_str(closer);
453     if result == opener && result.ends_with(' ') {
454         // Trailing space.
455         result.pop();
456     }
457
458     Some(result)
459 }
460
461 /// Returns true if the given string MAY include URLs or alike.
462 fn has_url(s: &str) -> bool {
463     // This function may return false positive, but should get its job done in most cases.
464     s.contains("https://") || s.contains("http://") || s.contains("ftp://") || s.contains("file://")
465 }
466
467 /// Given the span, rewrite the missing comment inside it if available.
468 /// Note that the given span must only include comments (or leading/trailing whitespaces).
469 pub fn rewrite_missing_comment(
470     span: Span,
471     shape: Shape,
472     context: &RewriteContext,
473 ) -> Option<String> {
474     let missing_snippet = context.snippet(span);
475     let trimmed_snippet = missing_snippet.trim();
476     if !trimmed_snippet.is_empty() {
477         rewrite_comment(trimmed_snippet, false, shape, context.config)
478     } else {
479         Some(String::new())
480     }
481 }
482
483 /// Recover the missing comments in the specified span, if available.
484 /// The layout of the comments will be preserved as long as it does not break the code
485 /// and its total width does not exceed the max width.
486 pub fn recover_missing_comment_in_span(
487     span: Span,
488     shape: Shape,
489     context: &RewriteContext,
490     used_width: usize,
491 ) -> Option<String> {
492     let missing_comment = rewrite_missing_comment(span, shape, context)?;
493     if missing_comment.is_empty() {
494         Some(String::new())
495     } else {
496         let missing_snippet = context.snippet(span);
497         let pos = missing_snippet.chars().position(|c| c == '/').unwrap_or(0);
498         // 1 = ` `
499         let total_width = missing_comment.len() + used_width + 1;
500         let force_new_line_before_comment =
501             missing_snippet[..pos].contains('\n') || total_width > context.config.max_width();
502         let sep = if force_new_line_before_comment {
503             format!("\n{}", shape.indent.to_string(context.config))
504         } else {
505             String::from(" ")
506         };
507         Some(format!("{}{}", sep, missing_comment))
508     }
509 }
510
511 /// Trim trailing whitespaces unless they consist of two whitespaces.
512 fn trim_right_unless_two_whitespaces(s: &str, is_doc_comment: bool) -> &str {
513     if is_doc_comment && s.ends_with("  ")
514         && !s.chars().rev().nth(2).map_or(true, char::is_whitespace)
515     {
516         s
517     } else {
518         s.trim_right()
519     }
520 }
521
522 /// Trims whitespace and aligns to indent, but otherwise does not change comments.
523 fn light_rewrite_comment(
524     orig: &str,
525     offset: Indent,
526     config: &Config,
527     is_doc_comment: bool,
528 ) -> Option<String> {
529     let lines: Vec<&str> = orig.lines()
530         .map(|l| {
531             // This is basically just l.trim(), but in the case that a line starts
532             // with `*` we want to leave one space before it, so it aligns with the
533             // `*` in `/*`.
534             let first_non_whitespace = l.find(|c| !char::is_whitespace(c));
535             let left_trimmed = if let Some(fnw) = first_non_whitespace {
536                 if l.as_bytes()[fnw] == b'*' && fnw > 0 {
537                     &l[fnw - 1..]
538                 } else {
539                     &l[fnw..]
540                 }
541             } else {
542                 ""
543             };
544             // Preserve markdown's double-space line break syntax.
545             trim_right_unless_two_whitespaces(left_trimmed, is_doc_comment)
546         })
547         .collect();
548     Some(lines.join(&format!("\n{}", offset.to_string(config))))
549 }
550
551 /// Trims comment characters and possibly a single space from the left of a string.
552 /// Does not trim all whitespace. If a single space is trimmed from the left of the string,
553 /// this function returns true.
554 fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle) -> (&'a str, bool) {
555     if line.starts_with("//! ") || line.starts_with("/// ") || line.starts_with("/*! ")
556         || line.starts_with("/** ")
557     {
558         (&line[4..], true)
559     } else if let CommentStyle::Custom(opener) = *style {
560         if line.starts_with(opener) {
561             (&line[opener.len()..], true)
562         } else {
563             (&line[opener.trim_right().len()..], false)
564         }
565     } else if line.starts_with("/* ") || line.starts_with("// ") || line.starts_with("//!")
566         || line.starts_with("///") || line.starts_with("** ")
567         || line.starts_with("/*!")
568         || (line.starts_with("/**") && !line.starts_with("/**/"))
569     {
570         (&line[3..], line.chars().nth(2).unwrap() == ' ')
571     } else if line.starts_with("/*") || line.starts_with("* ") || line.starts_with("//")
572         || line.starts_with("**")
573     {
574         (&line[2..], line.chars().nth(1).unwrap() == ' ')
575     } else if line.starts_with('*') {
576         (&line[1..], false)
577     } else {
578         (line, line.starts_with(' '))
579     }
580 }
581
582 pub trait FindUncommented {
583     fn find_uncommented(&self, pat: &str) -> Option<usize>;
584 }
585
586 impl FindUncommented for str {
587     fn find_uncommented(&self, pat: &str) -> Option<usize> {
588         let mut needle_iter = pat.chars();
589         for (kind, (i, b)) in CharClasses::new(self.char_indices()) {
590             match needle_iter.next() {
591                 None => {
592                     return Some(i - pat.len());
593                 }
594                 Some(c) => match kind {
595                     FullCodeCharKind::Normal | FullCodeCharKind::InString if b == c => {}
596                     _ => {
597                         needle_iter = pat.chars();
598                     }
599                 },
600             }
601         }
602
603         // Handle case where the pattern is a suffix of the search string
604         match needle_iter.next() {
605             Some(_) => None,
606             None => Some(self.len() - pat.len()),
607         }
608     }
609 }
610
611 // Returns the first byte position after the first comment. The given string
612 // is expected to be prefixed by a comment, including delimiters.
613 // Good: "/* /* inner */ outer */ code();"
614 // Bad:  "code(); // hello\n world!"
615 pub fn find_comment_end(s: &str) -> Option<usize> {
616     let mut iter = CharClasses::new(s.char_indices());
617     for (kind, (i, _c)) in &mut iter {
618         if kind == FullCodeCharKind::Normal || kind == FullCodeCharKind::InString {
619             return Some(i);
620         }
621     }
622
623     // Handle case where the comment ends at the end of s.
624     if iter.status == CharClassesStatus::Normal {
625         Some(s.len())
626     } else {
627         None
628     }
629 }
630
631 /// Returns true if text contains any comment.
632 pub fn contains_comment(text: &str) -> bool {
633     CharClasses::new(text.chars()).any(|(kind, _)| kind.is_comment())
634 }
635
636 /// Remove trailing spaces from the specified snippet. We do not remove spaces
637 /// inside strings or comments.
638 pub fn remove_trailing_white_spaces(text: &str) -> String {
639     let mut buffer = String::with_capacity(text.len());
640     let mut space_buffer = String::with_capacity(128);
641     for (char_kind, c) in CharClasses::new(text.chars()) {
642         match c {
643             '\n' => {
644                 if char_kind == FullCodeCharKind::InString {
645                     buffer.push_str(&space_buffer);
646                 }
647                 space_buffer.clear();
648                 buffer.push('\n');
649             }
650             _ if c.is_whitespace() => {
651                 space_buffer.push(c);
652             }
653             _ => {
654                 if !space_buffer.is_empty() {
655                     buffer.push_str(&space_buffer);
656                     space_buffer.clear();
657                 }
658                 buffer.push(c);
659             }
660         }
661     }
662     buffer
663 }
664
665 pub struct CharClasses<T>
666 where
667     T: Iterator,
668     T::Item: RichChar,
669 {
670     base: iter::Peekable<T>,
671     status: CharClassesStatus,
672 }
673
674 pub trait RichChar {
675     fn get_char(&self) -> char;
676 }
677
678 impl RichChar for char {
679     fn get_char(&self) -> char {
680         *self
681     }
682 }
683
684 impl RichChar for (usize, char) {
685     fn get_char(&self) -> char {
686         self.1
687     }
688 }
689
690 impl RichChar for (char, usize) {
691     fn get_char(&self) -> char {
692         self.0
693     }
694 }
695
696 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
697 enum CharClassesStatus {
698     Normal,
699     LitString,
700     LitStringEscape,
701     LitChar,
702     LitCharEscape,
703     // The u32 is the nesting deepness of the comment
704     BlockComment(u32),
705     // Status when the '/' has been consumed, but not yet the '*', deepness is
706     // the new deepness (after the comment opening).
707     BlockCommentOpening(u32),
708     // Status when the '*' has been consumed, but not yet the '/', deepness is
709     // the new deepness (after the comment closing).
710     BlockCommentClosing(u32),
711     LineComment,
712 }
713
714 /// Distinguish between functional part of code and comments
715 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
716 pub enum CodeCharKind {
717     Normal,
718     Comment,
719 }
720
721 /// Distinguish between functional part of code and comments,
722 /// describing opening and closing of comments for ease when chunking
723 /// code from tagged characters
724 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
725 pub enum FullCodeCharKind {
726     Normal,
727     /// The first character of a comment, there is only one for a comment (always '/')
728     StartComment,
729     /// Any character inside a comment including the second character of comment
730     /// marks ("//", "/*")
731     InComment,
732     /// Last character of a comment, '\n' for a line comment, '/' for a block comment.
733     EndComment,
734     /// Inside a string.
735     InString,
736 }
737
738 impl FullCodeCharKind {
739     pub fn is_comment(&self) -> bool {
740         match *self {
741             FullCodeCharKind::StartComment
742             | FullCodeCharKind::InComment
743             | FullCodeCharKind::EndComment => true,
744             _ => false,
745         }
746     }
747
748     pub fn is_string(&self) -> bool {
749         *self == FullCodeCharKind::InString
750     }
751
752     fn to_codecharkind(&self) -> CodeCharKind {
753         if self.is_comment() {
754             CodeCharKind::Comment
755         } else {
756             CodeCharKind::Normal
757         }
758     }
759 }
760
761 impl<T> CharClasses<T>
762 where
763     T: Iterator,
764     T::Item: RichChar,
765 {
766     pub fn new(base: T) -> CharClasses<T> {
767         CharClasses {
768             base: base.peekable(),
769             status: CharClassesStatus::Normal,
770         }
771     }
772 }
773
774 impl<T> Iterator for CharClasses<T>
775 where
776     T: Iterator,
777     T::Item: RichChar,
778 {
779     type Item = (FullCodeCharKind, T::Item);
780
781     fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> {
782         let item = self.base.next()?;
783         let chr = item.get_char();
784         let mut char_kind = FullCodeCharKind::Normal;
785         self.status = match self.status {
786             CharClassesStatus::LitString => match chr {
787                 '"' => CharClassesStatus::Normal,
788                 '\\' => {
789                     char_kind = FullCodeCharKind::InString;
790                     CharClassesStatus::LitStringEscape
791                 }
792                 _ => {
793                     char_kind = FullCodeCharKind::InString;
794                     CharClassesStatus::LitString
795                 }
796             },
797             CharClassesStatus::LitStringEscape => {
798                 char_kind = FullCodeCharKind::InString;
799                 CharClassesStatus::LitString
800             }
801             CharClassesStatus::LitChar => match chr {
802                 '\\' => CharClassesStatus::LitCharEscape,
803                 '\'' => CharClassesStatus::Normal,
804                 _ => CharClassesStatus::LitChar,
805             },
806             CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar,
807             CharClassesStatus::Normal => match chr {
808                 '"' => {
809                     char_kind = FullCodeCharKind::InString;
810                     CharClassesStatus::LitString
811                 }
812                 '\'' => CharClassesStatus::LitChar,
813                 '/' => match self.base.peek() {
814                     Some(next) if next.get_char() == '*' => {
815                         self.status = CharClassesStatus::BlockCommentOpening(1);
816                         return Some((FullCodeCharKind::StartComment, item));
817                     }
818                     Some(next) if next.get_char() == '/' => {
819                         self.status = CharClassesStatus::LineComment;
820                         return Some((FullCodeCharKind::StartComment, item));
821                     }
822                     _ => CharClassesStatus::Normal,
823                 },
824                 _ => CharClassesStatus::Normal,
825             },
826             CharClassesStatus::BlockComment(deepness) => {
827                 assert_ne!(deepness, 0);
828                 self.status = match self.base.peek() {
829                     Some(next) if next.get_char() == '/' && chr == '*' => {
830                         CharClassesStatus::BlockCommentClosing(deepness - 1)
831                     }
832                     Some(next) if next.get_char() == '*' && chr == '/' => {
833                         CharClassesStatus::BlockCommentOpening(deepness + 1)
834                     }
835                     _ => CharClassesStatus::BlockComment(deepness),
836                 };
837                 return Some((FullCodeCharKind::InComment, item));
838             }
839             CharClassesStatus::BlockCommentOpening(deepness) => {
840                 assert_eq!(chr, '*');
841                 self.status = CharClassesStatus::BlockComment(deepness);
842                 return Some((FullCodeCharKind::InComment, item));
843             }
844             CharClassesStatus::BlockCommentClosing(deepness) => {
845                 assert_eq!(chr, '/');
846                 if deepness == 0 {
847                     self.status = CharClassesStatus::Normal;
848                     return Some((FullCodeCharKind::EndComment, item));
849                 } else {
850                     self.status = CharClassesStatus::BlockComment(deepness);
851                     return Some((FullCodeCharKind::InComment, item));
852                 }
853             }
854             CharClassesStatus::LineComment => match chr {
855                 '\n' => {
856                     self.status = CharClassesStatus::Normal;
857                     return Some((FullCodeCharKind::EndComment, item));
858                 }
859                 _ => {
860                     self.status = CharClassesStatus::LineComment;
861                     return Some((FullCodeCharKind::InComment, item));
862                 }
863             },
864         };
865         Some((char_kind, item))
866     }
867 }
868
869 /// Iterator over functional and commented parts of a string. Any part of a string is either
870 /// functional code, either *one* block comment, either *one* line comment. Whitespace between
871 /// comments is functional code. Line comments contain their ending newlines.
872 struct UngroupedCommentCodeSlices<'a> {
873     slice: &'a str,
874     iter: iter::Peekable<CharClasses<std::str::CharIndices<'a>>>,
875 }
876
877 impl<'a> UngroupedCommentCodeSlices<'a> {
878     fn new(code: &'a str) -> UngroupedCommentCodeSlices<'a> {
879         UngroupedCommentCodeSlices {
880             slice: code,
881             iter: CharClasses::new(code.char_indices()).peekable(),
882         }
883     }
884 }
885
886 impl<'a> Iterator for UngroupedCommentCodeSlices<'a> {
887     type Item = (CodeCharKind, usize, &'a str);
888
889     fn next(&mut self) -> Option<Self::Item> {
890         let (kind, (start_idx, _)) = self.iter.next()?;
891         match kind {
892             FullCodeCharKind::Normal | FullCodeCharKind::InString => {
893                 // Consume all the Normal code
894                 while let Some(&(char_kind, _)) = self.iter.peek() {
895                     if char_kind.is_comment() {
896                         break;
897                     }
898                     let _ = self.iter.next();
899                 }
900             }
901             FullCodeCharKind::StartComment => {
902                 // Consume the whole comment
903                 while let Some((FullCodeCharKind::InComment, (_, _))) = self.iter.next() {}
904             }
905             _ => panic!(),
906         }
907         let slice = match self.iter.peek() {
908             Some(&(_, (end_idx, _))) => &self.slice[start_idx..end_idx],
909             None => &self.slice[start_idx..],
910         };
911         Some((
912             if kind.is_comment() {
913                 CodeCharKind::Comment
914             } else {
915                 CodeCharKind::Normal
916             },
917             start_idx,
918             slice,
919         ))
920     }
921 }
922
923 /// Iterator over an alternating sequence of functional and commented parts of
924 /// a string. The first item is always a, possibly zero length, subslice of
925 /// functional text. Line style comments contain their ending newlines.
926 pub struct CommentCodeSlices<'a> {
927     slice: &'a str,
928     last_slice_kind: CodeCharKind,
929     last_slice_end: usize,
930 }
931
932 impl<'a> CommentCodeSlices<'a> {
933     pub fn new(slice: &'a str) -> CommentCodeSlices<'a> {
934         CommentCodeSlices {
935             slice,
936             last_slice_kind: CodeCharKind::Comment,
937             last_slice_end: 0,
938         }
939     }
940 }
941
942 impl<'a> Iterator for CommentCodeSlices<'a> {
943     type Item = (CodeCharKind, usize, &'a str);
944
945     fn next(&mut self) -> Option<Self::Item> {
946         if self.last_slice_end == self.slice.len() {
947             return None;
948         }
949
950         let mut sub_slice_end = self.last_slice_end;
951         let mut first_whitespace = None;
952         let subslice = &self.slice[self.last_slice_end..];
953         let mut iter = CharClasses::new(subslice.char_indices());
954
955         for (kind, (i, c)) in &mut iter {
956             let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal
957                 && &subslice[..2] == "//"
958                 && [' ', '\t'].contains(&c);
959
960             if is_comment_connector && first_whitespace.is_none() {
961                 first_whitespace = Some(i);
962             }
963
964             if kind.to_codecharkind() == self.last_slice_kind && !is_comment_connector {
965                 let last_index = match first_whitespace {
966                     Some(j) => j,
967                     None => i,
968                 };
969                 sub_slice_end = self.last_slice_end + last_index;
970                 break;
971             }
972
973             if !is_comment_connector {
974                 first_whitespace = None;
975             }
976         }
977
978         if let (None, true) = (iter.next(), sub_slice_end == self.last_slice_end) {
979             // This was the last subslice.
980             sub_slice_end = match first_whitespace {
981                 Some(i) => self.last_slice_end + i,
982                 None => self.slice.len(),
983             };
984         }
985
986         let kind = match self.last_slice_kind {
987             CodeCharKind::Comment => CodeCharKind::Normal,
988             CodeCharKind::Normal => CodeCharKind::Comment,
989         };
990         let res = (
991             kind,
992             self.last_slice_end,
993             &self.slice[self.last_slice_end..sub_slice_end],
994         );
995         self.last_slice_end = sub_slice_end;
996         self.last_slice_kind = kind;
997
998         Some(res)
999     }
1000 }
1001
1002 /// Checks is `new` didn't miss any comment from `span`, if it removed any, return previous text
1003 /// (if it fits in the width/offset, else return None), else return `new`
1004 pub fn recover_comment_removed(
1005     new: String,
1006     span: Span,
1007     context: &RewriteContext,
1008 ) -> Option<String> {
1009     let snippet = context.snippet(span);
1010     if snippet != new && changed_comment_content(snippet, &new) {
1011         // We missed some comments. Keep the original text.
1012         Some(snippet.to_owned())
1013     } else {
1014         Some(new)
1015     }
1016 }
1017
1018 /// Return true if the two strings of code have the same payload of comments.
1019 /// The payload of comments is everything in the string except:
1020 ///     - actual code (not comments)
1021 ///     - comment start/end marks
1022 ///     - whitespace
1023 ///     - '*' at the beginning of lines in block comments
1024 fn changed_comment_content(orig: &str, new: &str) -> bool {
1025     // Cannot write this as a fn since we cannot return types containing closures
1026     let code_comment_content = |code| {
1027         let slices = UngroupedCommentCodeSlices::new(code);
1028         slices
1029             .filter(|&(ref kind, _, _)| *kind == CodeCharKind::Comment)
1030             .flat_map(|(_, _, s)| CommentReducer::new(s))
1031     };
1032     let res = code_comment_content(orig).ne(code_comment_content(new));
1033     debug!(
1034         "comment::changed_comment_content: {}\norig: '{}'\nnew: '{}'\nraw_old: {}\nraw_new: {}",
1035         res,
1036         orig,
1037         new,
1038         code_comment_content(orig).collect::<String>(),
1039         code_comment_content(new).collect::<String>()
1040     );
1041     res
1042 }
1043
1044 /// Iterator over the 'payload' characters of a comment.
1045 /// It skips whitespace, comment start/end marks, and '*' at the beginning of lines.
1046 /// The comment must be one comment, ie not more than one start mark (no multiple line comments,
1047 /// for example).
1048 struct CommentReducer<'a> {
1049     is_block: bool,
1050     at_start_line: bool,
1051     iter: std::str::Chars<'a>,
1052 }
1053
1054 impl<'a> CommentReducer<'a> {
1055     fn new(comment: &'a str) -> CommentReducer<'a> {
1056         let is_block = comment.starts_with("/*");
1057         let comment = remove_comment_header(comment);
1058         CommentReducer {
1059             is_block,
1060             at_start_line: false, // There are no supplementary '*' on the first line
1061             iter: comment.chars(),
1062         }
1063     }
1064 }
1065
1066 impl<'a> Iterator for CommentReducer<'a> {
1067     type Item = char;
1068     fn next(&mut self) -> Option<Self::Item> {
1069         loop {
1070             let mut c = self.iter.next()?;
1071             if self.is_block && self.at_start_line {
1072                 while c.is_whitespace() {
1073                     c = self.iter.next()?;
1074                 }
1075                 // Ignore leading '*'
1076                 if c == '*' {
1077                     c = self.iter.next()?;
1078                 }
1079             } else if c == '\n' {
1080                 self.at_start_line = true;
1081             }
1082             if !c.is_whitespace() {
1083                 return Some(c);
1084             }
1085         }
1086     }
1087 }
1088
1089 fn remove_comment_header(comment: &str) -> &str {
1090     if comment.starts_with("///") || comment.starts_with("//!") {
1091         &comment[3..]
1092     } else if comment.starts_with("//") {
1093         &comment[2..]
1094     } else if (comment.starts_with("/**") && !comment.starts_with("/**/"))
1095         || comment.starts_with("/*!")
1096     {
1097         &comment[3..comment.len() - 2]
1098     } else {
1099         assert!(
1100             comment.starts_with("/*"),
1101             format!("string '{}' is not a comment", comment)
1102         );
1103         &comment[2..comment.len() - 2]
1104     }
1105 }
1106
1107 #[cfg(test)]
1108 mod test {
1109     use super::{contains_comment, rewrite_comment, CharClasses, CodeCharKind, CommentCodeSlices,
1110                 FindUncommented, FullCodeCharKind};
1111     use shape::{Indent, Shape};
1112
1113     #[test]
1114     fn char_classes() {
1115         let mut iter = CharClasses::new("//\n\n".chars());
1116
1117         assert_eq!((FullCodeCharKind::StartComment, '/'), iter.next().unwrap());
1118         assert_eq!((FullCodeCharKind::InComment, '/'), iter.next().unwrap());
1119         assert_eq!((FullCodeCharKind::EndComment, '\n'), iter.next().unwrap());
1120         assert_eq!((FullCodeCharKind::Normal, '\n'), iter.next().unwrap());
1121         assert_eq!(None, iter.next());
1122     }
1123
1124     #[test]
1125     fn comment_code_slices() {
1126         let input = "code(); /* test */ 1 + 1";
1127         let mut iter = CommentCodeSlices::new(input);
1128
1129         assert_eq!((CodeCharKind::Normal, 0, "code(); "), iter.next().unwrap());
1130         assert_eq!(
1131             (CodeCharKind::Comment, 8, "/* test */"),
1132             iter.next().unwrap()
1133         );
1134         assert_eq!((CodeCharKind::Normal, 18, " 1 + 1"), iter.next().unwrap());
1135         assert_eq!(None, iter.next());
1136     }
1137
1138     #[test]
1139     fn comment_code_slices_two() {
1140         let input = "// comment\n    test();";
1141         let mut iter = CommentCodeSlices::new(input);
1142
1143         assert_eq!((CodeCharKind::Normal, 0, ""), iter.next().unwrap());
1144         assert_eq!(
1145             (CodeCharKind::Comment, 0, "// comment\n"),
1146             iter.next().unwrap()
1147         );
1148         assert_eq!(
1149             (CodeCharKind::Normal, 11, "    test();"),
1150             iter.next().unwrap()
1151         );
1152         assert_eq!(None, iter.next());
1153     }
1154
1155     #[test]
1156     fn comment_code_slices_three() {
1157         let input = "1 // comment\n    // comment2\n\n";
1158         let mut iter = CommentCodeSlices::new(input);
1159
1160         assert_eq!((CodeCharKind::Normal, 0, "1 "), iter.next().unwrap());
1161         assert_eq!(
1162             (CodeCharKind::Comment, 2, "// comment\n    // comment2\n"),
1163             iter.next().unwrap()
1164         );
1165         assert_eq!((CodeCharKind::Normal, 29, "\n"), iter.next().unwrap());
1166         assert_eq!(None, iter.next());
1167     }
1168
1169     #[test]
1170     #[cfg_attr(rustfmt, rustfmt_skip)]
1171     fn format_comments() {
1172         let mut config: ::config::Config = Default::default();
1173         config.set().wrap_comments(true);
1174         config.set().normalize_comments(true);
1175
1176         let comment = rewrite_comment(" //test",
1177                                       true,
1178                                       Shape::legacy(100, Indent::new(0, 100)),
1179                                       &config).unwrap();
1180         assert_eq!("/* test */", comment);
1181
1182         let comment = rewrite_comment("// comment on a",
1183                                       false,
1184                                       Shape::legacy(10, Indent::empty()),
1185                                       &config).unwrap();
1186         assert_eq!("// comment\n// on a", comment);
1187
1188         let comment = rewrite_comment("//  A multi line comment\n             // between args.",
1189                                       false,
1190                                       Shape::legacy(60, Indent::new(0, 12)),
1191                                       &config).unwrap();
1192         assert_eq!("//  A multi line comment\n            // between args.", comment);
1193
1194         let input = "// comment";
1195         let expected =
1196             "/* comment */";
1197         let comment = rewrite_comment(input,
1198                                       true,
1199                                       Shape::legacy(9, Indent::new(0, 69)),
1200                                       &config).unwrap();
1201         assert_eq!(expected, comment);
1202
1203         let comment = rewrite_comment("/*   trimmed    */",
1204                                       true,
1205                                       Shape::legacy(100, Indent::new(0, 100)),
1206                                       &config).unwrap();
1207         assert_eq!("/* trimmed */", comment);
1208     }
1209
1210     // This is probably intended to be a non-test fn, but it is not used. I'm
1211     // keeping it around unless it helps us test stuff.
1212     fn uncommented(text: &str) -> String {
1213         CharClasses::new(text.chars())
1214             .filter_map(|(s, c)| match s {
1215                 FullCodeCharKind::Normal | FullCodeCharKind::InString => Some(c),
1216                 _ => None,
1217             })
1218             .collect()
1219     }
1220
1221     #[test]
1222     fn test_uncommented() {
1223         assert_eq!(&uncommented("abc/*...*/"), "abc");
1224         assert_eq!(
1225             &uncommented("// .... /* \n../* /* *** / */ */a/* // */c\n"),
1226             "..ac\n"
1227         );
1228         assert_eq!(&uncommented("abc \" /* */\" qsdf"), "abc \" /* */\" qsdf");
1229     }
1230
1231     #[test]
1232     fn test_contains_comment() {
1233         assert_eq!(contains_comment("abc"), false);
1234         assert_eq!(contains_comment("abc // qsdf"), true);
1235         assert_eq!(contains_comment("abc /* kqsdf"), true);
1236         assert_eq!(contains_comment("abc \" /* */\" qsdf"), false);
1237     }
1238
1239     #[test]
1240     fn test_find_uncommented() {
1241         fn check(haystack: &str, needle: &str, expected: Option<usize>) {
1242             assert_eq!(expected, haystack.find_uncommented(needle));
1243         }
1244
1245         check("/*/ */test", "test", Some(6));
1246         check("//test\ntest", "test", Some(7));
1247         check("/* comment only */", "whatever", None);
1248         check(
1249             "/* comment */ some text /* more commentary */ result",
1250             "result",
1251             Some(46),
1252         );
1253         check("sup // sup", "p", Some(2));
1254         check("sup", "x", None);
1255         check(r#"π? /**/ π is nice!"#, r#"π is nice"#, Some(9));
1256         check("/*sup yo? \n sup*/ sup", "p", Some(20));
1257         check("hel/*lohello*/lo", "hello", None);
1258         check("acb", "ab", None);
1259         check(",/*A*/ ", ",", Some(0));
1260         check("abc", "abc", Some(0));
1261         check("/* abc */", "abc", None);
1262         check("/**/abc/* */", "abc", Some(4));
1263         check("\"/* abc */\"", "abc", Some(4));
1264         check("\"/* abc", "abc", Some(4));
1265     }
1266 }