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.
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.
11 // Formatting and tools for comments.
13 use std::{self, borrow::Cow, iter};
15 use itertools::{multipeek, MultiPeek};
16 use syntax::codemap::Span;
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};
25 fn is_custom_comment(comment: &str) -> bool {
26 if !comment.starts_with("//") {
28 } else if let Some(c) = comment.chars().nth(2) {
29 !c.is_alphanumeric() && !c.is_whitespace()
35 #[derive(Copy, Clone, PartialEq, Eq)]
36 pub enum CommentStyle<'a> {
46 fn custom_opener(s: &str) -> &str {
47 s.lines().next().map_or("", |first_line| {
50 .map_or(first_line, |space_index| &first_line[0..space_index + 1])
54 impl<'a> CommentStyle<'a> {
55 pub fn is_doc_comment(&self) -> bool {
57 CommentStyle::TripleSlash | CommentStyle::Doc => true,
62 pub fn opener(&self) -> &'a str {
64 CommentStyle::DoubleSlash => "// ",
65 CommentStyle::TripleSlash => "/// ",
66 CommentStyle::Doc => "//! ",
67 CommentStyle::SingleBullet => "/* ",
68 CommentStyle::DoubleBullet => "/** ",
69 CommentStyle::Exclamation => "/*! ",
70 CommentStyle::Custom(opener) => opener,
74 pub fn closer(&self) -> &'a str {
76 CommentStyle::DoubleSlash
77 | CommentStyle::TripleSlash
78 | CommentStyle::Custom(..)
79 | CommentStyle::Doc => "",
80 CommentStyle::DoubleBullet => " **/",
81 CommentStyle::SingleBullet | CommentStyle::Exclamation => " */",
85 pub fn line_start(&self) -> &'a str {
87 CommentStyle::DoubleSlash => "// ",
88 CommentStyle::TripleSlash => "/// ",
89 CommentStyle::Doc => "//! ",
90 CommentStyle::SingleBullet | CommentStyle::Exclamation => " * ",
91 CommentStyle::DoubleBullet => " ** ",
92 CommentStyle::Custom(opener) => opener,
96 pub fn to_str_tuplet(&self) -> (&'a str, &'a str, &'a str) {
97 (self.opener(), self.closer(), self.line_start())
100 pub fn line_with_same_comment_style(&self, line: &str, normalize_comments: bool) -> bool {
102 CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => {
103 line.trim_left().starts_with(self.line_start().trim_left())
104 || comment_style(line, normalize_comments) == *self
106 CommentStyle::DoubleBullet | CommentStyle::SingleBullet | CommentStyle::Exclamation => {
107 line.trim_left().starts_with(self.closer().trim_left())
108 || line.trim_left().starts_with(self.line_start().trim_left())
109 || comment_style(line, normalize_comments) == *self
111 CommentStyle::Custom(opener) => line.trim_left().starts_with(opener.trim_right()),
116 fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle {
117 if !normalize_comments {
118 if orig.starts_with("/**") && !orig.starts_with("/**/") {
119 CommentStyle::DoubleBullet
120 } else if orig.starts_with("/*!") {
121 CommentStyle::Exclamation
122 } else if orig.starts_with("/*") {
123 CommentStyle::SingleBullet
124 } else if orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/') {
125 CommentStyle::TripleSlash
126 } else if orig.starts_with("//!") {
128 } else if is_custom_comment(orig) {
129 CommentStyle::Custom(custom_opener(orig))
131 CommentStyle::DoubleSlash
133 } else if (orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/'))
134 || (orig.starts_with("/**") && !orig.starts_with("/**/"))
136 CommentStyle::TripleSlash
137 } else if orig.starts_with("//!") || orig.starts_with("/*!") {
139 } else if is_custom_comment(orig) {
140 CommentStyle::Custom(custom_opener(orig))
142 CommentStyle::DoubleSlash
146 /// Combine `prev_str` and `next_str` into a single `String`. `span` may contain
147 /// comments between two strings. If there are such comments, then that will be
148 /// recovered. If `allow_extend` is true and there is no comment between the two
149 /// strings, then they will be put on a single line as long as doing so does not
150 /// exceed max width.
151 pub fn combine_strs_with_missing_comments(
152 context: &RewriteContext,
158 ) -> Option<String> {
160 String::with_capacity(prev_str.len() + next_str.len() + shape.indent.width() + 128);
161 result.push_str(prev_str);
162 let mut allow_one_line = !prev_str.contains('\n') && !next_str.contains('\n');
163 let first_sep = if prev_str.is_empty() || next_str.is_empty() {
168 let mut one_line_width =
169 last_line_width(prev_str) + first_line_width(next_str) + first_sep.len();
171 let config = context.config;
172 let indent = shape.indent;
173 let missing_comment = rewrite_missing_comment(span, shape, context)?;
175 if missing_comment.is_empty() {
176 if allow_extend && prev_str.len() + first_sep.len() + next_str.len() <= shape.width {
177 result.push_str(first_sep);
178 } else if !prev_str.is_empty() {
179 result.push_str(&indent.to_string_with_newline(config))
181 result.push_str(next_str);
185 // We have a missing comment between the first expression and the second expression.
187 // Peek the the original source code and find out whether there is a newline between the first
188 // expression and the second expression or the missing comment. We will preserve the original
189 // layout whenever possible.
190 let original_snippet = context.snippet(span);
191 let prefer_same_line = if let Some(pos) = original_snippet.find('/') {
192 !original_snippet[..pos].contains('\n')
194 !original_snippet.contains('\n')
197 one_line_width -= first_sep.len();
198 let first_sep = if prev_str.is_empty() || missing_comment.is_empty() {
201 let one_line_width = last_line_width(prev_str) + first_line_width(&missing_comment) + 1;
202 if prefer_same_line && one_line_width <= shape.width {
205 indent.to_string_with_newline(config)
208 result.push_str(&first_sep);
209 result.push_str(&missing_comment);
211 let second_sep = if missing_comment.is_empty() || next_str.is_empty() {
213 } else if missing_comment.starts_with("//") {
214 indent.to_string_with_newline(config)
216 one_line_width += missing_comment.len() + first_sep.len() + 1;
217 allow_one_line &= !missing_comment.starts_with("//") && !missing_comment.contains('\n');
218 if prefer_same_line && allow_one_line && one_line_width <= shape.width {
221 indent.to_string_with_newline(config)
224 result.push_str(&second_sep);
225 result.push_str(next_str);
230 pub fn rewrite_doc_comment(orig: &str, shape: Shape, config: &Config) -> Option<String> {
231 _rewrite_comment(orig, false, shape, config, true)
234 pub fn rewrite_comment(
239 ) -> Option<String> {
240 _rewrite_comment(orig, block_style, shape, config, false)
248 is_doc_comment: bool,
249 ) -> Option<String> {
250 // If there are lines without a starting sigil, we won't format them correctly
251 // so in that case we won't even re-align (if !config.normalize_comments()) and
252 // we should stop now.
253 let num_bare_lines = orig
255 .map(|line| line.trim())
256 .filter(|l| !(l.starts_with('*') || l.starts_with("//") || l.starts_with("/*")))
258 if num_bare_lines > 0 && !config.normalize_comments() {
259 return Some(orig.to_owned());
261 if !config.normalize_comments() && !config.wrap_comments() {
262 return light_rewrite_comment(orig, shape.indent, config, is_doc_comment);
265 identify_comment(orig, block_style, shape, config, is_doc_comment)
273 is_doc_comment: bool,
274 ) -> Option<String> {
275 let style = comment_style(orig, false);
276 let first_group = orig
278 .take_while(|l| style.line_with_same_comment_style(l, false))
283 .skip(first_group.lines().count())
287 let first_group_str = rewrite_comment_inner(
293 is_doc_comment || style.is_doc_comment(),
296 Some(first_group_str)
298 identify_comment(&rest, block_style, shape, config, is_doc_comment).map(|rest_str| {
302 shape.indent.to_string(config),
309 fn rewrite_comment_inner(
315 is_doc_comment: bool,
316 ) -> Option<String> {
317 let (opener, closer, line_start) = if block_style {
318 CommentStyle::SingleBullet.to_str_tuplet()
320 comment_style(orig, config.normalize_comments()).to_str_tuplet()
323 let max_chars = shape
325 .checked_sub(closer.len() + opener.len())
327 let indent_str = shape.indent.to_string_with_newline(config);
328 let fmt_indent = shape.indent + (opener.len() - line_start.len());
329 let mut fmt = StringFormat {
334 shape: Shape::legacy(max_chars, fmt_indent),
339 let line_breaks = count_newlines(orig.trim_right());
343 .map(|(i, mut line)| {
344 line = trim_right_unless_two_whitespaces(line.trim_left(), is_doc_comment);
346 if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
347 line = line[..(line.len() - 2)].trim_right();
352 .map(|s| left_trim_comment_line(s, &style))
353 .map(|(line, has_leading_whitespace)| {
354 if orig.starts_with("/*") && line_breaks == 0 {
357 has_leading_whitespace || config.normalize_comments(),
360 (line, has_leading_whitespace || config.normalize_comments())
364 let mut result = String::with_capacity(orig.len() * 2);
365 result.push_str(opener);
366 let mut code_block_buffer = String::with_capacity(128);
367 let mut is_prev_line_multi_line = false;
368 let mut inside_code_block = false;
369 let comment_line_separator = format!("{}{}", indent_str, line_start);
370 let join_code_block_with_comment_line_separator = |s: &str| {
371 let mut result = String::with_capacity(s.len() + 128);
372 let mut iter = s.lines().peekable();
373 while let Some(line) = iter.next() {
374 result.push_str(line);
375 result.push_str(match iter.peek() {
376 Some(next_line) if next_line.is_empty() => comment_line_separator.trim_right(),
377 Some(..) => &comment_line_separator,
384 for (i, (line, has_leading_whitespace)) in lines.enumerate() {
385 let is_last = i == count_newlines(orig);
387 if inside_code_block {
388 if line.starts_with("```") {
389 inside_code_block = false;
390 result.push_str(&comment_line_separator);
392 let mut config = config.clone();
393 config.set().wrap_comments(false);
394 match ::format_code_block(&code_block_buffer, &config) {
395 Some(ref s) => trim_custom_comment_prefix(s),
396 None => trim_custom_comment_prefix(&code_block_buffer),
399 result.push_str(&join_code_block_with_comment_line_separator(&code_block));
400 code_block_buffer.clear();
401 result.push_str(&comment_line_separator);
402 result.push_str(line);
404 code_block_buffer.push_str(&hide_sharp_behind_comment(line));
405 code_block_buffer.push('\n');
408 // There is an code block that is not properly enclosed by backticks.
409 // We will leave them untouched.
410 result.push_str(&comment_line_separator);
411 result.push_str(&join_code_block_with_comment_line_separator(
412 &trim_custom_comment_prefix(&code_block_buffer),
419 inside_code_block = line.starts_with("```");
421 if result == opener {
422 let force_leading_whitespace = opener == "/* " && count_newlines(orig) == 0;
423 if !has_leading_whitespace && !force_leading_whitespace && result.ends_with(' ') {
429 } else if is_prev_line_multi_line && !line.is_empty() {
431 } else if is_last && !closer.is_empty() && line.is_empty() {
432 result.push_str(&indent_str);
434 result.push_str(&comment_line_separator);
435 if !has_leading_whitespace && result.ends_with(' ') {
441 if config.wrap_comments() && line.len() > fmt.shape.width && !has_url(line) {
442 match rewrite_string(line, &fmt) {
444 is_prev_line_multi_line = s.contains('\n');
447 None if is_prev_line_multi_line => {
448 // We failed to put the current `line` next to the previous `line`.
449 // Remove the trailing space, then start rewrite on the next line.
451 result.push_str(&comment_line_separator);
452 fmt.shape = Shape::legacy(max_chars, fmt_indent);
453 match rewrite_string(line, &fmt) {
455 is_prev_line_multi_line = s.contains('\n');
459 is_prev_line_multi_line = false;
460 result.push_str(line);
465 is_prev_line_multi_line = false;
466 result.push_str(line);
470 fmt.shape = if is_prev_line_multi_line {
472 let offset = 1 + last_line_width(&result) - line_start.len();
474 width: max_chars.saturating_sub(offset),
476 offset: fmt.shape.offset + offset,
479 Shape::legacy(max_chars, fmt_indent)
482 if line.is_empty() && result.ends_with(' ') && !is_last {
483 // Remove space if this is an empty comment or a doc comment.
486 result.push_str(line);
487 fmt.shape = Shape::legacy(max_chars, fmt_indent);
488 is_prev_line_multi_line = false;
492 result.push_str(closer);
493 if result.ends_with(opener) && opener.ends_with(' ') {
501 const RUSTFMT_CUSTOM_COMMENT_PREFIX: &str = "//#### ";
503 fn hide_sharp_behind_comment<'a>(s: &'a str) -> Cow<'a, str> {
504 if s.trim_left().starts_with('#') {
505 Cow::from(format!("{}{}", RUSTFMT_CUSTOM_COMMENT_PREFIX, s))
511 fn trim_custom_comment_prefix(s: &str) -> String {
514 let left_trimmed = line.trim_left();
515 if left_trimmed.starts_with(RUSTFMT_CUSTOM_COMMENT_PREFIX) {
516 left_trimmed.trim_left_matches(RUSTFMT_CUSTOM_COMMENT_PREFIX)
525 /// Returns true if the given string MAY include URLs or alike.
526 fn has_url(s: &str) -> bool {
527 // This function may return false positive, but should get its job done in most cases.
528 s.contains("https://") || s.contains("http://") || s.contains("ftp://") || s.contains("file://")
531 /// Given the span, rewrite the missing comment inside it if available.
532 /// Note that the given span must only include comments (or leading/trailing whitespaces).
533 pub fn rewrite_missing_comment(
536 context: &RewriteContext,
537 ) -> Option<String> {
538 let missing_snippet = context.snippet(span);
539 let trimmed_snippet = missing_snippet.trim();
540 if !trimmed_snippet.is_empty() {
541 rewrite_comment(trimmed_snippet, false, shape, context.config)
547 /// Recover the missing comments in the specified span, if available.
548 /// The layout of the comments will be preserved as long as it does not break the code
549 /// and its total width does not exceed the max width.
550 pub fn recover_missing_comment_in_span(
553 context: &RewriteContext,
555 ) -> Option<String> {
556 let missing_comment = rewrite_missing_comment(span, shape, context)?;
557 if missing_comment.is_empty() {
560 let missing_snippet = context.snippet(span);
561 let pos = missing_snippet.find('/').unwrap_or(0);
563 let total_width = missing_comment.len() + used_width + 1;
564 let force_new_line_before_comment =
565 missing_snippet[..pos].contains('\n') || total_width > context.config.max_width();
566 let sep = if force_new_line_before_comment {
567 shape.indent.to_string_with_newline(context.config)
571 Some(format!("{}{}", sep, missing_comment))
575 /// Trim trailing whitespaces unless they consist of two or more whitespaces.
576 fn trim_right_unless_two_whitespaces(s: &str, is_doc_comment: bool) -> &str {
577 if is_doc_comment && s.ends_with(" ") {
584 /// Trims whitespace and aligns to indent, but otherwise does not change comments.
585 fn light_rewrite_comment(
589 is_doc_comment: bool,
590 ) -> Option<String> {
591 let lines: Vec<&str> = orig
594 // This is basically just l.trim(), but in the case that a line starts
595 // with `*` we want to leave one space before it, so it aligns with the
597 let first_non_whitespace = l.find(|c| !char::is_whitespace(c));
598 let left_trimmed = if let Some(fnw) = first_non_whitespace {
599 if l.as_bytes()[fnw] == b'*' && fnw > 0 {
607 // Preserve markdown's double-space line break syntax in doc comment.
608 trim_right_unless_two_whitespaces(left_trimmed, is_doc_comment)
611 Some(lines.join(&format!("\n{}", offset.to_string(config))))
614 /// Trims comment characters and possibly a single space from the left of a string.
615 /// Does not trim all whitespace. If a single space is trimmed from the left of the string,
616 /// this function returns true.
617 fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle) -> (&'a str, bool) {
618 if line.starts_with("//! ")
619 || line.starts_with("/// ")
620 || line.starts_with("/*! ")
621 || line.starts_with("/** ")
624 } else if let CommentStyle::Custom(opener) = *style {
625 if line.starts_with(opener) {
626 (&line[opener.len()..], true)
628 (&line[opener.trim_right().len()..], false)
630 } else if line.starts_with("/* ")
631 || line.starts_with("// ")
632 || line.starts_with("//!")
633 || line.starts_with("///")
634 || line.starts_with("** ")
635 || line.starts_with("/*!")
636 || (line.starts_with("/**") && !line.starts_with("/**/"))
638 (&line[3..], line.chars().nth(2).unwrap() == ' ')
639 } else if line.starts_with("/*")
640 || line.starts_with("* ")
641 || line.starts_with("//")
642 || line.starts_with("**")
644 (&line[2..], line.chars().nth(1).unwrap() == ' ')
645 } else if line.starts_with('*') {
648 (line, line.starts_with(' '))
652 pub trait FindUncommented {
653 fn find_uncommented(&self, pat: &str) -> Option<usize>;
656 impl FindUncommented for str {
657 fn find_uncommented(&self, pat: &str) -> Option<usize> {
658 let mut needle_iter = pat.chars();
659 for (kind, (i, b)) in CharClasses::new(self.char_indices()) {
660 match needle_iter.next() {
662 return Some(i - pat.len());
664 Some(c) => match kind {
665 FullCodeCharKind::Normal | FullCodeCharKind::InString if b == c => {}
667 needle_iter = pat.chars();
673 // Handle case where the pattern is a suffix of the search string
674 match needle_iter.next() {
676 None => Some(self.len() - pat.len()),
681 // Returns the first byte position after the first comment. The given string
682 // is expected to be prefixed by a comment, including delimiters.
683 // Good: "/* /* inner */ outer */ code();"
684 // Bad: "code(); // hello\n world!"
685 pub fn find_comment_end(s: &str) -> Option<usize> {
686 let mut iter = CharClasses::new(s.char_indices());
687 for (kind, (i, _c)) in &mut iter {
688 if kind == FullCodeCharKind::Normal || kind == FullCodeCharKind::InString {
693 // Handle case where the comment ends at the end of s.
694 if iter.status == CharClassesStatus::Normal {
701 /// Returns true if text contains any comment.
702 pub fn contains_comment(text: &str) -> bool {
703 CharClasses::new(text.chars()).any(|(kind, _)| kind.is_comment())
706 /// Remove trailing spaces from the specified snippet. We do not remove spaces
707 /// inside strings or comments.
708 pub fn remove_trailing_white_spaces(text: &str) -> String {
709 let mut buffer = String::with_capacity(text.len());
710 let mut space_buffer = String::with_capacity(128);
711 for (char_kind, c) in CharClasses::new(text.chars()) {
714 if char_kind == FullCodeCharKind::InString {
715 buffer.push_str(&space_buffer);
717 space_buffer.clear();
720 _ if c.is_whitespace() => {
721 space_buffer.push(c);
724 if !space_buffer.is_empty() {
725 buffer.push_str(&space_buffer);
726 space_buffer.clear();
735 pub struct CharClasses<T>
741 status: CharClassesStatus,
745 fn get_char(&self) -> char;
748 impl RichChar for char {
749 fn get_char(&self) -> char {
754 impl RichChar for (usize, char) {
755 fn get_char(&self) -> char {
760 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
761 enum CharClassesStatus {
767 // The u32 is the nesting deepness of the comment
769 // Status when the '/' has been consumed, but not yet the '*', deepness is
770 // the new deepness (after the comment opening).
771 BlockCommentOpening(u32),
772 // Status when the '*' has been consumed, but not yet the '/', deepness is
773 // the new deepness (after the comment closing).
774 BlockCommentClosing(u32),
778 /// Distinguish between functional part of code and comments
779 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
780 pub enum CodeCharKind {
785 /// Distinguish between functional part of code and comments,
786 /// describing opening and closing of comments for ease when chunking
787 /// code from tagged characters
788 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
789 pub enum FullCodeCharKind {
791 /// The first character of a comment, there is only one for a comment (always '/')
793 /// Any character inside a comment including the second character of comment
794 /// marks ("//", "/*")
796 /// Last character of a comment, '\n' for a line comment, '/' for a block comment.
802 impl FullCodeCharKind {
803 pub fn is_comment(&self) -> bool {
805 FullCodeCharKind::StartComment
806 | FullCodeCharKind::InComment
807 | FullCodeCharKind::EndComment => true,
812 pub fn is_string(&self) -> bool {
813 *self == FullCodeCharKind::InString
816 fn to_codecharkind(&self) -> CodeCharKind {
817 if self.is_comment() {
818 CodeCharKind::Comment
825 impl<T> CharClasses<T>
830 pub fn new(base: T) -> CharClasses<T> {
832 base: multipeek(base),
833 status: CharClassesStatus::Normal,
838 impl<T> Iterator for CharClasses<T>
843 type Item = (FullCodeCharKind, T::Item);
845 fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> {
846 let item = self.base.next()?;
847 let chr = item.get_char();
848 let mut char_kind = FullCodeCharKind::Normal;
849 self.status = match self.status {
850 CharClassesStatus::LitString => match chr {
851 '"' => CharClassesStatus::Normal,
853 char_kind = FullCodeCharKind::InString;
854 CharClassesStatus::LitStringEscape
857 char_kind = FullCodeCharKind::InString;
858 CharClassesStatus::LitString
861 CharClassesStatus::LitStringEscape => {
862 char_kind = FullCodeCharKind::InString;
863 CharClassesStatus::LitString
865 CharClassesStatus::LitChar => match chr {
866 '\\' => CharClassesStatus::LitCharEscape,
867 '\'' => CharClassesStatus::Normal,
868 _ => CharClassesStatus::LitChar,
870 CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar,
871 CharClassesStatus::Normal => match chr {
873 char_kind = FullCodeCharKind::InString;
874 CharClassesStatus::LitString
877 // HACK: Work around mut borrow.
878 match self.base.peek() {
879 Some(next) if next.get_char() == '\\' => {
880 self.status = CharClassesStatus::LitChar;
881 return Some((char_kind, item));
886 match self.base.peek() {
887 Some(next) if next.get_char() == '\'' => CharClassesStatus::LitChar,
888 _ => CharClassesStatus::Normal,
891 '/' => match self.base.peek() {
892 Some(next) if next.get_char() == '*' => {
893 self.status = CharClassesStatus::BlockCommentOpening(1);
894 return Some((FullCodeCharKind::StartComment, item));
896 Some(next) if next.get_char() == '/' => {
897 self.status = CharClassesStatus::LineComment;
898 return Some((FullCodeCharKind::StartComment, item));
900 _ => CharClassesStatus::Normal,
902 _ => CharClassesStatus::Normal,
904 CharClassesStatus::BlockComment(deepness) => {
905 assert_ne!(deepness, 0);
906 self.status = match self.base.peek() {
907 Some(next) if next.get_char() == '/' && chr == '*' => {
908 CharClassesStatus::BlockCommentClosing(deepness - 1)
910 Some(next) if next.get_char() == '*' && chr == '/' => {
911 CharClassesStatus::BlockCommentOpening(deepness + 1)
913 _ => CharClassesStatus::BlockComment(deepness),
915 return Some((FullCodeCharKind::InComment, item));
917 CharClassesStatus::BlockCommentOpening(deepness) => {
918 assert_eq!(chr, '*');
919 self.status = CharClassesStatus::BlockComment(deepness);
920 return Some((FullCodeCharKind::InComment, item));
922 CharClassesStatus::BlockCommentClosing(deepness) => {
923 assert_eq!(chr, '/');
925 self.status = CharClassesStatus::Normal;
926 return Some((FullCodeCharKind::EndComment, item));
928 self.status = CharClassesStatus::BlockComment(deepness);
929 return Some((FullCodeCharKind::InComment, item));
932 CharClassesStatus::LineComment => match chr {
934 self.status = CharClassesStatus::Normal;
935 return Some((FullCodeCharKind::EndComment, item));
938 self.status = CharClassesStatus::LineComment;
939 return Some((FullCodeCharKind::InComment, item));
943 Some((char_kind, item))
947 /// An iterator over the lines of a string, paired with the char kind at the
949 pub struct LineClasses<'a> {
950 base: iter::Peekable<CharClasses<std::str::Chars<'a>>>,
951 kind: FullCodeCharKind,
954 impl<'a> LineClasses<'a> {
955 pub fn new(s: &'a str) -> Self {
957 base: CharClasses::new(s.chars()).peekable(),
958 kind: FullCodeCharKind::Normal,
963 impl<'a> Iterator for LineClasses<'a> {
964 type Item = (FullCodeCharKind, String);
966 fn next(&mut self) -> Option<Self::Item> {
967 if self.base.peek().is_none() {
971 let mut line = String::new();
973 while let Some((kind, c)) = self.base.next() {
982 Some((self.kind, line))
986 /// Iterator over functional and commented parts of a string. Any part of a string is either
987 /// functional code, either *one* block comment, either *one* line comment. Whitespace between
988 /// comments is functional code. Line comments contain their ending newlines.
989 struct UngroupedCommentCodeSlices<'a> {
991 iter: iter::Peekable<CharClasses<std::str::CharIndices<'a>>>,
994 impl<'a> UngroupedCommentCodeSlices<'a> {
995 fn new(code: &'a str) -> UngroupedCommentCodeSlices<'a> {
996 UngroupedCommentCodeSlices {
998 iter: CharClasses::new(code.char_indices()).peekable(),
1003 impl<'a> Iterator for UngroupedCommentCodeSlices<'a> {
1004 type Item = (CodeCharKind, usize, &'a str);
1006 fn next(&mut self) -> Option<Self::Item> {
1007 let (kind, (start_idx, _)) = self.iter.next()?;
1009 FullCodeCharKind::Normal | FullCodeCharKind::InString => {
1010 // Consume all the Normal code
1011 while let Some(&(char_kind, _)) = self.iter.peek() {
1012 if char_kind.is_comment() {
1015 let _ = self.iter.next();
1018 FullCodeCharKind::StartComment => {
1019 // Consume the whole comment
1020 while let Some((FullCodeCharKind::InComment, (_, _))) = self.iter.next() {}
1024 let slice = match self.iter.peek() {
1025 Some(&(_, (end_idx, _))) => &self.slice[start_idx..end_idx],
1026 None => &self.slice[start_idx..],
1029 if kind.is_comment() {
1030 CodeCharKind::Comment
1032 CodeCharKind::Normal
1040 /// Iterator over an alternating sequence of functional and commented parts of
1041 /// a string. The first item is always a, possibly zero length, subslice of
1042 /// functional text. Line style comments contain their ending newlines.
1043 pub struct CommentCodeSlices<'a> {
1045 last_slice_kind: CodeCharKind,
1046 last_slice_end: usize,
1049 impl<'a> CommentCodeSlices<'a> {
1050 pub fn new(slice: &'a str) -> CommentCodeSlices<'a> {
1053 last_slice_kind: CodeCharKind::Comment,
1059 impl<'a> Iterator for CommentCodeSlices<'a> {
1060 type Item = (CodeCharKind, usize, &'a str);
1062 fn next(&mut self) -> Option<Self::Item> {
1063 if self.last_slice_end == self.slice.len() {
1067 let mut sub_slice_end = self.last_slice_end;
1068 let mut first_whitespace = None;
1069 let subslice = &self.slice[self.last_slice_end..];
1070 let mut iter = CharClasses::new(subslice.char_indices());
1072 for (kind, (i, c)) in &mut iter {
1073 let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal
1074 && &subslice[..2] == "//"
1075 && [' ', '\t'].contains(&c);
1077 if is_comment_connector && first_whitespace.is_none() {
1078 first_whitespace = Some(i);
1081 if kind.to_codecharkind() == self.last_slice_kind && !is_comment_connector {
1082 let last_index = match first_whitespace {
1086 sub_slice_end = self.last_slice_end + last_index;
1090 if !is_comment_connector {
1091 first_whitespace = None;
1095 if let (None, true) = (iter.next(), sub_slice_end == self.last_slice_end) {
1096 // This was the last subslice.
1097 sub_slice_end = match first_whitespace {
1098 Some(i) => self.last_slice_end + i,
1099 None => self.slice.len(),
1103 let kind = match self.last_slice_kind {
1104 CodeCharKind::Comment => CodeCharKind::Normal,
1105 CodeCharKind::Normal => CodeCharKind::Comment,
1109 self.last_slice_end,
1110 &self.slice[self.last_slice_end..sub_slice_end],
1112 self.last_slice_end = sub_slice_end;
1113 self.last_slice_kind = kind;
1119 /// Checks is `new` didn't miss any comment from `span`, if it removed any, return previous text
1120 /// (if it fits in the width/offset, else return None), else return `new`
1121 pub fn recover_comment_removed(
1124 context: &RewriteContext,
1125 ) -> Option<String> {
1126 let snippet = context.snippet(span);
1127 if snippet != new && changed_comment_content(snippet, &new) {
1128 // We missed some comments. Warn and keep the original text.
1129 if context.config.error_on_unformatted() {
1130 context.report.append(
1131 context.codemap.span_to_filename(span).into(),
1132 vec![FormattingError::from_span(
1135 ErrorKind::LostComment,
1139 Some(snippet.to_owned())
1145 /// Return true if the two strings of code have the same payload of comments.
1146 /// The payload of comments is everything in the string except:
1147 /// - actual code (not comments)
1148 /// - comment start/end marks
1150 /// - '*' at the beginning of lines in block comments
1151 fn changed_comment_content(orig: &str, new: &str) -> bool {
1152 // Cannot write this as a fn since we cannot return types containing closures
1153 let code_comment_content = |code| {
1154 let slices = UngroupedCommentCodeSlices::new(code);
1156 .filter(|&(ref kind, _, _)| *kind == CodeCharKind::Comment)
1157 .flat_map(|(_, _, s)| CommentReducer::new(s))
1159 let res = code_comment_content(orig).ne(code_comment_content(new));
1161 "comment::changed_comment_content: {}\norig: '{}'\nnew: '{}'\nraw_old: {}\nraw_new: {}",
1165 code_comment_content(orig).collect::<String>(),
1166 code_comment_content(new).collect::<String>()
1171 /// Iterator over the 'payload' characters of a comment.
1172 /// It skips whitespace, comment start/end marks, and '*' at the beginning of lines.
1173 /// The comment must be one comment, ie not more than one start mark (no multiple line comments,
1175 struct CommentReducer<'a> {
1177 at_start_line: bool,
1178 iter: std::str::Chars<'a>,
1181 impl<'a> CommentReducer<'a> {
1182 fn new(comment: &'a str) -> CommentReducer<'a> {
1183 let is_block = comment.starts_with("/*");
1184 let comment = remove_comment_header(comment);
1187 at_start_line: false, // There are no supplementary '*' on the first line
1188 iter: comment.chars(),
1193 impl<'a> Iterator for CommentReducer<'a> {
1196 fn next(&mut self) -> Option<Self::Item> {
1198 let mut c = self.iter.next()?;
1199 if self.is_block && self.at_start_line {
1200 while c.is_whitespace() {
1201 c = self.iter.next()?;
1203 // Ignore leading '*'
1205 c = self.iter.next()?;
1207 } else if c == '\n' {
1208 self.at_start_line = true;
1210 if !c.is_whitespace() {
1217 fn remove_comment_header(comment: &str) -> &str {
1218 if comment.starts_with("///") || comment.starts_with("//!") {
1220 } else if comment.starts_with("//") {
1222 } else if (comment.starts_with("/**") && !comment.starts_with("/**/"))
1223 || comment.starts_with("/*!")
1225 &comment[3..comment.len() - 2]
1228 comment.starts_with("/*"),
1229 format!("string '{}' is not a comment", comment)
1231 &comment[2..comment.len() - 2]
1238 use shape::{Indent, Shape};
1242 let mut iter = CharClasses::new("//\n\n".chars());
1244 assert_eq!((FullCodeCharKind::StartComment, '/'), iter.next().unwrap());
1245 assert_eq!((FullCodeCharKind::InComment, '/'), iter.next().unwrap());
1246 assert_eq!((FullCodeCharKind::EndComment, '\n'), iter.next().unwrap());
1247 assert_eq!((FullCodeCharKind::Normal, '\n'), iter.next().unwrap());
1248 assert_eq!(None, iter.next());
1252 fn comment_code_slices() {
1253 let input = "code(); /* test */ 1 + 1";
1254 let mut iter = CommentCodeSlices::new(input);
1256 assert_eq!((CodeCharKind::Normal, 0, "code(); "), iter.next().unwrap());
1258 (CodeCharKind::Comment, 8, "/* test */"),
1259 iter.next().unwrap()
1261 assert_eq!((CodeCharKind::Normal, 18, " 1 + 1"), iter.next().unwrap());
1262 assert_eq!(None, iter.next());
1266 fn comment_code_slices_two() {
1267 let input = "// comment\n test();";
1268 let mut iter = CommentCodeSlices::new(input);
1270 assert_eq!((CodeCharKind::Normal, 0, ""), iter.next().unwrap());
1272 (CodeCharKind::Comment, 0, "// comment\n"),
1273 iter.next().unwrap()
1276 (CodeCharKind::Normal, 11, " test();"),
1277 iter.next().unwrap()
1279 assert_eq!(None, iter.next());
1283 fn comment_code_slices_three() {
1284 let input = "1 // comment\n // comment2\n\n";
1285 let mut iter = CommentCodeSlices::new(input);
1287 assert_eq!((CodeCharKind::Normal, 0, "1 "), iter.next().unwrap());
1289 (CodeCharKind::Comment, 2, "// comment\n // comment2\n"),
1290 iter.next().unwrap()
1292 assert_eq!((CodeCharKind::Normal, 29, "\n"), iter.next().unwrap());
1293 assert_eq!(None, iter.next());
1298 fn format_comments() {
1299 let mut config: ::config::Config = Default::default();
1300 config.set().wrap_comments(true);
1301 config.set().normalize_comments(true);
1303 let comment = rewrite_comment(" //test",
1305 Shape::legacy(100, Indent::new(0, 100)),
1307 assert_eq!("/* test */", comment);
1309 let comment = rewrite_comment("// comment on a",
1311 Shape::legacy(10, Indent::empty()),
1313 assert_eq!("// comment\n// on a", comment);
1315 let comment = rewrite_comment("// A multi line comment\n // between args.",
1317 Shape::legacy(60, Indent::new(0, 12)),
1319 assert_eq!("// A multi line comment\n // between args.", comment);
1321 let input = "// comment";
1324 let comment = rewrite_comment(input,
1326 Shape::legacy(9, Indent::new(0, 69)),
1328 assert_eq!(expected, comment);
1330 let comment = rewrite_comment("/* trimmed */",
1332 Shape::legacy(100, Indent::new(0, 100)),
1334 assert_eq!("/* trimmed */", comment);
1337 // This is probably intended to be a non-test fn, but it is not used. I'm
1338 // keeping it around unless it helps us test stuff.
1339 fn uncommented(text: &str) -> String {
1340 CharClasses::new(text.chars())
1341 .filter_map(|(s, c)| match s {
1342 FullCodeCharKind::Normal | FullCodeCharKind::InString => Some(c),
1349 fn test_uncommented() {
1350 assert_eq!(&uncommented("abc/*...*/"), "abc");
1352 &uncommented("// .... /* \n../* /* *** / */ */a/* // */c\n"),
1355 assert_eq!(&uncommented("abc \" /* */\" qsdf"), "abc \" /* */\" qsdf");
1359 fn test_contains_comment() {
1360 assert_eq!(contains_comment("abc"), false);
1361 assert_eq!(contains_comment("abc // qsdf"), true);
1362 assert_eq!(contains_comment("abc /* kqsdf"), true);
1363 assert_eq!(contains_comment("abc \" /* */\" qsdf"), false);
1367 fn test_find_uncommented() {
1368 fn check(haystack: &str, needle: &str, expected: Option<usize>) {
1369 assert_eq!(expected, haystack.find_uncommented(needle));
1372 check("/*/ */test", "test", Some(6));
1373 check("//test\ntest", "test", Some(7));
1374 check("/* comment only */", "whatever", None);
1376 "/* comment */ some text /* more commentary */ result",
1380 check("sup // sup", "p", Some(2));
1381 check("sup", "x", None);
1382 check(r#"π? /**/ π is nice!"#, r#"π is nice"#, Some(9));
1383 check("/*sup yo? \n sup*/ sup", "p", Some(20));
1384 check("hel/*lohello*/lo", "hello", None);
1385 check("acb", "ab", None);
1386 check(",/*A*/ ", ",", Some(0));
1387 check("abc", "abc", Some(0));
1388 check("/* abc */", "abc", None);
1389 check("/**/abc/* */", "abc", Some(4));
1390 check("\"/* abc */\"", "abc", Some(4));
1391 check("\"/* abc", "abc", Some(4));
1395 fn test_remove_trailing_white_spaces() {
1396 let s = format!(" r#\"\n test\n \"#");
1397 assert_eq!(remove_trailing_white_spaces(&s), s);