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