]> git.lizzy.rs Git - rust.git/blob - src/comment.rs
Merge pull request #3070 from topecongiro/issue-3030
[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, borrow::Cow, iter};
14
15 use itertools::{multipeek, MultiPeek};
16 use syntax::source_map::Span;
17
18 use config::Config;
19 use rewrite::RewriteContext;
20 use shape::{Indent, Shape};
21 use string::{rewrite_string, StringFormat};
22 use utils::{count_newlines, first_line_width, last_line_width};
23 use {ErrorKind, FormattingError};
24
25 fn is_custom_comment(comment: &str) -> bool {
26     if !comment.starts_with("//") {
27         false
28     } else if let Some(c) = comment.chars().nth(2) {
29         !c.is_alphanumeric() && !c.is_whitespace()
30     } else {
31         false
32     }
33 }
34
35 #[derive(Copy, Clone, PartialEq, Eq)]
36 pub enum CommentStyle<'a> {
37     DoubleSlash,
38     TripleSlash,
39     Doc,
40     SingleBullet,
41     DoubleBullet,
42     Exclamation,
43     Custom(&'a str),
44 }
45
46 fn custom_opener(s: &str) -> &str {
47     s.lines().next().map_or("", |first_line| {
48         first_line
49             .find(' ')
50             .map_or(first_line, |space_index| &first_line[0..=space_index])
51     })
52 }
53
54 impl<'a> CommentStyle<'a> {
55     /// Returns true if the commenting style covers a line only.
56     pub fn is_line_comment(&self) -> bool {
57         match *self {
58             CommentStyle::DoubleSlash
59             | CommentStyle::TripleSlash
60             | CommentStyle::Doc
61             | CommentStyle::Custom(_) => true,
62             _ => false,
63         }
64     }
65
66     /// Returns true if the commenting style can span over multiple lines.
67     pub fn is_block_comment(&self) -> bool {
68         match *self {
69             CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
70                 true
71             }
72             _ => false,
73         }
74     }
75
76     /// Returns true if the commenting style is for documentation.
77     pub fn is_doc_comment(&self) -> bool {
78         match *self {
79             CommentStyle::TripleSlash | CommentStyle::Doc => true,
80             _ => false,
81         }
82     }
83
84     pub fn opener(&self) -> &'a str {
85         match *self {
86             CommentStyle::DoubleSlash => "// ",
87             CommentStyle::TripleSlash => "/// ",
88             CommentStyle::Doc => "//! ",
89             CommentStyle::SingleBullet => "/* ",
90             CommentStyle::DoubleBullet => "/** ",
91             CommentStyle::Exclamation => "/*! ",
92             CommentStyle::Custom(opener) => opener,
93         }
94     }
95
96     pub fn closer(&self) -> &'a str {
97         match *self {
98             CommentStyle::DoubleSlash
99             | CommentStyle::TripleSlash
100             | CommentStyle::Custom(..)
101             | CommentStyle::Doc => "",
102             CommentStyle::DoubleBullet => " **/",
103             CommentStyle::SingleBullet | CommentStyle::Exclamation => " */",
104         }
105     }
106
107     pub fn line_start(&self) -> &'a str {
108         match *self {
109             CommentStyle::DoubleSlash => "// ",
110             CommentStyle::TripleSlash => "/// ",
111             CommentStyle::Doc => "//! ",
112             CommentStyle::SingleBullet | CommentStyle::Exclamation => " * ",
113             CommentStyle::DoubleBullet => " ** ",
114             CommentStyle::Custom(opener) => opener,
115         }
116     }
117
118     pub fn to_str_tuplet(&self) -> (&'a str, &'a str, &'a str) {
119         (self.opener(), self.closer(), self.line_start())
120     }
121 }
122
123 fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle {
124     if !normalize_comments {
125         if orig.starts_with("/**") && !orig.starts_with("/**/") {
126             CommentStyle::DoubleBullet
127         } else if orig.starts_with("/*!") {
128             CommentStyle::Exclamation
129         } else if orig.starts_with("/*") {
130             CommentStyle::SingleBullet
131         } else if orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/') {
132             CommentStyle::TripleSlash
133         } else if orig.starts_with("//!") {
134             CommentStyle::Doc
135         } else if is_custom_comment(orig) {
136             CommentStyle::Custom(custom_opener(orig))
137         } else {
138             CommentStyle::DoubleSlash
139         }
140     } else if (orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/'))
141         || (orig.starts_with("/**") && !orig.starts_with("/**/"))
142     {
143         CommentStyle::TripleSlash
144     } else if orig.starts_with("//!") || orig.starts_with("/*!") {
145         CommentStyle::Doc
146     } else if is_custom_comment(orig) {
147         CommentStyle::Custom(custom_opener(orig))
148     } else {
149         CommentStyle::DoubleSlash
150     }
151 }
152
153 /// Combine `prev_str` and `next_str` into a single `String`. `span` may contain
154 /// comments between two strings. If there are such comments, then that will be
155 /// recovered. If `allow_extend` is true and there is no comment between the two
156 /// strings, then they will be put on a single line as long as doing so does not
157 /// exceed max width.
158 pub fn combine_strs_with_missing_comments(
159     context: &RewriteContext,
160     prev_str: &str,
161     next_str: &str,
162     span: Span,
163     shape: Shape,
164     allow_extend: bool,
165 ) -> Option<String> {
166     let mut result =
167         String::with_capacity(prev_str.len() + next_str.len() + shape.indent.width() + 128);
168     result.push_str(prev_str);
169     let mut allow_one_line = !prev_str.contains('\n') && !next_str.contains('\n');
170     let first_sep = if prev_str.is_empty() || next_str.is_empty() {
171         ""
172     } else {
173         " "
174     };
175     let mut one_line_width =
176         last_line_width(prev_str) + first_line_width(next_str) + first_sep.len();
177
178     let config = context.config;
179     let indent = shape.indent;
180     let missing_comment = rewrite_missing_comment(span, shape, context)?;
181
182     if missing_comment.is_empty() {
183         if allow_extend && prev_str.len() + first_sep.len() + next_str.len() <= shape.width {
184             result.push_str(first_sep);
185         } else if !prev_str.is_empty() {
186             result.push_str(&indent.to_string_with_newline(config))
187         }
188         result.push_str(next_str);
189         return Some(result);
190     }
191
192     // We have a missing comment between the first expression and the second expression.
193
194     // Peek the the original source code and find out whether there is a newline between the first
195     // expression and the second expression or the missing comment. We will preserve the original
196     // layout whenever possible.
197     let original_snippet = context.snippet(span);
198     let prefer_same_line = if let Some(pos) = original_snippet.find('/') {
199         !original_snippet[..pos].contains('\n')
200     } else {
201         !original_snippet.contains('\n')
202     };
203
204     one_line_width -= first_sep.len();
205     let first_sep = if prev_str.is_empty() || missing_comment.is_empty() {
206         Cow::from("")
207     } else {
208         let one_line_width = last_line_width(prev_str) + first_line_width(&missing_comment) + 1;
209         if prefer_same_line && one_line_width <= shape.width {
210             Cow::from(" ")
211         } else {
212             indent.to_string_with_newline(config)
213         }
214     };
215     result.push_str(&first_sep);
216     result.push_str(&missing_comment);
217
218     let second_sep = if missing_comment.is_empty() || next_str.is_empty() {
219         Cow::from("")
220     } else if missing_comment.starts_with("//") {
221         indent.to_string_with_newline(config)
222     } else {
223         one_line_width += missing_comment.len() + first_sep.len() + 1;
224         allow_one_line &= !missing_comment.starts_with("//") && !missing_comment.contains('\n');
225         if prefer_same_line && allow_one_line && one_line_width <= shape.width {
226             Cow::from(" ")
227         } else {
228             indent.to_string_with_newline(config)
229         }
230     };
231     result.push_str(&second_sep);
232     result.push_str(next_str);
233
234     Some(result)
235 }
236
237 pub fn rewrite_doc_comment(orig: &str, shape: Shape, config: &Config) -> Option<String> {
238     identify_comment(orig, false, shape, config, true)
239 }
240
241 pub fn rewrite_comment(
242     orig: &str,
243     block_style: bool,
244     shape: Shape,
245     config: &Config,
246 ) -> Option<String> {
247     identify_comment(orig, block_style, shape, config, false)
248 }
249
250 fn identify_comment(
251     orig: &str,
252     block_style: bool,
253     shape: Shape,
254     config: &Config,
255     is_doc_comment: bool,
256 ) -> Option<String> {
257     let style = comment_style(orig, false);
258
259     // Computes the len of line taking into account a newline if the line is part of a paragraph.
260     fn compute_len(orig: &str, line: &str) -> usize {
261         if orig.len() > line.len() {
262             if orig.as_bytes()[line.len()] == b'\r' {
263                 line.len() + 2
264             } else {
265                 line.len() + 1
266             }
267         } else {
268             line.len()
269         }
270     }
271
272     // Get the first group of line comments having the same commenting style.
273     //
274     // Returns a tuple with:
275     // - a boolean indicating if there is a blank line
276     // - a number indicating the size of the first group of comments
277     fn consume_same_line_comments(
278         style: CommentStyle,
279         orig: &str,
280         line_start: &str,
281     ) -> (bool, usize) {
282         let mut first_group_ending = 0;
283         let mut hbl = false;
284
285         for line in orig.lines() {
286             let trimmed_line = line.trim_left();
287             if trimmed_line.is_empty() {
288                 hbl = true;
289                 break;
290             } else if trimmed_line.starts_with(line_start)
291                 || comment_style(trimmed_line, false) == style
292             {
293                 first_group_ending += compute_len(&orig[first_group_ending..], line);
294             } else {
295                 break;
296             }
297         }
298         (hbl, first_group_ending)
299     }
300
301     let (has_bare_lines, first_group_ending) = match style {
302         CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => {
303             let line_start = style.line_start().trim_left();
304             consume_same_line_comments(style, orig, line_start)
305         }
306         CommentStyle::Custom(opener) => {
307             let trimmed_opener = opener.trim_right();
308             consume_same_line_comments(style, orig, trimmed_opener)
309         }
310         // for a block comment, search for the closing symbol
311         CommentStyle::DoubleBullet | CommentStyle::SingleBullet | CommentStyle::Exclamation => {
312             let closer = style.closer().trim_left();
313             let mut closing_symbol_offset = 0;
314             let mut hbl = false;
315             for line in orig.lines() {
316                 closing_symbol_offset += compute_len(&orig[closing_symbol_offset..], line);
317                 let trimmed_line = line.trim_left();
318                 if !trimmed_line.starts_with('*')
319                     && !trimmed_line.starts_with("//")
320                     && !trimmed_line.starts_with("/*")
321                 {
322                     hbl = true;
323                 }
324                 if trimmed_line.ends_with(closer) {
325                     break;
326                 }
327             }
328             (hbl, closing_symbol_offset)
329         }
330     };
331
332     let (first_group, rest) = orig.split_at(first_group_ending);
333     let rewritten_first_group =
334         if !config.normalize_comments() && has_bare_lines && style.is_block_comment() {
335             light_rewrite_block_comment_with_bare_lines(first_group, shape, config)?
336         } else if !config.normalize_comments() && !config.wrap_comments() {
337             light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)?
338         } else {
339             rewrite_comment_inner(
340                 first_group,
341                 block_style,
342                 style,
343                 shape,
344                 config,
345                 is_doc_comment || style.is_doc_comment(),
346             )?
347         };
348     if rest.is_empty() {
349         Some(rewritten_first_group)
350     } else {
351         identify_comment(rest.trim_left(), block_style, shape, config, is_doc_comment).map(
352             |rest_str| {
353                 format!(
354                     "{}\n{}{}{}",
355                     rewritten_first_group,
356                     // insert back the blank line
357                     if has_bare_lines && style.is_line_comment() {
358                         "\n"
359                     } else {
360                         ""
361                     },
362                     shape.indent.to_string(config),
363                     rest_str
364                 )
365             },
366         )
367     }
368 }
369
370 /// Trims a minimum of leading whitespaces so that the content layout is kept and aligns to indent.
371 fn light_rewrite_block_comment_with_bare_lines(
372     orig: &str,
373     shape: Shape,
374     config: &Config,
375 ) -> Option<String> {
376     let prefix_whitespace_min = orig
377         .lines()
378         // skip the line with the starting sigil since the leading whitespace is removed
379         // otherwise, the minimum would always be zero
380         .skip(1)
381         .filter(|line| !line.is_empty())
382         .map(|line| {
383             let mut width = 0;
384             for c in line.chars() {
385                 match c {
386                     ' ' => width += 1,
387                     '\t' => width += config.tab_spaces(),
388                     _ => break,
389                 }
390             }
391             width
392         })
393         .min()?;
394
395     let indent_str = shape.indent.to_string(config);
396     let mut lines = orig.lines();
397     let first_line = lines.next()?;
398     let rest = lines
399         .map(|line| {
400             if line.is_empty() {
401                 line
402             } else {
403                 &line[prefix_whitespace_min..]
404             }
405         })
406         .collect::<Vec<&str>>()
407         .join(&format!("\n{}", indent_str));
408     Some(format!("{}\n{}{}", first_line, indent_str, rest))
409 }
410
411 /// Attributes for code blocks in rustdoc.
412 /// See https://doc.rust-lang.org/rustdoc/print.html#attributes
413 enum CodeBlockAttribute {
414     Rust,
415     Ignore,
416     Text,
417     ShouldPanic,
418     NoRun,
419     CompileFail,
420 }
421
422 impl CodeBlockAttribute {
423     fn new(attribute: &str) -> CodeBlockAttribute {
424         match attribute {
425             "rust" | "" => CodeBlockAttribute::Rust,
426             "ignore" => CodeBlockAttribute::Ignore,
427             "text" => CodeBlockAttribute::Text,
428             "should_panic" => CodeBlockAttribute::ShouldPanic,
429             "no_run" => CodeBlockAttribute::NoRun,
430             "compile_fail" => CodeBlockAttribute::CompileFail,
431             _ => CodeBlockAttribute::Text,
432         }
433     }
434 }
435
436 fn rewrite_comment_inner(
437     orig: &str,
438     block_style: bool,
439     style: CommentStyle,
440     shape: Shape,
441     config: &Config,
442     is_doc_comment: bool,
443 ) -> Option<String> {
444     let (opener, closer, line_start) = if block_style {
445         CommentStyle::SingleBullet.to_str_tuplet()
446     } else {
447         comment_style(orig, config.normalize_comments()).to_str_tuplet()
448     };
449
450     let max_chars = shape
451         .width
452         .checked_sub(closer.len() + opener.len())
453         .unwrap_or(1);
454     let indent_str = shape.indent.to_string_with_newline(config);
455     let fmt_indent = shape.indent + (opener.len() - line_start.len());
456     let mut fmt = StringFormat {
457         opener: "",
458         closer: "",
459         line_start,
460         line_end: "",
461         shape: Shape::legacy(max_chars, fmt_indent),
462         trim_end: true,
463         config,
464     };
465
466     let line_breaks = count_newlines(orig.trim_right());
467     let lines = orig
468         .lines()
469         .enumerate()
470         .map(|(i, mut line)| {
471             line = trim_right_unless_two_whitespaces(line.trim_left(), is_doc_comment);
472             // Drop old closer.
473             if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
474                 line = line[..(line.len() - 2)].trim_right();
475             }
476
477             line
478         })
479         .map(|s| left_trim_comment_line(s, &style))
480         .map(|(line, has_leading_whitespace)| {
481             if orig.starts_with("/*") && line_breaks == 0 {
482                 (
483                     line.trim_left(),
484                     has_leading_whitespace || config.normalize_comments(),
485                 )
486             } else {
487                 (line, has_leading_whitespace || config.normalize_comments())
488             }
489         });
490
491     let mut result = String::with_capacity(orig.len() * 2);
492     result.push_str(opener);
493     let mut code_block_buffer = String::with_capacity(128);
494     let mut is_prev_line_multi_line = false;
495     let mut code_block_attr = None;
496     let comment_line_separator = format!("{}{}", indent_str, line_start);
497     let join_code_block_with_comment_line_separator = |s: &str| {
498         let mut result = String::with_capacity(s.len() + 128);
499         let mut iter = s.lines().peekable();
500         while let Some(line) = iter.next() {
501             result.push_str(line);
502             result.push_str(match iter.peek() {
503                 Some(next_line) if next_line.is_empty() => comment_line_separator.trim_right(),
504                 Some(..) => &comment_line_separator,
505                 None => "",
506             });
507         }
508         result
509     };
510
511     for (i, (line, has_leading_whitespace)) in lines.enumerate() {
512         let is_last = i == count_newlines(orig);
513
514         if let Some(ref attr) = code_block_attr {
515             if line.starts_with("```") {
516                 let code_block = match attr {
517                     CodeBlockAttribute::Ignore | CodeBlockAttribute::Text => {
518                         trim_custom_comment_prefix(&code_block_buffer)
519                     }
520                     _ if code_block_buffer.is_empty() => String::new(),
521                     _ => {
522                         let mut config = config.clone();
523                         config.set().wrap_comments(false);
524                         match ::format_code_block(&code_block_buffer, &config) {
525                             Some(ref s) => trim_custom_comment_prefix(s),
526                             None => trim_custom_comment_prefix(&code_block_buffer),
527                         }
528                     }
529                 };
530                 if !code_block.is_empty() {
531                     result.push_str(&comment_line_separator);
532                     result.push_str(&join_code_block_with_comment_line_separator(&code_block));
533                 }
534                 code_block_buffer.clear();
535                 result.push_str(&comment_line_separator);
536                 result.push_str(line);
537                 code_block_attr = None;
538             } else {
539                 code_block_buffer.push_str(&hide_sharp_behind_comment(line));
540                 code_block_buffer.push('\n');
541
542                 if is_last {
543                     // There is a code block that is not properly enclosed by backticks.
544                     // We will leave them untouched.
545                     result.push_str(&comment_line_separator);
546                     result.push_str(&join_code_block_with_comment_line_separator(
547                         &trim_custom_comment_prefix(&code_block_buffer),
548                     ));
549                 }
550             }
551
552             continue;
553         } else {
554             code_block_attr = if line.starts_with("```") {
555                 Some(CodeBlockAttribute::new(&line[3..]))
556             } else {
557                 None
558             };
559
560             if result == opener {
561                 let force_leading_whitespace = opener == "/* " && count_newlines(orig) == 0;
562                 if !has_leading_whitespace && !force_leading_whitespace && result.ends_with(' ') {
563                     result.pop();
564                 }
565                 if line.is_empty() {
566                     continue;
567                 }
568             } else if is_prev_line_multi_line && !line.is_empty() {
569                 result.push(' ')
570             } else if is_last && line.is_empty() {
571                 // trailing blank lines are unwanted
572                 if !closer.is_empty() {
573                     result.push_str(&indent_str);
574                 }
575                 break;
576             } else {
577                 result.push_str(&comment_line_separator);
578                 if !has_leading_whitespace && result.ends_with(' ') {
579                     result.pop();
580                 }
581             }
582         }
583
584         if config.wrap_comments() && line.len() > fmt.shape.width && !has_url(line) {
585             match rewrite_string(line, &fmt) {
586                 Some(ref s) => {
587                     is_prev_line_multi_line = s.contains('\n');
588                     result.push_str(s);
589                 }
590                 None if is_prev_line_multi_line => {
591                     // We failed to put the current `line` next to the previous `line`.
592                     // Remove the trailing space, then start rewrite on the next line.
593                     result.pop();
594                     result.push_str(&comment_line_separator);
595                     fmt.shape = Shape::legacy(max_chars, fmt_indent);
596                     match rewrite_string(line, &fmt) {
597                         Some(ref s) => {
598                             is_prev_line_multi_line = s.contains('\n');
599                             result.push_str(s);
600                         }
601                         None => {
602                             is_prev_line_multi_line = false;
603                             result.push_str(line);
604                         }
605                     }
606                 }
607                 None => {
608                     is_prev_line_multi_line = false;
609                     result.push_str(line);
610                 }
611             }
612
613             fmt.shape = if is_prev_line_multi_line {
614                 // 1 = " "
615                 let offset = 1 + last_line_width(&result) - line_start.len();
616                 Shape {
617                     width: max_chars.saturating_sub(offset),
618                     indent: fmt_indent,
619                     offset: fmt.shape.offset + offset,
620                 }
621             } else {
622                 Shape::legacy(max_chars, fmt_indent)
623             };
624         } else {
625             if line.is_empty() && result.ends_with(' ') && !is_last {
626                 // Remove space if this is an empty comment or a doc comment.
627                 result.pop();
628             }
629             result.push_str(line);
630             fmt.shape = Shape::legacy(max_chars, fmt_indent);
631             is_prev_line_multi_line = false;
632         }
633     }
634
635     result.push_str(closer);
636     if result.ends_with(opener) && opener.ends_with(' ') {
637         // Trailing space.
638         result.pop();
639     }
640
641     Some(result)
642 }
643
644 const RUSTFMT_CUSTOM_COMMENT_PREFIX: &str = "//#### ";
645
646 fn hide_sharp_behind_comment(s: &str) -> Cow<str> {
647     if s.trim_left().starts_with("# ") {
648         Cow::from(format!("{}{}", RUSTFMT_CUSTOM_COMMENT_PREFIX, s))
649     } else {
650         Cow::from(s)
651     }
652 }
653
654 fn trim_custom_comment_prefix(s: &str) -> String {
655     s.lines()
656         .map(|line| {
657             let left_trimmed = line.trim_left();
658             if left_trimmed.starts_with(RUSTFMT_CUSTOM_COMMENT_PREFIX) {
659                 left_trimmed.trim_left_matches(RUSTFMT_CUSTOM_COMMENT_PREFIX)
660             } else {
661                 line
662             }
663         })
664         .collect::<Vec<_>>()
665         .join("\n")
666 }
667
668 /// Returns true if the given string MAY include URLs or alike.
669 fn has_url(s: &str) -> bool {
670     // This function may return false positive, but should get its job done in most cases.
671     s.contains("https://") || s.contains("http://") || s.contains("ftp://") || s.contains("file://")
672 }
673
674 /// Given the span, rewrite the missing comment inside it if available.
675 /// Note that the given span must only include comments (or leading/trailing whitespaces).
676 pub fn rewrite_missing_comment(
677     span: Span,
678     shape: Shape,
679     context: &RewriteContext,
680 ) -> Option<String> {
681     let missing_snippet = context.snippet(span);
682     let trimmed_snippet = missing_snippet.trim();
683     if !trimmed_snippet.is_empty() {
684         rewrite_comment(trimmed_snippet, false, shape, context.config)
685     } else {
686         Some(String::new())
687     }
688 }
689
690 /// Recover the missing comments in the specified span, if available.
691 /// The layout of the comments will be preserved as long as it does not break the code
692 /// and its total width does not exceed the max width.
693 pub fn recover_missing_comment_in_span(
694     span: Span,
695     shape: Shape,
696     context: &RewriteContext,
697     used_width: usize,
698 ) -> Option<String> {
699     let missing_comment = rewrite_missing_comment(span, shape, context)?;
700     if missing_comment.is_empty() {
701         Some(String::new())
702     } else {
703         let missing_snippet = context.snippet(span);
704         let pos = missing_snippet.find('/').unwrap_or(0);
705         // 1 = ` `
706         let total_width = missing_comment.len() + used_width + 1;
707         let force_new_line_before_comment =
708             missing_snippet[..pos].contains('\n') || total_width > context.config.max_width();
709         let sep = if force_new_line_before_comment {
710             shape.indent.to_string_with_newline(context.config)
711         } else {
712             Cow::from(" ")
713         };
714         Some(format!("{}{}", sep, missing_comment))
715     }
716 }
717
718 /// Trim trailing whitespaces unless they consist of two or more whitespaces.
719 fn trim_right_unless_two_whitespaces(s: &str, is_doc_comment: bool) -> &str {
720     if is_doc_comment && s.ends_with("  ") {
721         s
722     } else {
723         s.trim_right()
724     }
725 }
726
727 /// Trims whitespace and aligns to indent, but otherwise does not change comments.
728 fn light_rewrite_comment(
729     orig: &str,
730     offset: Indent,
731     config: &Config,
732     is_doc_comment: bool,
733 ) -> Option<String> {
734     let lines: Vec<&str> = orig
735         .lines()
736         .map(|l| {
737             // This is basically just l.trim(), but in the case that a line starts
738             // with `*` we want to leave one space before it, so it aligns with the
739             // `*` in `/*`.
740             let first_non_whitespace = l.find(|c| !char::is_whitespace(c));
741             let left_trimmed = if let Some(fnw) = first_non_whitespace {
742                 if l.as_bytes()[fnw] == b'*' && fnw > 0 {
743                     &l[fnw - 1..]
744                 } else {
745                     &l[fnw..]
746                 }
747             } else {
748                 ""
749             };
750             // Preserve markdown's double-space line break syntax in doc comment.
751             trim_right_unless_two_whitespaces(left_trimmed, is_doc_comment)
752         })
753         .collect();
754     Some(lines.join(&format!("\n{}", offset.to_string(config))))
755 }
756
757 /// Trims comment characters and possibly a single space from the left of a string.
758 /// Does not trim all whitespace. If a single space is trimmed from the left of the string,
759 /// this function returns true.
760 fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle) -> (&'a str, bool) {
761     if line.starts_with("//! ")
762         || line.starts_with("/// ")
763         || line.starts_with("/*! ")
764         || line.starts_with("/** ")
765     {
766         (&line[4..], true)
767     } else if let CommentStyle::Custom(opener) = *style {
768         if line.starts_with(opener) {
769             (&line[opener.len()..], true)
770         } else {
771             (&line[opener.trim_right().len()..], false)
772         }
773     } else if line.starts_with("/* ")
774         || line.starts_with("// ")
775         || line.starts_with("//!")
776         || line.starts_with("///")
777         || line.starts_with("** ")
778         || line.starts_with("/*!")
779         || (line.starts_with("/**") && !line.starts_with("/**/"))
780     {
781         (&line[3..], line.chars().nth(2).unwrap() == ' ')
782     } else if line.starts_with("/*")
783         || line.starts_with("* ")
784         || line.starts_with("//")
785         || line.starts_with("**")
786     {
787         (&line[2..], line.chars().nth(1).unwrap() == ' ')
788     } else if line.starts_with('*') {
789         (&line[1..], false)
790     } else {
791         (line, line.starts_with(' '))
792     }
793 }
794
795 pub trait FindUncommented {
796     fn find_uncommented(&self, pat: &str) -> Option<usize>;
797 }
798
799 impl FindUncommented for str {
800     fn find_uncommented(&self, pat: &str) -> Option<usize> {
801         let mut needle_iter = pat.chars();
802         for (kind, (i, b)) in CharClasses::new(self.char_indices()) {
803             match needle_iter.next() {
804                 None => {
805                     return Some(i - pat.len());
806                 }
807                 Some(c) => match kind {
808                     FullCodeCharKind::Normal | FullCodeCharKind::InString if b == c => {}
809                     _ => {
810                         needle_iter = pat.chars();
811                     }
812                 },
813             }
814         }
815
816         // Handle case where the pattern is a suffix of the search string
817         match needle_iter.next() {
818             Some(_) => None,
819             None => Some(self.len() - pat.len()),
820         }
821     }
822 }
823
824 // Returns the first byte position after the first comment. The given string
825 // is expected to be prefixed by a comment, including delimiters.
826 // Good: "/* /* inner */ outer */ code();"
827 // Bad:  "code(); // hello\n world!"
828 pub fn find_comment_end(s: &str) -> Option<usize> {
829     let mut iter = CharClasses::new(s.char_indices());
830     for (kind, (i, _c)) in &mut iter {
831         if kind == FullCodeCharKind::Normal || kind == FullCodeCharKind::InString {
832             return Some(i);
833         }
834     }
835
836     // Handle case where the comment ends at the end of s.
837     if iter.status == CharClassesStatus::Normal {
838         Some(s.len())
839     } else {
840         None
841     }
842 }
843
844 /// Returns true if text contains any comment.
845 pub fn contains_comment(text: &str) -> bool {
846     CharClasses::new(text.chars()).any(|(kind, _)| kind.is_comment())
847 }
848
849 /// Remove trailing spaces from the specified snippet. We do not remove spaces
850 /// inside strings or comments.
851 pub fn remove_trailing_white_spaces(text: &str) -> String {
852     let mut buffer = String::with_capacity(text.len());
853     let mut space_buffer = String::with_capacity(128);
854     for (char_kind, c) in CharClasses::new(text.chars()) {
855         match c {
856             '\n' => {
857                 if char_kind == FullCodeCharKind::InString {
858                     buffer.push_str(&space_buffer);
859                 }
860                 space_buffer.clear();
861                 buffer.push('\n');
862             }
863             _ if c.is_whitespace() => {
864                 space_buffer.push(c);
865             }
866             _ => {
867                 if !space_buffer.is_empty() {
868                     buffer.push_str(&space_buffer);
869                     space_buffer.clear();
870                 }
871                 buffer.push(c);
872             }
873         }
874     }
875     buffer
876 }
877
878 pub struct CharClasses<T>
879 where
880     T: Iterator,
881     T::Item: RichChar,
882 {
883     base: MultiPeek<T>,
884     status: CharClassesStatus,
885 }
886
887 pub trait RichChar {
888     fn get_char(&self) -> char;
889 }
890
891 impl RichChar for char {
892     fn get_char(&self) -> char {
893         *self
894     }
895 }
896
897 impl RichChar for (usize, char) {
898     fn get_char(&self) -> char {
899         self.1
900     }
901 }
902
903 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
904 enum CharClassesStatus {
905     Normal,
906     LitString,
907     LitStringEscape,
908     LitRawString(u32),
909     RawStringPrefix(u32),
910     RawStringSuffix(u32),
911     LitChar,
912     LitCharEscape,
913     // The u32 is the nesting deepness of the comment
914     BlockComment(u32),
915     // Status when the '/' has been consumed, but not yet the '*', deepness is
916     // the new deepness (after the comment opening).
917     BlockCommentOpening(u32),
918     // Status when the '*' has been consumed, but not yet the '/', deepness is
919     // the new deepness (after the comment closing).
920     BlockCommentClosing(u32),
921     LineComment,
922 }
923
924 /// Distinguish between functional part of code and comments
925 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
926 pub enum CodeCharKind {
927     Normal,
928     Comment,
929 }
930
931 /// Distinguish between functional part of code and comments,
932 /// describing opening and closing of comments for ease when chunking
933 /// code from tagged characters
934 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
935 pub enum FullCodeCharKind {
936     Normal,
937     /// The first character of a comment, there is only one for a comment (always '/')
938     StartComment,
939     /// Any character inside a comment including the second character of comment
940     /// marks ("//", "/*")
941     InComment,
942     /// Last character of a comment, '\n' for a line comment, '/' for a block comment.
943     EndComment,
944     /// Start of a mutlitine string
945     StartString,
946     /// End of a mutlitine string
947     EndString,
948     /// Inside a string.
949     InString,
950 }
951
952 impl FullCodeCharKind {
953     pub fn is_comment(self) -> bool {
954         match self {
955             FullCodeCharKind::StartComment
956             | FullCodeCharKind::InComment
957             | FullCodeCharKind::EndComment => true,
958             _ => false,
959         }
960     }
961
962     pub fn is_string(self) -> bool {
963         self == FullCodeCharKind::InString || self == FullCodeCharKind::StartString
964     }
965
966     fn to_codecharkind(self) -> CodeCharKind {
967         if self.is_comment() {
968             CodeCharKind::Comment
969         } else {
970             CodeCharKind::Normal
971         }
972     }
973 }
974
975 impl<T> CharClasses<T>
976 where
977     T: Iterator,
978     T::Item: RichChar,
979 {
980     pub fn new(base: T) -> CharClasses<T> {
981         CharClasses {
982             base: multipeek(base),
983             status: CharClassesStatus::Normal,
984         }
985     }
986 }
987
988 fn is_raw_string_suffix<T>(iter: &mut MultiPeek<T>, count: u32) -> bool
989 where
990     T: Iterator,
991     T::Item: RichChar,
992 {
993     for _ in 0..count {
994         match iter.peek() {
995             Some(c) if c.get_char() == '#' => continue,
996             _ => return false,
997         }
998     }
999     true
1000 }
1001
1002 impl<T> Iterator for CharClasses<T>
1003 where
1004     T: Iterator,
1005     T::Item: RichChar,
1006 {
1007     type Item = (FullCodeCharKind, T::Item);
1008
1009     fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> {
1010         let item = self.base.next()?;
1011         let chr = item.get_char();
1012         let mut char_kind = FullCodeCharKind::Normal;
1013         self.status = match self.status {
1014             CharClassesStatus::LitRawString(sharps) => {
1015                 char_kind = FullCodeCharKind::InString;
1016                 match chr {
1017                     '"' => {
1018                         if sharps == 0 {
1019                             char_kind = FullCodeCharKind::Normal;
1020                             CharClassesStatus::Normal
1021                         } else if is_raw_string_suffix(&mut self.base, sharps) {
1022                             CharClassesStatus::RawStringSuffix(sharps)
1023                         } else {
1024                             CharClassesStatus::LitRawString(sharps)
1025                         }
1026                     }
1027                     _ => CharClassesStatus::LitRawString(sharps),
1028                 }
1029             }
1030             CharClassesStatus::RawStringPrefix(sharps) => {
1031                 char_kind = FullCodeCharKind::InString;
1032                 match chr {
1033                     '#' => CharClassesStatus::RawStringPrefix(sharps + 1),
1034                     '"' => CharClassesStatus::LitRawString(sharps),
1035                     _ => CharClassesStatus::Normal, // Unreachable.
1036                 }
1037             }
1038             CharClassesStatus::RawStringSuffix(sharps) => {
1039                 match chr {
1040                     '#' => {
1041                         if sharps == 1 {
1042                             CharClassesStatus::Normal
1043                         } else {
1044                             char_kind = FullCodeCharKind::InString;
1045                             CharClassesStatus::RawStringSuffix(sharps - 1)
1046                         }
1047                     }
1048                     _ => CharClassesStatus::Normal, // Unreachable
1049                 }
1050             }
1051             CharClassesStatus::LitString => {
1052                 char_kind = FullCodeCharKind::InString;
1053                 match chr {
1054                     '"' => CharClassesStatus::Normal,
1055                     '\\' => CharClassesStatus::LitStringEscape,
1056                     _ => CharClassesStatus::LitString,
1057                 }
1058             }
1059             CharClassesStatus::LitStringEscape => {
1060                 char_kind = FullCodeCharKind::InString;
1061                 CharClassesStatus::LitString
1062             }
1063             CharClassesStatus::LitChar => match chr {
1064                 '\\' => CharClassesStatus::LitCharEscape,
1065                 '\'' => CharClassesStatus::Normal,
1066                 _ => CharClassesStatus::LitChar,
1067             },
1068             CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar,
1069             CharClassesStatus::Normal => match chr {
1070                 'r' => match self.base.peek().map(|c| c.get_char()) {
1071                     Some('#') | Some('"') => {
1072                         char_kind = FullCodeCharKind::InString;
1073                         CharClassesStatus::RawStringPrefix(0)
1074                     }
1075                     _ => CharClassesStatus::Normal,
1076                 },
1077                 '"' => {
1078                     char_kind = FullCodeCharKind::InString;
1079                     CharClassesStatus::LitString
1080                 }
1081                 '\'' => {
1082                     // HACK: Work around mut borrow.
1083                     match self.base.peek() {
1084                         Some(next) if next.get_char() == '\\' => {
1085                             self.status = CharClassesStatus::LitChar;
1086                             return Some((char_kind, item));
1087                         }
1088                         _ => (),
1089                     }
1090
1091                     match self.base.peek() {
1092                         Some(next) if next.get_char() == '\'' => CharClassesStatus::LitChar,
1093                         _ => CharClassesStatus::Normal,
1094                     }
1095                 }
1096                 '/' => match self.base.peek() {
1097                     Some(next) if next.get_char() == '*' => {
1098                         self.status = CharClassesStatus::BlockCommentOpening(1);
1099                         return Some((FullCodeCharKind::StartComment, item));
1100                     }
1101                     Some(next) if next.get_char() == '/' => {
1102                         self.status = CharClassesStatus::LineComment;
1103                         return Some((FullCodeCharKind::StartComment, item));
1104                     }
1105                     _ => CharClassesStatus::Normal,
1106                 },
1107                 _ => CharClassesStatus::Normal,
1108             },
1109             CharClassesStatus::BlockComment(deepness) => {
1110                 assert_ne!(deepness, 0);
1111                 self.status = match self.base.peek() {
1112                     Some(next) if next.get_char() == '/' && chr == '*' => {
1113                         CharClassesStatus::BlockCommentClosing(deepness - 1)
1114                     }
1115                     Some(next) if next.get_char() == '*' && chr == '/' => {
1116                         CharClassesStatus::BlockCommentOpening(deepness + 1)
1117                     }
1118                     _ => CharClassesStatus::BlockComment(deepness),
1119                 };
1120                 return Some((FullCodeCharKind::InComment, item));
1121             }
1122             CharClassesStatus::BlockCommentOpening(deepness) => {
1123                 assert_eq!(chr, '*');
1124                 self.status = CharClassesStatus::BlockComment(deepness);
1125                 return Some((FullCodeCharKind::InComment, item));
1126             }
1127             CharClassesStatus::BlockCommentClosing(deepness) => {
1128                 assert_eq!(chr, '/');
1129                 if deepness == 0 {
1130                     self.status = CharClassesStatus::Normal;
1131                     return Some((FullCodeCharKind::EndComment, item));
1132                 } else {
1133                     self.status = CharClassesStatus::BlockComment(deepness);
1134                     return Some((FullCodeCharKind::InComment, item));
1135                 }
1136             }
1137             CharClassesStatus::LineComment => match chr {
1138                 '\n' => {
1139                     self.status = CharClassesStatus::Normal;
1140                     return Some((FullCodeCharKind::EndComment, item));
1141                 }
1142                 _ => {
1143                     self.status = CharClassesStatus::LineComment;
1144                     return Some((FullCodeCharKind::InComment, item));
1145                 }
1146             },
1147         };
1148         Some((char_kind, item))
1149     }
1150 }
1151
1152 /// An iterator over the lines of a string, paired with the char kind at the
1153 /// end of the line.
1154 pub struct LineClasses<'a> {
1155     base: iter::Peekable<CharClasses<std::str::Chars<'a>>>,
1156     kind: FullCodeCharKind,
1157 }
1158
1159 impl<'a> LineClasses<'a> {
1160     pub fn new(s: &'a str) -> Self {
1161         LineClasses {
1162             base: CharClasses::new(s.chars()).peekable(),
1163             kind: FullCodeCharKind::Normal,
1164         }
1165     }
1166 }
1167
1168 impl<'a> Iterator for LineClasses<'a> {
1169     type Item = (FullCodeCharKind, String);
1170
1171     fn next(&mut self) -> Option<Self::Item> {
1172         self.base.peek()?;
1173
1174         let mut line = String::new();
1175
1176         let start_class = match self.base.peek() {
1177             Some((kind, _)) => *kind,
1178             None => FullCodeCharKind::Normal,
1179         };
1180
1181         while let Some((kind, c)) = self.base.next() {
1182             if c == '\n' {
1183                 self.kind = match (start_class, kind) {
1184                     (FullCodeCharKind::Normal, FullCodeCharKind::InString) => {
1185                         FullCodeCharKind::StartString
1186                     }
1187                     (FullCodeCharKind::InString, FullCodeCharKind::Normal) => {
1188                         FullCodeCharKind::EndString
1189                     }
1190                     _ => kind,
1191                 };
1192                 break;
1193             } else {
1194                 line.push(c);
1195             }
1196         }
1197
1198         Some((self.kind, line))
1199     }
1200 }
1201
1202 /// Iterator over functional and commented parts of a string. Any part of a string is either
1203 /// functional code, either *one* block comment, either *one* line comment. Whitespace between
1204 /// comments is functional code. Line comments contain their ending newlines.
1205 struct UngroupedCommentCodeSlices<'a> {
1206     slice: &'a str,
1207     iter: iter::Peekable<CharClasses<std::str::CharIndices<'a>>>,
1208 }
1209
1210 impl<'a> UngroupedCommentCodeSlices<'a> {
1211     fn new(code: &'a str) -> UngroupedCommentCodeSlices<'a> {
1212         UngroupedCommentCodeSlices {
1213             slice: code,
1214             iter: CharClasses::new(code.char_indices()).peekable(),
1215         }
1216     }
1217 }
1218
1219 impl<'a> Iterator for UngroupedCommentCodeSlices<'a> {
1220     type Item = (CodeCharKind, usize, &'a str);
1221
1222     fn next(&mut self) -> Option<Self::Item> {
1223         let (kind, (start_idx, _)) = self.iter.next()?;
1224         match kind {
1225             FullCodeCharKind::Normal | FullCodeCharKind::InString => {
1226                 // Consume all the Normal code
1227                 while let Some(&(char_kind, _)) = self.iter.peek() {
1228                     if char_kind.is_comment() {
1229                         break;
1230                     }
1231                     let _ = self.iter.next();
1232                 }
1233             }
1234             FullCodeCharKind::StartComment => {
1235                 // Consume the whole comment
1236                 while let Some((FullCodeCharKind::InComment, (_, _))) = self.iter.next() {}
1237             }
1238             _ => panic!(),
1239         }
1240         let slice = match self.iter.peek() {
1241             Some(&(_, (end_idx, _))) => &self.slice[start_idx..end_idx],
1242             None => &self.slice[start_idx..],
1243         };
1244         Some((
1245             if kind.is_comment() {
1246                 CodeCharKind::Comment
1247             } else {
1248                 CodeCharKind::Normal
1249             },
1250             start_idx,
1251             slice,
1252         ))
1253     }
1254 }
1255
1256 /// Iterator over an alternating sequence of functional and commented parts of
1257 /// a string. The first item is always a, possibly zero length, subslice of
1258 /// functional text. Line style comments contain their ending newlines.
1259 pub struct CommentCodeSlices<'a> {
1260     slice: &'a str,
1261     last_slice_kind: CodeCharKind,
1262     last_slice_end: usize,
1263 }
1264
1265 impl<'a> CommentCodeSlices<'a> {
1266     pub fn new(slice: &'a str) -> CommentCodeSlices<'a> {
1267         CommentCodeSlices {
1268             slice,
1269             last_slice_kind: CodeCharKind::Comment,
1270             last_slice_end: 0,
1271         }
1272     }
1273 }
1274
1275 impl<'a> Iterator for CommentCodeSlices<'a> {
1276     type Item = (CodeCharKind, usize, &'a str);
1277
1278     fn next(&mut self) -> Option<Self::Item> {
1279         if self.last_slice_end == self.slice.len() {
1280             return None;
1281         }
1282
1283         let mut sub_slice_end = self.last_slice_end;
1284         let mut first_whitespace = None;
1285         let subslice = &self.slice[self.last_slice_end..];
1286         let mut iter = CharClasses::new(subslice.char_indices());
1287
1288         for (kind, (i, c)) in &mut iter {
1289             let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal
1290                 && &subslice[..2] == "//"
1291                 && [' ', '\t'].contains(&c);
1292
1293             if is_comment_connector && first_whitespace.is_none() {
1294                 first_whitespace = Some(i);
1295             }
1296
1297             if kind.to_codecharkind() == self.last_slice_kind && !is_comment_connector {
1298                 let last_index = match first_whitespace {
1299                     Some(j) => j,
1300                     None => i,
1301                 };
1302                 sub_slice_end = self.last_slice_end + last_index;
1303                 break;
1304             }
1305
1306             if !is_comment_connector {
1307                 first_whitespace = None;
1308             }
1309         }
1310
1311         if let (None, true) = (iter.next(), sub_slice_end == self.last_slice_end) {
1312             // This was the last subslice.
1313             sub_slice_end = match first_whitespace {
1314                 Some(i) => self.last_slice_end + i,
1315                 None => self.slice.len(),
1316             };
1317         }
1318
1319         let kind = match self.last_slice_kind {
1320             CodeCharKind::Comment => CodeCharKind::Normal,
1321             CodeCharKind::Normal => CodeCharKind::Comment,
1322         };
1323         let res = (
1324             kind,
1325             self.last_slice_end,
1326             &self.slice[self.last_slice_end..sub_slice_end],
1327         );
1328         self.last_slice_end = sub_slice_end;
1329         self.last_slice_kind = kind;
1330
1331         Some(res)
1332     }
1333 }
1334
1335 /// Checks is `new` didn't miss any comment from `span`, if it removed any, return previous text
1336 /// (if it fits in the width/offset, else return None), else return `new`
1337 pub fn recover_comment_removed(
1338     new: String,
1339     span: Span,
1340     context: &RewriteContext,
1341 ) -> Option<String> {
1342     let snippet = context.snippet(span);
1343     if snippet != new && changed_comment_content(snippet, &new) {
1344         // We missed some comments. Warn and keep the original text.
1345         if context.config.error_on_unformatted() {
1346             context.report.append(
1347                 context.source_map.span_to_filename(span).into(),
1348                 vec![FormattingError::from_span(
1349                     span,
1350                     &context.source_map,
1351                     ErrorKind::LostComment,
1352                 )],
1353             );
1354         }
1355         Some(snippet.to_owned())
1356     } else {
1357         Some(new)
1358     }
1359 }
1360
1361 pub fn filter_normal_code(code: &str) -> String {
1362     let mut buffer = String::with_capacity(code.len());
1363     LineClasses::new(code).for_each(|(kind, line)| match kind {
1364         FullCodeCharKind::Normal
1365         | FullCodeCharKind::StartString
1366         | FullCodeCharKind::InString
1367         | FullCodeCharKind::EndString => {
1368             buffer.push_str(&line);
1369             buffer.push('\n');
1370         }
1371         _ => (),
1372     });
1373     if !code.ends_with('\n') && buffer.ends_with('\n') {
1374         buffer.pop();
1375     }
1376     buffer
1377 }
1378
1379 /// Return true if the two strings of code have the same payload of comments.
1380 /// The payload of comments is everything in the string except:
1381 ///     - actual code (not comments)
1382 ///     - comment start/end marks
1383 ///     - whitespace
1384 ///     - '*' at the beginning of lines in block comments
1385 fn changed_comment_content(orig: &str, new: &str) -> bool {
1386     // Cannot write this as a fn since we cannot return types containing closures
1387     let code_comment_content = |code| {
1388         let slices = UngroupedCommentCodeSlices::new(code);
1389         slices
1390             .filter(|&(ref kind, _, _)| *kind == CodeCharKind::Comment)
1391             .flat_map(|(_, _, s)| CommentReducer::new(s))
1392     };
1393     let res = code_comment_content(orig).ne(code_comment_content(new));
1394     debug!(
1395         "comment::changed_comment_content: {}\norig: '{}'\nnew: '{}'\nraw_old: {}\nraw_new: {}",
1396         res,
1397         orig,
1398         new,
1399         code_comment_content(orig).collect::<String>(),
1400         code_comment_content(new).collect::<String>()
1401     );
1402     res
1403 }
1404
1405 /// Iterator over the 'payload' characters of a comment.
1406 /// It skips whitespace, comment start/end marks, and '*' at the beginning of lines.
1407 /// The comment must be one comment, ie not more than one start mark (no multiple line comments,
1408 /// for example).
1409 struct CommentReducer<'a> {
1410     is_block: bool,
1411     at_start_line: bool,
1412     iter: std::str::Chars<'a>,
1413 }
1414
1415 impl<'a> CommentReducer<'a> {
1416     fn new(comment: &'a str) -> CommentReducer<'a> {
1417         let is_block = comment.starts_with("/*");
1418         let comment = remove_comment_header(comment);
1419         CommentReducer {
1420             is_block,
1421             at_start_line: false, // There are no supplementary '*' on the first line
1422             iter: comment.chars(),
1423         }
1424     }
1425 }
1426
1427 impl<'a> Iterator for CommentReducer<'a> {
1428     type Item = char;
1429
1430     fn next(&mut self) -> Option<Self::Item> {
1431         loop {
1432             let mut c = self.iter.next()?;
1433             if self.is_block && self.at_start_line {
1434                 while c.is_whitespace() {
1435                     c = self.iter.next()?;
1436                 }
1437                 // Ignore leading '*'
1438                 if c == '*' {
1439                     c = self.iter.next()?;
1440                 }
1441             } else if c == '\n' {
1442                 self.at_start_line = true;
1443             }
1444             if !c.is_whitespace() {
1445                 return Some(c);
1446             }
1447         }
1448     }
1449 }
1450
1451 fn remove_comment_header(comment: &str) -> &str {
1452     if comment.starts_with("///") || comment.starts_with("//!") {
1453         &comment[3..]
1454     } else if comment.starts_with("//") {
1455         &comment[2..]
1456     } else if (comment.starts_with("/**") && !comment.starts_with("/**/"))
1457         || comment.starts_with("/*!")
1458     {
1459         &comment[3..comment.len() - 2]
1460     } else {
1461         assert!(
1462             comment.starts_with("/*"),
1463             format!("string '{}' is not a comment", comment)
1464         );
1465         &comment[2..comment.len() - 2]
1466     }
1467 }
1468
1469 #[cfg(test)]
1470 mod test {
1471     use super::*;
1472     use shape::{Indent, Shape};
1473
1474     #[test]
1475     fn char_classes() {
1476         let mut iter = CharClasses::new("//\n\n".chars());
1477
1478         assert_eq!((FullCodeCharKind::StartComment, '/'), iter.next().unwrap());
1479         assert_eq!((FullCodeCharKind::InComment, '/'), iter.next().unwrap());
1480         assert_eq!((FullCodeCharKind::EndComment, '\n'), iter.next().unwrap());
1481         assert_eq!((FullCodeCharKind::Normal, '\n'), iter.next().unwrap());
1482         assert_eq!(None, iter.next());
1483     }
1484
1485     #[test]
1486     fn comment_code_slices() {
1487         let input = "code(); /* test */ 1 + 1";
1488         let mut iter = CommentCodeSlices::new(input);
1489
1490         assert_eq!((CodeCharKind::Normal, 0, "code(); "), iter.next().unwrap());
1491         assert_eq!(
1492             (CodeCharKind::Comment, 8, "/* test */"),
1493             iter.next().unwrap()
1494         );
1495         assert_eq!((CodeCharKind::Normal, 18, " 1 + 1"), iter.next().unwrap());
1496         assert_eq!(None, iter.next());
1497     }
1498
1499     #[test]
1500     fn comment_code_slices_two() {
1501         let input = "// comment\n    test();";
1502         let mut iter = CommentCodeSlices::new(input);
1503
1504         assert_eq!((CodeCharKind::Normal, 0, ""), iter.next().unwrap());
1505         assert_eq!(
1506             (CodeCharKind::Comment, 0, "// comment\n"),
1507             iter.next().unwrap()
1508         );
1509         assert_eq!(
1510             (CodeCharKind::Normal, 11, "    test();"),
1511             iter.next().unwrap()
1512         );
1513         assert_eq!(None, iter.next());
1514     }
1515
1516     #[test]
1517     fn comment_code_slices_three() {
1518         let input = "1 // comment\n    // comment2\n\n";
1519         let mut iter = CommentCodeSlices::new(input);
1520
1521         assert_eq!((CodeCharKind::Normal, 0, "1 "), iter.next().unwrap());
1522         assert_eq!(
1523             (CodeCharKind::Comment, 2, "// comment\n    // comment2\n"),
1524             iter.next().unwrap()
1525         );
1526         assert_eq!((CodeCharKind::Normal, 29, "\n"), iter.next().unwrap());
1527         assert_eq!(None, iter.next());
1528     }
1529
1530     #[test]
1531     #[rustfmt::skip]
1532     fn format_comments() {
1533         let mut wrap_normalize_config: ::config::Config = Default::default();
1534         wrap_normalize_config.set().wrap_comments(true);
1535         wrap_normalize_config.set().normalize_comments(true);
1536
1537         let mut wrap_config: ::config::Config = Default::default();
1538         wrap_config.set().wrap_comments(true);
1539
1540         let comment = rewrite_comment(" //test",
1541                                       true,
1542                                       Shape::legacy(100, Indent::new(0, 100)),
1543                                       &wrap_normalize_config).unwrap();
1544         assert_eq!("/* test */", comment);
1545
1546         let comment = rewrite_comment("// comment on a",
1547                                       false,
1548                                       Shape::legacy(10, Indent::empty()),
1549                                       &wrap_normalize_config).unwrap();
1550         assert_eq!("// comment\n// on a", comment);
1551
1552         let comment = rewrite_comment("//  A multi line comment\n             // between args.",
1553                                       false,
1554                                       Shape::legacy(60, Indent::new(0, 12)),
1555                                       &wrap_normalize_config).unwrap();
1556         assert_eq!("//  A multi line comment\n            // between args.", comment);
1557
1558         let input = "// comment";
1559         let expected =
1560             "/* comment */";
1561         let comment = rewrite_comment(input,
1562                                       true,
1563                                       Shape::legacy(9, Indent::new(0, 69)),
1564                                       &wrap_normalize_config).unwrap();
1565         assert_eq!(expected, comment);
1566
1567         let comment = rewrite_comment("/*   trimmed    */",
1568                                       true,
1569                                       Shape::legacy(100, Indent::new(0, 100)),
1570                                       &wrap_normalize_config).unwrap();
1571         assert_eq!("/* trimmed */", comment);
1572
1573         // check that different comment style are properly recognised
1574         let comment = rewrite_comment(r#"/// test1
1575                                          /// test2
1576                                          /*
1577                                           * test3
1578                                           */"#,
1579                                       false,
1580                                       Shape::legacy(100, Indent::new(0, 0)),
1581                                       &wrap_normalize_config).unwrap();
1582         assert_eq!("/// test1\n/// test2\n// test3", comment);
1583
1584         // check that the blank line marks the end of a commented paragraph
1585         let comment = rewrite_comment(r#"// test1
1586
1587                                          // test2"#,
1588                                       false,
1589                                       Shape::legacy(100, Indent::new(0, 0)),
1590                                       &wrap_normalize_config).unwrap();
1591         assert_eq!("// test1\n\n// test2", comment);
1592
1593         // check that the blank line marks the end of a custom-commented paragraph
1594         let comment = rewrite_comment(r#"//@ test1
1595
1596                                          //@ test2"#,
1597                                       false,
1598                                       Shape::legacy(100, Indent::new(0, 0)),
1599                                       &wrap_normalize_config).unwrap();
1600         assert_eq!("//@ test1\n\n//@ test2", comment);
1601
1602         // check that bare lines are just indented but left unchanged otherwise
1603         let comment = rewrite_comment(r#"// test1
1604                                          /*
1605                                            a bare line!
1606
1607                                                 another bare line!
1608                                           */"#,
1609                                       false,
1610                                       Shape::legacy(100, Indent::new(0, 0)),
1611                                       &wrap_config).unwrap();
1612         assert_eq!("// test1\n/*\n a bare line!\n\n      another bare line!\n*/", comment);
1613     }
1614
1615     // This is probably intended to be a non-test fn, but it is not used. I'm
1616     // keeping it around unless it helps us test stuff.
1617     fn uncommented(text: &str) -> String {
1618         CharClasses::new(text.chars())
1619             .filter_map(|(s, c)| match s {
1620                 FullCodeCharKind::Normal | FullCodeCharKind::InString => Some(c),
1621                 _ => None,
1622             })
1623             .collect()
1624     }
1625
1626     #[test]
1627     fn test_uncommented() {
1628         assert_eq!(&uncommented("abc/*...*/"), "abc");
1629         assert_eq!(
1630             &uncommented("// .... /* \n../* /* *** / */ */a/* // */c\n"),
1631             "..ac\n"
1632         );
1633         assert_eq!(&uncommented("abc \" /* */\" qsdf"), "abc \" /* */\" qsdf");
1634     }
1635
1636     #[test]
1637     fn test_contains_comment() {
1638         assert_eq!(contains_comment("abc"), false);
1639         assert_eq!(contains_comment("abc // qsdf"), true);
1640         assert_eq!(contains_comment("abc /* kqsdf"), true);
1641         assert_eq!(contains_comment("abc \" /* */\" qsdf"), false);
1642     }
1643
1644     #[test]
1645     fn test_find_uncommented() {
1646         fn check(haystack: &str, needle: &str, expected: Option<usize>) {
1647             assert_eq!(expected, haystack.find_uncommented(needle));
1648         }
1649
1650         check("/*/ */test", "test", Some(6));
1651         check("//test\ntest", "test", Some(7));
1652         check("/* comment only */", "whatever", None);
1653         check(
1654             "/* comment */ some text /* more commentary */ result",
1655             "result",
1656             Some(46),
1657         );
1658         check("sup // sup", "p", Some(2));
1659         check("sup", "x", None);
1660         check(r#"π? /**/ π is nice!"#, r#"π is nice"#, Some(9));
1661         check("/*sup yo? \n sup*/ sup", "p", Some(20));
1662         check("hel/*lohello*/lo", "hello", None);
1663         check("acb", "ab", None);
1664         check(",/*A*/ ", ",", Some(0));
1665         check("abc", "abc", Some(0));
1666         check("/* abc */", "abc", None);
1667         check("/**/abc/* */", "abc", Some(4));
1668         check("\"/* abc */\"", "abc", Some(4));
1669         check("\"/* abc", "abc", Some(4));
1670     }
1671
1672     #[test]
1673     fn test_remove_trailing_white_spaces() {
1674         let s = "    r#\"\n        test\n    \"#";
1675         assert_eq!(remove_trailing_white_spaces(&s), s);
1676     }
1677
1678     #[test]
1679     fn test_filter_normal_code() {
1680         let s = r#"
1681 fn main() {
1682     println!("hello, world");
1683 }
1684 "#;
1685         assert_eq!(s, filter_normal_code(s));
1686         let s_with_comment = r#"
1687 fn main() {
1688     // hello, world
1689     println!("hello, world");
1690 }
1691 "#;
1692         assert_eq!(s, filter_normal_code(s_with_comment));
1693     }
1694 }