1 // Formatting and tools for comments.
3 use std
::{self, borrow::Cow, iter}
;
5 use itertools
::{multipeek, MultiPeek}
;
8 use crate::config
::Config
;
9 use crate::rewrite
::RewriteContext
;
10 use crate::shape
::{Indent, Shape}
;
11 use crate::string
::{rewrite_string, StringFormat}
;
13 count_newlines
, first_line_width
, last_line_width
, trim_left_preserve_layout
, unicode_str_width
,
15 use crate::{ErrorKind, FormattingError}
;
17 fn is_custom_comment(comment
: &str) -> bool
{
18 if !comment
.starts_with("//") {
20 } else if let Some(c
) = comment
.chars().nth(2) {
21 !c
.is_alphanumeric() && !c
.is_whitespace()
27 #[derive(Copy, Clone, PartialEq, Eq)]
28 pub(crate) enum CommentStyle
<'a
> {
38 fn custom_opener(s
: &str) -> &str {
39 s
.lines().next().map_or("", |first_line
| {
42 .map_or(first_line
, |space_index
| &first_line
[0..=space_index
])
46 impl<'a
> CommentStyle
<'a
> {
47 /// Returns `true` if the commenting style covers a line only.
48 pub(crate) fn is_line_comment(&self) -> bool
{
50 CommentStyle
::DoubleSlash
51 | CommentStyle
::TripleSlash
53 | CommentStyle
::Custom(_
) => true,
58 /// Returns `true` if the commenting style can span over multiple lines.
59 pub(crate) fn is_block_comment(&self) -> bool
{
61 CommentStyle
::SingleBullet
| CommentStyle
::DoubleBullet
| CommentStyle
::Exclamation
=> {
68 /// Returns `true` if the commenting style is for documentation.
69 pub(crate) fn is_doc_comment(&self) -> bool
{
71 CommentStyle
::TripleSlash
| CommentStyle
::Doc
=> true,
76 pub(crate) fn opener(&self) -> &'a
str {
78 CommentStyle
::DoubleSlash
=> "// ",
79 CommentStyle
::TripleSlash
=> "/// ",
80 CommentStyle
::Doc
=> "//! ",
81 CommentStyle
::SingleBullet
=> "/* ",
82 CommentStyle
::DoubleBullet
=> "/** ",
83 CommentStyle
::Exclamation
=> "/*! ",
84 CommentStyle
::Custom(opener
) => opener
,
88 pub(crate) fn closer(&self) -> &'a
str {
90 CommentStyle
::DoubleSlash
91 | CommentStyle
::TripleSlash
92 | CommentStyle
::Custom(..)
93 | CommentStyle
::Doc
=> "",
94 CommentStyle
::SingleBullet
| CommentStyle
::DoubleBullet
| CommentStyle
::Exclamation
=> {
100 pub(crate) fn line_start(&self) -> &'a
str {
102 CommentStyle
::DoubleSlash
=> "// ",
103 CommentStyle
::TripleSlash
=> "/// ",
104 CommentStyle
::Doc
=> "//! ",
105 CommentStyle
::SingleBullet
| CommentStyle
::DoubleBullet
| CommentStyle
::Exclamation
=> {
108 CommentStyle
::Custom(opener
) => opener
,
112 pub(crate) fn to_str_tuplet(&self) -> (&'a
str, &'a
str, &'a
str) {
113 (self.opener(), self.closer(), self.line_start())
117 pub(crate) fn comment_style(orig
: &str, normalize_comments
: bool
) -> CommentStyle
<'_
> {
118 if !normalize_comments
{
119 if orig
.starts_with("/**") && !orig
.starts_with("/**/") {
120 CommentStyle
::DoubleBullet
121 } else if orig
.starts_with("/*!") {
122 CommentStyle
::Exclamation
123 } else if orig
.starts_with("/*") {
124 CommentStyle
::SingleBullet
125 } else if orig
.starts_with("///") && orig
.chars().nth(3).map_or(true, |c
| c
!= '
/'
) {
126 CommentStyle
::TripleSlash
127 } else if orig
.starts_with("//!") {
129 } else if is_custom_comment(orig
) {
130 CommentStyle
::Custom(custom_opener(orig
))
132 CommentStyle
::DoubleSlash
134 } else if (orig
.starts_with("///") && orig
.chars().nth(3).map_or(true, |c
| c
!= '
/'
))
135 || (orig
.starts_with("/**") && !orig
.starts_with("/**/"))
137 CommentStyle
::TripleSlash
138 } else if orig
.starts_with("//!") || orig
.starts_with("/*!") {
140 } else if is_custom_comment(orig
) {
141 CommentStyle
::Custom(custom_opener(orig
))
143 CommentStyle
::DoubleSlash
147 /// Returns true if the last line of the passed string finishes with a block-comment.
148 pub(crate) fn is_last_comment_block(s
: &str) -> bool
{
149 s
.trim_end().ends_with("*/")
152 /// Combine `prev_str` and `next_str` into a single `String`. `span` may contain
153 /// comments between two strings. If there are such comments, then that will be
154 /// recovered. If `allow_extend` is true and there is no comment between the two
155 /// strings, then they will be put on a single line as long as doing so does not
156 /// exceed max width.
157 pub(crate) fn combine_strs_with_missing_comments(
158 context
: &RewriteContext
<'_
>,
164 ) -> Option
<String
> {
166 "combine_strs_with_missing_comments `{}` `{}` {:?} {:?}",
174 String
::with_capacity(prev_str
.len() + next_str
.len() + shape
.indent
.width() + 128);
175 result
.push_str(prev_str
);
176 let mut allow_one_line
= !prev_str
.contains('
\n'
) && !next_str
.contains('
\n'
);
177 let first_sep
= if prev_str
.is_empty() || next_str
.is_empty() {
182 let mut one_line_width
=
183 last_line_width(prev_str
) + first_line_width(next_str
) + first_sep
.len();
185 let config
= context
.config
;
186 let indent
= shape
.indent
;
187 let missing_comment
= rewrite_missing_comment(span
, shape
, context
)?
;
189 if missing_comment
.is_empty() {
190 if allow_extend
&& prev_str
.len() + first_sep
.len() + next_str
.len() <= shape
.width
{
191 result
.push_str(first_sep
);
192 } else if !prev_str
.is_empty() {
193 result
.push_str(&indent
.to_string_with_newline(config
))
195 result
.push_str(next_str
);
199 // We have a missing comment between the first expression and the second expression.
201 // Peek the the original source code and find out whether there is a newline between the first
202 // expression and the second expression or the missing comment. We will preserve the original
203 // layout whenever possible.
204 let original_snippet
= context
.snippet(span
);
205 let prefer_same_line
= if let Some(pos
) = original_snippet
.find('
/'
) {
206 !original_snippet
[..pos
].contains('
\n'
)
208 !original_snippet
.contains('
\n'
)
211 one_line_width
-= first_sep
.len();
212 let first_sep
= if prev_str
.is_empty() || missing_comment
.is_empty() {
215 let one_line_width
= last_line_width(prev_str
) + first_line_width(&missing_comment
) + 1;
216 if prefer_same_line
&& one_line_width
<= shape
.width
{
219 indent
.to_string_with_newline(config
)
222 result
.push_str(&first_sep
);
223 result
.push_str(&missing_comment
);
225 let second_sep
= if missing_comment
.is_empty() || next_str
.is_empty() {
227 } else if missing_comment
.starts_with("//") {
228 indent
.to_string_with_newline(config
)
230 one_line_width
+= missing_comment
.len() + first_sep
.len() + 1;
231 allow_one_line
&= !missing_comment
.starts_with("//") && !missing_comment
.contains('
\n'
);
232 if prefer_same_line
&& allow_one_line
&& one_line_width
<= shape
.width
{
235 indent
.to_string_with_newline(config
)
238 result
.push_str(&second_sep
);
239 result
.push_str(next_str
);
244 pub(crate) fn rewrite_doc_comment(orig
: &str, shape
: Shape
, config
: &Config
) -> Option
<String
> {
245 identify_comment(orig
, false, shape
, config
, true)
248 pub(crate) fn rewrite_comment(
253 ) -> Option
<String
> {
254 identify_comment(orig
, block_style
, shape
, config
, false)
262 is_doc_comment
: bool
,
263 ) -> Option
<String
> {
264 let style
= comment_style(orig
, false);
266 // Computes the byte length of line taking into account a newline if the line is part of a
268 fn compute_len(orig
: &str, line
: &str) -> usize {
269 if orig
.len() > line
.len() {
270 if orig
.as_bytes()[line
.len()] == b'
\r'
{
280 // Get the first group of line comments having the same commenting style.
282 // Returns a tuple with:
283 // - a boolean indicating if there is a blank line
284 // - a number indicating the size of the first group of comments
285 fn consume_same_line_comments(
286 style
: CommentStyle
<'_
>,
290 let mut first_group_ending
= 0;
293 for line
in orig
.lines() {
294 let trimmed_line
= line
.trim_start();
295 if trimmed_line
.is_empty() {
298 } else if trimmed_line
.starts_with(line_start
)
299 || comment_style(trimmed_line
, false) == style
301 first_group_ending
+= compute_len(&orig
[first_group_ending
..], line
);
306 (hbl
, first_group_ending
)
309 let (has_bare_lines
, first_group_ending
) = match style
{
310 CommentStyle
::DoubleSlash
| CommentStyle
::TripleSlash
| CommentStyle
::Doc
=> {
311 let line_start
= style
.line_start().trim_start();
312 consume_same_line_comments(style
, orig
, line_start
)
314 CommentStyle
::Custom(opener
) => {
315 let trimmed_opener
= opener
.trim_end();
316 consume_same_line_comments(style
, orig
, trimmed_opener
)
318 // for a block comment, search for the closing symbol
319 CommentStyle
::DoubleBullet
| CommentStyle
::SingleBullet
| CommentStyle
::Exclamation
=> {
320 let closer
= style
.closer().trim_start();
321 let mut count
= orig
.matches(closer
).count();
322 let mut closing_symbol_offset
= 0;
324 let mut first
= true;
325 for line
in orig
.lines() {
326 closing_symbol_offset
+= compute_len(&orig
[closing_symbol_offset
..], line
);
327 let mut trimmed_line
= line
.trim_start();
328 if !trimmed_line
.starts_with('
*'
)
329 && !trimmed_line
.starts_with("//")
330 && !trimmed_line
.starts_with("/*")
335 // Remove opener from consideration when searching for closer
337 let opener
= style
.opener().trim_end();
338 trimmed_line
= &trimmed_line
[opener
.len()..];
341 if trimmed_line
.ends_with(closer
) {
348 (hbl
, closing_symbol_offset
)
352 let (first_group
, rest
) = orig
.split_at(first_group_ending
);
353 let rewritten_first_group
=
354 if !config
.normalize_comments() && has_bare_lines
&& style
.is_block_comment() {
355 trim_left_preserve_layout(first_group
, shape
.indent
, config
)?
356 } else if !config
.normalize_comments()
357 && !config
.wrap_comments()
358 && !config
.format_code_in_doc_comments()
360 light_rewrite_comment(first_group
, shape
.indent
, config
, is_doc_comment
)
362 rewrite_comment_inner(
368 is_doc_comment
|| style
.is_doc_comment(),
372 Some(rewritten_first_group
)
384 rewritten_first_group
,
385 // insert back the blank line
386 if has_bare_lines
&& style
.is_line_comment() {
391 shape
.indent
.to_string(config
),
398 /// Attributes for code blocks in rustdoc.
399 /// See https://doc.rust-lang.org/rustdoc/print.html#attributes
400 enum CodeBlockAttribute
{
409 impl CodeBlockAttribute
{
410 fn new(attribute
: &str) -> CodeBlockAttribute
{
412 "rust" | "" => CodeBlockAttribute
::Rust
,
413 "ignore" => CodeBlockAttribute
::Ignore
,
414 "text" => CodeBlockAttribute
::Text
,
415 "should_panic" => CodeBlockAttribute
::ShouldPanic
,
416 "no_run" => CodeBlockAttribute
::NoRun
,
417 "compile_fail" => CodeBlockAttribute
::CompileFail
,
418 _
=> CodeBlockAttribute
::Text
,
423 /// Block that is formatted as an item.
425 /// An item starts with either a star `*` or a dash `-`. Different level of indentation are
426 /// handled by shrinking the shape accordingly.
427 struct ItemizedBlock
{
428 /// the lines that are identified as part of an itemized block
430 /// the number of whitespaces up to the item sigil
432 /// the string that marks the start of an item
434 /// sequence of whitespaces to prefix new lines that are part of the item
439 /// Returns `true` if the line is formatted as an item
440 fn is_itemized_line(line
: &str) -> bool
{
441 let trimmed
= line
.trim_start();
442 trimmed
.starts_with("* ") || trimmed
.starts_with("- ")
445 /// Creates a new ItemizedBlock described with the given line.
446 /// The `is_itemized_line` needs to be called first.
447 fn new(line
: &str) -> ItemizedBlock
{
448 let space_to_sigil
= line
.chars().take_while(|c
| c
.is_whitespace()).count();
449 let indent
= space_to_sigil
+ 2;
451 lines
: vec
![line
[indent
..].to_string()],
453 opener
: line
[..indent
].to_string(),
454 line_start
: " ".repeat(indent
),
458 /// Returns a `StringFormat` used for formatting the content of an item.
459 fn create_string_format
<'a
>(&'a
self, fmt
: &'a StringFormat
<'_
>) -> StringFormat
<'a
> {
465 shape
: Shape
::legacy(fmt
.shape
.width
.saturating_sub(self.indent
), Indent
::empty()),
471 /// Returns `true` if the line is part of the current itemized block.
472 /// If it is, then it is added to the internal lines list.
473 fn add_line(&mut self, line
: &str) -> bool
{
474 if !ItemizedBlock
::is_itemized_line(line
)
475 && self.indent
<= line
.chars().take_while(|c
| c
.is_whitespace()).count()
477 self.lines
.push(line
.to_string());
483 /// Returns the block as a string, with each line trimmed at the start.
484 fn trimmed_block_as_string(&self) -> String
{
487 .map(|line
| format
!("{} ", line
.trim_start()))
491 /// Returns the block as a string under its original form.
492 fn original_block_as_string(&self) -> String
{
493 self.lines
.join("\n")
497 struct CommentRewrite
<'a
> {
499 code_block_buffer
: String
,
500 is_prev_line_multi_line
: bool
,
501 code_block_attr
: Option
<CodeBlockAttribute
>,
502 item_block
: Option
<ItemizedBlock
>,
503 comment_line_separator
: String
,
507 fmt
: StringFormat
<'a
>,
514 impl<'a
> CommentRewrite
<'a
> {
520 ) -> CommentRewrite
<'a
> {
521 let (opener
, closer
, line_start
) = if block_style
{
522 CommentStyle
::SingleBullet
.to_str_tuplet()
524 comment_style(orig
, config
.normalize_comments()).to_str_tuplet()
527 let max_width
= shape
529 .checked_sub(closer
.len() + opener
.len())
531 let indent_str
= shape
.indent
.to_string_with_newline(config
).to_string();
533 let mut cr
= CommentRewrite
{
534 result
: String
::with_capacity(orig
.len() * 2),
535 code_block_buffer
: String
::with_capacity(128),
536 is_prev_line_multi_line
: false,
537 code_block_attr
: None
,
539 comment_line_separator
: format
!("{}{}", indent_str
, line_start
),
542 fmt_indent
: shape
.indent
,
549 shape
: Shape
::legacy(max_width
, shape
.indent
),
554 opener
: opener
.to_owned(),
555 closer
: closer
.to_owned(),
556 line_start
: line_start
.to_owned(),
558 cr
.result
.push_str(opener
);
562 fn join_block(s
: &str, sep
: &str) -> String
{
563 let mut result
= String
::with_capacity(s
.len() + 128);
564 let mut iter
= s
.lines().peekable();
565 while let Some(line
) = iter
.next() {
566 result
.push_str(line
);
567 result
.push_str(match iter
.peek() {
568 Some(next_line
) if next_line
.is_empty() => sep
.trim_end(),
576 fn finish(mut self) -> String
{
577 if !self.code_block_buffer
.is_empty() {
578 // There is a code block that is not properly enclosed by backticks.
579 // We will leave them untouched.
580 self.result
.push_str(&self.comment_line_separator
);
581 self.result
.push_str(&Self::join_block(
582 &trim_custom_comment_prefix(&self.code_block_buffer
),
583 &self.comment_line_separator
,
587 if let Some(ref ib
) = self.item_block
{
588 // the last few lines are part of an itemized block
589 self.fmt
.shape
= Shape
::legacy(self.max_width
, self.fmt_indent
);
590 let item_fmt
= ib
.create_string_format(&self.fmt
);
591 self.result
.push_str(&self.comment_line_separator
);
592 self.result
.push_str(&ib
.opener
);
593 match rewrite_string(
594 &ib
.trimmed_block_as_string(),
596 self.max_width
.saturating_sub(ib
.indent
),
598 Some(s
) => self.result
.push_str(&Self::join_block(
600 &format
!("{}{}", self.comment_line_separator
, ib
.line_start
),
602 None
=> self.result
.push_str(&Self::join_block(
603 &ib
.original_block_as_string(),
604 &self.comment_line_separator
,
609 self.result
.push_str(&self.closer
);
610 if self.result
.ends_with(&self.opener
) && self.opener
.ends_with(' '
) {
623 has_leading_whitespace
: bool
,
625 let is_last
= i
== count_newlines(orig
);
627 if let Some(ref mut ib
) = self.item_block
{
628 if ib
.add_line(&line
) {
631 self.is_prev_line_multi_line
= false;
632 self.fmt
.shape
= Shape
::legacy(self.max_width
, self.fmt_indent
);
633 let item_fmt
= ib
.create_string_format(&self.fmt
);
634 self.result
.push_str(&self.comment_line_separator
);
635 self.result
.push_str(&ib
.opener
);
636 match rewrite_string(
637 &ib
.trimmed_block_as_string(),
639 self.max_width
.saturating_sub(ib
.indent
),
641 Some(s
) => self.result
.push_str(&Self::join_block(
643 &format
!("{}{}", self.comment_line_separator
, ib
.line_start
),
645 None
=> self.result
.push_str(&Self::join_block(
646 &ib
.original_block_as_string(),
647 &self.comment_line_separator
,
650 } else if self.code_block_attr
.is_some() {
651 if line
.starts_with("```") {
652 let code_block
= match self.code_block_attr
.as_ref().unwrap() {
653 CodeBlockAttribute
::Ignore
| CodeBlockAttribute
::Text
=> {
654 trim_custom_comment_prefix(&self.code_block_buffer
)
656 _
if self.code_block_buffer
.is_empty() => String
::new(),
658 let mut config
= self.fmt
.config
.clone();
659 config
.set().wrap_comments(false);
660 if config
.format_code_in_doc_comments() {
662 crate::format_code_block(&self.code_block_buffer
, &config
, false)
664 trim_custom_comment_prefix(&s
.snippet
)
666 trim_custom_comment_prefix(&self.code_block_buffer
)
669 trim_custom_comment_prefix(&self.code_block_buffer
)
673 if !code_block
.is_empty() {
674 self.result
.push_str(&self.comment_line_separator
);
676 .push_str(&Self::join_block(&code_block
, &self.comment_line_separator
));
678 self.code_block_buffer
.clear();
679 self.result
.push_str(&self.comment_line_separator
);
680 self.result
.push_str(line
);
681 self.code_block_attr
= None
;
683 self.code_block_buffer
684 .push_str(&hide_sharp_behind_comment(line
));
685 self.code_block_buffer
.push('
\n'
);
690 self.code_block_attr
= None
;
691 self.item_block
= None
;
692 if line
.starts_with("```") {
693 self.code_block_attr
= Some(CodeBlockAttribute
::new(&line
[3..]))
694 } else if self.fmt
.config
.wrap_comments() && ItemizedBlock
::is_itemized_line(&line
) {
695 let ib
= ItemizedBlock
::new(&line
);
696 self.item_block
= Some(ib
);
700 if self.result
== self.opener
{
701 let force_leading_whitespace
= &self.opener
== "/* " && count_newlines(orig
) == 0;
702 if !has_leading_whitespace
&& !force_leading_whitespace
&& self.result
.ends_with(' '
) {
708 } else if self.is_prev_line_multi_line
&& !line
.is_empty() {
709 self.result
.push(' '
)
710 } else if is_last
&& line
.is_empty() {
711 // trailing blank lines are unwanted
712 if !self.closer
.is_empty() {
713 self.result
.push_str(&self.indent_str
);
717 self.result
.push_str(&self.comment_line_separator
);
718 if !has_leading_whitespace
&& self.result
.ends_with(' '
) {
723 if self.fmt
.config
.wrap_comments()
724 && unicode_str_width(line
) > self.fmt
.shape
.width
727 match rewrite_string(line
, &self.fmt
, self.max_width
) {
729 self.is_prev_line_multi_line
= s
.contains('
\n'
);
730 self.result
.push_str(s
);
732 None
if self.is_prev_line_multi_line
=> {
733 // We failed to put the current `line` next to the previous `line`.
734 // Remove the trailing space, then start rewrite on the next line.
736 self.result
.push_str(&self.comment_line_separator
);
737 self.fmt
.shape
= Shape
::legacy(self.max_width
, self.fmt_indent
);
738 match rewrite_string(line
, &self.fmt
, self.max_width
) {
740 self.is_prev_line_multi_line
= s
.contains('
\n'
);
741 self.result
.push_str(s
);
744 self.is_prev_line_multi_line
= false;
745 self.result
.push_str(line
);
750 self.is_prev_line_multi_line
= false;
751 self.result
.push_str(line
);
755 self.fmt
.shape
= if self.is_prev_line_multi_line
{
757 let offset
= 1 + last_line_width(&self.result
) - self.line_start
.len();
759 width
: self.max_width
.saturating_sub(offset
),
760 indent
: self.fmt_indent
,
761 offset
: self.fmt
.shape
.offset
+ offset
,
764 Shape
::legacy(self.max_width
, self.fmt_indent
)
767 if line
.is_empty() && self.result
.ends_with(' '
) && !is_last
{
768 // Remove space if this is an empty comment or a doc comment.
771 self.result
.push_str(line
);
772 self.fmt
.shape
= Shape
::legacy(self.max_width
, self.fmt_indent
);
773 self.is_prev_line_multi_line
= false;
780 fn rewrite_comment_inner(
783 style
: CommentStyle
<'_
>,
786 is_doc_comment
: bool
,
787 ) -> Option
<String
> {
788 let mut rewriter
= CommentRewrite
::new(orig
, block_style
, shape
, config
);
790 let line_breaks
= count_newlines(orig
.trim_end());
794 .map(|(i
, mut line
)| {
795 line
= trim_end_unless_two_whitespaces(line
.trim_start(), is_doc_comment
);
797 if i
== line_breaks
&& line
.ends_with("*/") && !line
.starts_with("//") {
798 line
= line
[..(line
.len() - 2)].trim_end();
803 .map(|s
| left_trim_comment_line(s
, &style
))
804 .map(|(line
, has_leading_whitespace
)| {
805 if orig
.starts_with("/*") && line_breaks
== 0 {
808 has_leading_whitespace
|| config
.normalize_comments(),
811 (line
, has_leading_whitespace
|| config
.normalize_comments())
815 for (i
, (line
, has_leading_whitespace
)) in lines
.enumerate() {
816 if rewriter
.handle_line(orig
, i
, line
, has_leading_whitespace
) {
821 Some(rewriter
.finish())
824 const RUSTFMT_CUSTOM_COMMENT_PREFIX
: &str = "//#### ";
826 fn hide_sharp_behind_comment(s
: &str) -> Cow
<'_
, str> {
827 let s_trimmed
= s
.trim();
828 if s_trimmed
.starts_with("# ") || s_trimmed
== "#" {
829 Cow
::from(format
!("{}{}", RUSTFMT_CUSTOM_COMMENT_PREFIX
, s
))
835 fn trim_custom_comment_prefix(s
: &str) -> String
{
838 let left_trimmed
= line
.trim_start();
839 if left_trimmed
.starts_with(RUSTFMT_CUSTOM_COMMENT_PREFIX
) {
840 left_trimmed
.trim_start_matches(RUSTFMT_CUSTOM_COMMENT_PREFIX
)
849 /// Returns `true` if the given string MAY include URLs or alike.
850 fn has_url(s
: &str) -> bool
{
851 // This function may return false positive, but should get its job done in most cases.
852 s
.contains("https://") || s
.contains("http://") || s
.contains("ftp://") || s
.contains("file://")
855 /// Given the span, rewrite the missing comment inside it if available.
856 /// Note that the given span must only include comments (or leading/trailing whitespaces).
857 pub(crate) fn rewrite_missing_comment(
860 context
: &RewriteContext
<'_
>,
861 ) -> Option
<String
> {
862 let missing_snippet
= context
.snippet(span
);
863 let trimmed_snippet
= missing_snippet
.trim();
864 // check the span starts with a comment
865 let pos
= trimmed_snippet
.find('
/'
);
866 if !trimmed_snippet
.is_empty() && pos
.is_some() {
867 rewrite_comment(trimmed_snippet
, false, shape
, context
.config
)
873 /// Recover the missing comments in the specified span, if available.
874 /// The layout of the comments will be preserved as long as it does not break the code
875 /// and its total width does not exceed the max width.
876 pub(crate) fn recover_missing_comment_in_span(
879 context
: &RewriteContext
<'_
>,
881 ) -> Option
<String
> {
882 let missing_comment
= rewrite_missing_comment(span
, shape
, context
)?
;
883 if missing_comment
.is_empty() {
886 let missing_snippet
= context
.snippet(span
);
887 let pos
= missing_snippet
.find('
/'
)?
;
889 let total_width
= missing_comment
.len() + used_width
+ 1;
890 let force_new_line_before_comment
=
891 missing_snippet
[..pos
].contains('
\n'
) || total_width
> context
.config
.max_width();
892 let sep
= if force_new_line_before_comment
{
893 shape
.indent
.to_string_with_newline(context
.config
)
897 Some(format
!("{}{}", sep
, missing_comment
))
901 /// Trim trailing whitespaces unless they consist of two or more whitespaces.
902 fn trim_end_unless_two_whitespaces(s
: &str, is_doc_comment
: bool
) -> &str {
903 if is_doc_comment
&& s
.ends_with(" ") {
910 /// Trims whitespace and aligns to indent, but otherwise does not change comments.
911 fn light_rewrite_comment(
915 is_doc_comment
: bool
,
917 let lines
: Vec
<&str> = orig
920 // This is basically just l.trim(), but in the case that a line starts
921 // with `*` we want to leave one space before it, so it aligns with the
923 let first_non_whitespace
= l
.find(|c
| !char::is_whitespace(c
));
924 let left_trimmed
= if let Some(fnw
) = first_non_whitespace
{
925 if l
.as_bytes()[fnw
] == b'
*'
&& fnw
> 0 {
933 // Preserve markdown's double-space line break syntax in doc comment.
934 trim_end_unless_two_whitespaces(left_trimmed
, is_doc_comment
)
937 lines
.join(&format
!("\n{}", offset
.to_string(config
)))
940 /// Trims comment characters and possibly a single space from the left of a string.
941 /// Does not trim all whitespace. If a single space is trimmed from the left of the string,
942 /// this function returns true.
943 fn left_trim_comment_line
<'a
>(line
: &'a
str, style
: &CommentStyle
<'_
>) -> (&'a
str, bool
) {
944 if line
.starts_with("//! ")
945 || line
.starts_with("/// ")
946 || line
.starts_with("/*! ")
947 || line
.starts_with("/** ")
950 } else if let CommentStyle
::Custom(opener
) = *style
{
951 if line
.starts_with(opener
) {
952 (&line
[opener
.len()..], true)
954 (&line
[opener
.trim_end().len()..], false)
956 } else if line
.starts_with("/* ")
957 || line
.starts_with("// ")
958 || line
.starts_with("//!")
959 || line
.starts_with("///")
960 || line
.starts_with("** ")
961 || line
.starts_with("/*!")
962 || (line
.starts_with("/**") && !line
.starts_with("/**/"))
964 (&line
[3..], line
.chars().nth(2).unwrap() == ' '
)
965 } else if line
.starts_with("/*")
966 || line
.starts_with("* ")
967 || line
.starts_with("//")
968 || line
.starts_with("**")
970 (&line
[2..], line
.chars().nth(1).unwrap() == ' '
)
971 } else if line
.starts_with('
*'
) {
974 (line
, line
.starts_with(' '
))
978 pub(crate) trait FindUncommented
{
979 fn find_uncommented(&self, pat
: &str) -> Option
<usize>;
982 impl FindUncommented
for str {
983 fn find_uncommented(&self, pat
: &str) -> Option
<usize> {
984 let mut needle_iter
= pat
.chars();
985 for (kind
, (i
, b
)) in CharClasses
::new(self.char_indices()) {
986 match needle_iter
.next() {
988 return Some(i
- pat
.len());
990 Some(c
) => match kind
{
991 FullCodeCharKind
::Normal
| FullCodeCharKind
::InString
if b
== c
=> {}
993 needle_iter
= pat
.chars();
999 // Handle case where the pattern is a suffix of the search string
1000 match needle_iter
.next() {
1002 None
=> Some(self.len() - pat
.len()),
1007 // Returns the first byte position after the first comment. The given string
1008 // is expected to be prefixed by a comment, including delimiters.
1009 // Good: `/* /* inner */ outer */ code();`
1010 // Bad: `code(); // hello\n world!`
1011 pub(crate) fn find_comment_end(s
: &str) -> Option
<usize> {
1012 let mut iter
= CharClasses
::new(s
.char_indices());
1013 for (kind
, (i
, _c
)) in &mut iter
{
1014 if kind
== FullCodeCharKind
::Normal
|| kind
== FullCodeCharKind
::InString
{
1019 // Handle case where the comment ends at the end of `s`.
1020 if iter
.status
== CharClassesStatus
::Normal
{
1027 /// Returns `true` if text contains any comment.
1028 pub(crate) fn contains_comment(text
: &str) -> bool
{
1029 CharClasses
::new(text
.chars()).any(|(kind
, _
)| kind
.is_comment())
1032 pub(crate) struct CharClasses
<T
>
1038 status
: CharClassesStatus
,
1041 pub(crate) trait RichChar
{
1042 fn get_char(&self) -> char;
1045 impl RichChar
for char {
1046 fn get_char(&self) -> char {
1051 impl RichChar
for (usize, char) {
1052 fn get_char(&self) -> char {
1057 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
1058 enum CharClassesStatus
{
1060 /// Character is within a string
1063 /// Character is within a raw string
1065 RawStringPrefix(u32),
1066 RawStringSuffix(u32),
1069 /// Character inside a block comment, with the integer indicating the nesting deepness of the
1072 /// Character inside a block-commented string, with the integer indicating the nesting deepness
1074 StringInBlockComment(u32),
1075 /// Status when the '/' has been consumed, but not yet the '*', deepness is
1076 /// the new deepness (after the comment opening).
1077 BlockCommentOpening(u32),
1078 /// Status when the '*' has been consumed, but not yet the '/', deepness is
1079 /// the new deepness (after the comment closing).
1080 BlockCommentClosing(u32),
1081 /// Character is within a line comment
1085 /// Distinguish between functional part of code and comments
1086 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
1087 pub(crate) enum CodeCharKind
{
1092 /// Distinguish between functional part of code and comments,
1093 /// describing opening and closing of comments for ease when chunking
1094 /// code from tagged characters
1095 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
1096 pub(crate) enum FullCodeCharKind
{
1098 /// The first character of a comment, there is only one for a comment (always '/')
1100 /// Any character inside a comment including the second character of comment
1101 /// marks ("//", "/*")
1103 /// Last character of a comment, '\n' for a line comment, '/' for a block comment.
1105 /// Start of a mutlitine string inside a comment
1106 StartStringCommented
,
1107 /// End of a mutlitine string inside a comment
1109 /// Inside a commented string
1111 /// Start of a mutlitine string
1113 /// End of a mutlitine string
1115 /// Inside a string.
1119 impl FullCodeCharKind
{
1120 pub(crate) fn is_comment(self) -> bool
{
1122 FullCodeCharKind
::StartComment
1123 | FullCodeCharKind
::InComment
1124 | FullCodeCharKind
::EndComment
1125 | FullCodeCharKind
::StartStringCommented
1126 | FullCodeCharKind
::InStringCommented
1127 | FullCodeCharKind
::EndStringCommented
=> true,
1132 /// Returns true if the character is inside a comment
1133 pub(crate) fn inside_comment(self) -> bool
{
1135 FullCodeCharKind
::InComment
1136 | FullCodeCharKind
::StartStringCommented
1137 | FullCodeCharKind
::InStringCommented
1138 | FullCodeCharKind
::EndStringCommented
=> true,
1143 pub(crate) fn is_string(self) -> bool
{
1144 self == FullCodeCharKind
::InString
|| self == FullCodeCharKind
::StartString
1147 /// Returns true if the character is within a commented string
1148 pub(crate) fn is_commented_string(self) -> bool
{
1149 self == FullCodeCharKind
::InStringCommented
1150 || self == FullCodeCharKind
::StartStringCommented
1153 fn to_codecharkind(self) -> CodeCharKind
{
1154 if self.is_comment() {
1155 CodeCharKind
::Comment
1157 CodeCharKind
::Normal
1162 impl<T
> CharClasses
<T
>
1167 pub(crate) fn new(base
: T
) -> CharClasses
<T
> {
1169 base
: multipeek(base
),
1170 status
: CharClassesStatus
::Normal
,
1175 fn is_raw_string_suffix
<T
>(iter
: &mut MultiPeek
<T
>, count
: u32) -> bool
1182 Some(c
) if c
.get_char() == '
#' => continue,
1189 impl<T
> Iterator
for CharClasses
<T
>
1194 type Item
= (FullCodeCharKind
, T
::Item
);
1196 fn next(&mut self) -> Option
<(FullCodeCharKind
, T
::Item
)> {
1197 let item
= self.base
.next()?
;
1198 let chr
= item
.get_char();
1199 let mut char_kind
= FullCodeCharKind
::Normal
;
1200 self.status
= match self.status
{
1201 CharClassesStatus
::LitRawString(sharps
) => {
1202 char_kind
= FullCodeCharKind
::InString
;
1206 char_kind = FullCodeCharKind::Normal;
1207 CharClassesStatus::Normal
1208 } else if is_raw_string_suffix(&mut self.base, sharps) {
1209 CharClassesStatus::RawStringSuffix(sharps)
1211 CharClassesStatus::LitRawString(sharps)
1214 _ => CharClassesStatus::LitRawString(sharps),
1217 CharClassesStatus::RawStringPrefix(sharps) => {
1218 char_kind = FullCodeCharKind::InString;
1220 '#' => CharClassesStatus::RawStringPrefix(sharps + 1),
1221 '"'
=> CharClassesStatus
::LitRawString(sharps
),
1222 _
=> CharClassesStatus
::Normal
, // Unreachable.
1225 CharClassesStatus
::RawStringSuffix(sharps
) => {
1229 CharClassesStatus
::Normal
1231 char_kind
= FullCodeCharKind
::InString
;
1232 CharClassesStatus
::RawStringSuffix(sharps
- 1)
1235 _
=> CharClassesStatus
::Normal
, // Unreachable
1238 CharClassesStatus
::LitString
=> {
1239 char_kind
= FullCodeCharKind
::InString
;
1241 '
"' => CharClassesStatus::Normal,
1242 '\\' => CharClassesStatus::LitStringEscape,
1243 _ => CharClassesStatus::LitString,
1246 CharClassesStatus::LitStringEscape => {
1247 char_kind = FullCodeCharKind::InString;
1248 CharClassesStatus::LitString
1250 CharClassesStatus::LitChar => match chr {
1251 '\\' => CharClassesStatus::LitCharEscape,
1252 '\'' => CharClassesStatus::Normal,
1253 _ => CharClassesStatus::LitChar,
1255 CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar,
1256 CharClassesStatus::Normal => match chr {
1257 'r' => match self.base.peek().map(RichChar::get_char) {
1258 Some('#') | Some('"'
) => {
1259 char_kind
= FullCodeCharKind
::InString
;
1260 CharClassesStatus
::RawStringPrefix(0)
1262 _
=> CharClassesStatus
::Normal
,
1265 char_kind = FullCodeCharKind::InString;
1266 CharClassesStatus::LitString
1269 // HACK: Work around mut borrow.
1270 match self.base.peek() {
1271 Some(next) if next.get_char() == '\\' => {
1272 self.status = CharClassesStatus::LitChar;
1273 return Some((char_kind, item));
1278 match self.base.peek() {
1279 Some(next) if next.get_char() == '\'' => CharClassesStatus::LitChar,
1280 _ => CharClassesStatus::Normal,
1283 '/' => match self.base.peek() {
1284 Some(next) if next.get_char() == '*' => {
1285 self.status = CharClassesStatus::BlockCommentOpening(1);
1286 return Some((FullCodeCharKind::StartComment, item));
1288 Some(next) if next.get_char() == '/' => {
1289 self.status = CharClassesStatus::LineComment;
1290 return Some((FullCodeCharKind::StartComment, item));
1292 _ => CharClassesStatus::Normal,
1294 _ => CharClassesStatus::Normal,
1296 CharClassesStatus::StringInBlockComment(deepness) => {
1297 char_kind = FullCodeCharKind::InStringCommented;
1299 CharClassesStatus
::BlockComment(deepness
)
1301 CharClassesStatus
::StringInBlockComment(deepness
)
1304 CharClassesStatus
::BlockComment(deepness
) => {
1305 assert_ne
!(deepness
, 0);
1306 char_kind
= FullCodeCharKind
::InComment
;
1307 match self.base
.peek() {
1308 Some(next
) if next
.get_char() == '
/'
&& chr
== '
*'
=> {
1309 CharClassesStatus
::BlockCommentClosing(deepness
- 1)
1311 Some(next
) if next
.get_char() == '
*'
&& chr
== '
/'
=> {
1312 CharClassesStatus
::BlockCommentOpening(deepness
+ 1)
1314 _
if chr
== '
"' => CharClassesStatus::StringInBlockComment(deepness),
1318 CharClassesStatus::BlockCommentOpening(deepness) => {
1319 assert_eq!(chr, '*');
1320 self.status = CharClassesStatus::BlockComment(deepness);
1321 return Some((FullCodeCharKind::InComment, item));
1323 CharClassesStatus::BlockCommentClosing(deepness) => {
1324 assert_eq!(chr, '/');
1326 self.status = CharClassesStatus::Normal;
1327 return Some((FullCodeCharKind::EndComment, item));
1329 self.status = CharClassesStatus::BlockComment(deepness);
1330 return Some((FullCodeCharKind::InComment, item));
1333 CharClassesStatus::LineComment => match chr {
1335 self.status = CharClassesStatus::Normal;
1336 return Some((FullCodeCharKind::EndComment, item));
1339 self.status = CharClassesStatus::LineComment;
1340 return Some((FullCodeCharKind::InComment, item));
1344 Some((char_kind, item))
1348 /// An iterator over the lines of a string, paired with the char kind at the
1349 /// end of the line.
1350 pub(crate) struct LineClasses<'a> {
1351 base: iter::Peekable<CharClasses<std::str::Chars<'a>>>,
1352 kind: FullCodeCharKind,
1355 impl<'a> LineClasses<'a> {
1356 pub(crate) fn new(s: &'a str) -> Self {
1358 base: CharClasses::new(s.chars()).peekable(),
1359 kind: FullCodeCharKind::Normal,
1364 impl<'a> Iterator for LineClasses<'a> {
1365 type Item = (FullCodeCharKind, String);
1367 fn next(&mut self) -> Option<Self::Item> {
1370 let mut line = String::new();
1372 let start_kind = match self.base.peek() {
1373 Some((kind, _)) => *kind,
1374 None => unreachable!(),
1377 while let Some((kind, c)) = self.base.next() {
1378 // needed to set the kind of the ending character on the last line
1381 self.kind = match (start_kind, kind) {
1382 (FullCodeCharKind::Normal, FullCodeCharKind::InString) => {
1383 FullCodeCharKind::StartString
1385 (FullCodeCharKind::InString, FullCodeCharKind::Normal) => {
1386 FullCodeCharKind::EndString
1388 (FullCodeCharKind::InComment, FullCodeCharKind::InStringCommented) => {
1389 FullCodeCharKind::StartStringCommented
1391 (FullCodeCharKind::InStringCommented, FullCodeCharKind::InComment) => {
1392 FullCodeCharKind::EndStringCommented
1401 // Workaround for CRLF newline.
1402 if line.ends_with('\r') {
1406 Some((self.kind, line))
1410 /// Iterator over functional and commented parts of a string. Any part of a string is either
1411 /// functional code, either *one* block comment, either *one* line comment. Whitespace between
1412 /// comments is functional code. Line comments contain their ending newlines.
1413 struct UngroupedCommentCodeSlices<'a> {
1415 iter: iter::Peekable<CharClasses<std::str::CharIndices<'a>>>,
1418 impl<'a> UngroupedCommentCodeSlices<'a> {
1419 fn new(code: &'a str) -> UngroupedCommentCodeSlices<'a> {
1420 UngroupedCommentCodeSlices {
1422 iter: CharClasses::new(code.char_indices()).peekable(),
1427 impl<'a> Iterator for UngroupedCommentCodeSlices<'a> {
1428 type Item = (CodeCharKind, usize, &'a str);
1430 fn next(&mut self) -> Option<Self::Item> {
1431 let (kind, (start_idx, _)) = self.iter.next()?;
1433 FullCodeCharKind::Normal | FullCodeCharKind::InString => {
1434 // Consume all the Normal code
1435 while let Some(&(char_kind, _)) = self.iter.peek() {
1436 if char_kind.is_comment() {
1439 let _ = self.iter.next();
1442 FullCodeCharKind::StartComment => {
1443 // Consume the whole comment
1445 match self.iter.next() {
1446 Some((kind, ..)) if kind.inside_comment() => continue,
1453 let slice = match self.iter.peek() {
1454 Some(&(_, (end_idx, _))) => &self.slice[start_idx..end_idx],
1455 None => &self.slice[start_idx..],
1458 if kind.is_comment() {
1459 CodeCharKind::Comment
1461 CodeCharKind::Normal
1469 /// Iterator over an alternating sequence of functional and commented parts of
1470 /// a string. The first item is always a, possibly zero length, subslice of
1471 /// functional text. Line style comments contain their ending newlines.
1472 pub(crate) struct CommentCodeSlices<'a> {
1474 last_slice_kind: CodeCharKind,
1475 last_slice_end: usize,
1478 impl<'a> CommentCodeSlices<'a> {
1479 pub(crate) fn new(slice: &'a str) -> CommentCodeSlices<'a> {
1482 last_slice_kind: CodeCharKind::Comment,
1488 impl<'a> Iterator for CommentCodeSlices<'a> {
1489 type Item = (CodeCharKind, usize, &'a str);
1491 fn next(&mut self) -> Option<Self::Item> {
1492 if self.last_slice_end == self.slice.len() {
1496 let mut sub_slice_end = self.last_slice_end;
1497 let mut first_whitespace = None;
1498 let subslice = &self.slice[self.last_slice_end..];
1499 let mut iter = CharClasses::new(subslice.char_indices());
1501 for (kind, (i, c)) in &mut iter {
1502 let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal
1503 && &subslice[..2] == "//"
1504 && [' '
, '
\t'
].contains(&c
);
1506 if is_comment_connector
&& first_whitespace
.is_none() {
1507 first_whitespace
= Some(i
);
1510 if kind
.to_codecharkind() == self.last_slice_kind
&& !is_comment_connector
{
1511 let last_index
= match first_whitespace
{
1515 sub_slice_end
= self.last_slice_end
+ last_index
;
1519 if !is_comment_connector
{
1520 first_whitespace
= None
;
1524 if let (None
, true) = (iter
.next(), sub_slice_end
== self.last_slice_end
) {
1525 // This was the last subslice.
1526 sub_slice_end
= match first_whitespace
{
1527 Some(i
) => self.last_slice_end
+ i
,
1528 None
=> self.slice
.len(),
1532 let kind
= match self.last_slice_kind
{
1533 CodeCharKind
::Comment
=> CodeCharKind
::Normal
,
1534 CodeCharKind
::Normal
=> CodeCharKind
::Comment
,
1538 self.last_slice_end
,
1539 &self.slice
[self.last_slice_end
..sub_slice_end
],
1541 self.last_slice_end
= sub_slice_end
;
1542 self.last_slice_kind
= kind
;
1548 /// Checks is `new` didn't miss any comment from `span`, if it removed any, return previous text
1549 /// (if it fits in the width/offset, else return `None`), else return `new`
1550 pub(crate) fn recover_comment_removed(
1553 context
: &RewriteContext
<'_
>,
1554 ) -> Option
<String
> {
1555 let snippet
= context
.snippet(span
);
1556 if snippet
!= new
&& changed_comment_content(snippet
, &new
) {
1557 // We missed some comments. Warn and keep the original text.
1558 if context
.config
.error_on_unformatted() {
1559 context
.report
.append(
1560 context
.parse_sess
.span_to_filename(span
),
1561 vec
![FormattingError
::from_span(
1563 &context
.parse_sess
,
1564 ErrorKind
::LostComment
,
1568 Some(snippet
.to_owned())
1574 pub(crate) fn filter_normal_code(code
: &str) -> String
{
1575 let mut buffer
= String
::with_capacity(code
.len());
1576 LineClasses
::new(code
).for_each(|(kind
, line
)| match kind
{
1577 FullCodeCharKind
::Normal
1578 | FullCodeCharKind
::StartString
1579 | FullCodeCharKind
::InString
1580 | FullCodeCharKind
::EndString
=> {
1581 buffer
.push_str(&line
);
1586 if !code
.ends_with('
\n'
) && buffer
.ends_with('
\n'
) {
1592 /// Returns `true` if the two strings of code have the same payload of comments.
1593 /// The payload of comments is everything in the string except:
1594 /// - actual code (not comments),
1595 /// - comment start/end marks,
1597 /// - '*' at the beginning of lines in block comments.
1598 fn changed_comment_content(orig
: &str, new
: &str) -> bool
{
1599 // Cannot write this as a fn since we cannot return types containing closures.
1600 let code_comment_content
= |code
| {
1601 let slices
= UngroupedCommentCodeSlices
::new(code
);
1603 .filter(|&(ref kind
, _
, _
)| *kind
== CodeCharKind
::Comment
)
1604 .flat_map(|(_
, _
, s
)| CommentReducer
::new(s
))
1606 let res
= code_comment_content(orig
).ne(code_comment_content(new
));
1608 "comment::changed_comment_content: {}\norig: '{}'\nnew: '{}'\nraw_old: {}\nraw_new: {}",
1612 code_comment_content(orig
).collect
::<String
>(),
1613 code_comment_content(new
).collect
::<String
>()
1618 /// Iterator over the 'payload' characters of a comment.
1619 /// It skips whitespace, comment start/end marks, and '*' at the beginning of lines.
1620 /// The comment must be one comment, ie not more than one start mark (no multiple line comments,
1622 struct CommentReducer
<'a
> {
1624 at_start_line
: bool
,
1625 iter
: std
::str::Chars
<'a
>,
1628 impl<'a
> CommentReducer
<'a
> {
1629 fn new(comment
: &'a
str) -> CommentReducer
<'a
> {
1630 let is_block
= comment
.starts_with("/*");
1631 let comment
= remove_comment_header(comment
);
1634 // There are no supplementary '*' on the first line.
1635 at_start_line
: false,
1636 iter
: comment
.chars(),
1641 impl<'a
> Iterator
for CommentReducer
<'a
> {
1644 fn next(&mut self) -> Option
<Self::Item
> {
1646 let mut c
= self.iter
.next()?
;
1647 if self.is_block
&& self.at_start_line
{
1648 while c
.is_whitespace() {
1649 c
= self.iter
.next()?
;
1651 // Ignore leading '*'.
1653 c
= self.iter
.next()?
;
1655 } else if c
== '
\n'
{
1656 self.at_start_line
= true;
1658 if !c
.is_whitespace() {
1665 fn remove_comment_header(comment
: &str) -> &str {
1666 if comment
.starts_with("///") || comment
.starts_with("//!") {
1668 } else if comment
.starts_with("//") {
1670 } else if (comment
.starts_with("/**") && !comment
.starts_with("/**/"))
1671 || comment
.starts_with("/*!")
1673 &comment
[3..comment
.len() - 2]
1676 comment
.starts_with("/*"),
1677 format
!("string '{}' is not a comment", comment
)
1679 &comment
[2..comment
.len() - 2]
1686 use crate::shape
::{Indent, Shape}
;
1690 let mut iter
= CharClasses
::new("//\n\n".chars());
1692 assert_eq
!((FullCodeCharKind
::StartComment
, '
/'
), iter
.next().unwrap());
1693 assert_eq
!((FullCodeCharKind
::InComment
, '
/'
), iter
.next().unwrap());
1694 assert_eq
!((FullCodeCharKind
::EndComment
, '
\n'
), iter
.next().unwrap());
1695 assert_eq
!((FullCodeCharKind
::Normal
, '
\n'
), iter
.next().unwrap());
1696 assert_eq
!(None
, iter
.next());
1700 fn comment_code_slices() {
1701 let input
= "code(); /* test */ 1 + 1";
1702 let mut iter
= CommentCodeSlices
::new(input
);
1704 assert_eq
!((CodeCharKind
::Normal
, 0, "code(); "), iter
.next().unwrap());
1706 (CodeCharKind
::Comment
, 8, "/* test */"),
1707 iter
.next().unwrap()
1709 assert_eq
!((CodeCharKind
::Normal
, 18, " 1 + 1"), iter
.next().unwrap());
1710 assert_eq
!(None
, iter
.next());
1714 fn comment_code_slices_two() {
1715 let input
= "// comment\n test();";
1716 let mut iter
= CommentCodeSlices
::new(input
);
1718 assert_eq
!((CodeCharKind
::Normal
, 0, ""), iter
.next().unwrap());
1720 (CodeCharKind
::Comment
, 0, "// comment\n"),
1721 iter
.next().unwrap()
1724 (CodeCharKind
::Normal
, 11, " test();"),
1725 iter
.next().unwrap()
1727 assert_eq
!(None
, iter
.next());
1731 fn comment_code_slices_three() {
1732 let input
= "1 // comment\n // comment2\n\n";
1733 let mut iter
= CommentCodeSlices
::new(input
);
1735 assert_eq
!((CodeCharKind
::Normal
, 0, "1 "), iter
.next().unwrap());
1737 (CodeCharKind
::Comment
, 2, "// comment\n // comment2\n"),
1738 iter
.next().unwrap()
1740 assert_eq
!((CodeCharKind
::Normal
, 29, "\n"), iter
.next().unwrap());
1741 assert_eq
!(None
, iter
.next());
1746 fn format_doc_comments() {
1747 let mut wrap_normalize_config
: crate::config
::Config
= Default
::default();
1748 wrap_normalize_config
.set().wrap_comments(true);
1749 wrap_normalize_config
.set().normalize_comments(true);
1751 let mut wrap_config
: crate::config
::Config
= Default
::default();
1752 wrap_config
.set().wrap_comments(true);
1754 let comment
= rewrite_comment(" //test",
1756 Shape
::legacy(100, Indent
::new(0, 100)),
1757 &wrap_normalize_config
).unwrap();
1758 assert_eq
!("/* test */", comment
);
1760 let comment
= rewrite_comment("// comment on a",
1762 Shape
::legacy(10, Indent
::empty()),
1763 &wrap_normalize_config
).unwrap();
1764 assert_eq
!("// comment\n// on a", comment
);
1766 let comment
= rewrite_comment("// A multi line comment\n // between args.",
1768 Shape
::legacy(60, Indent
::new(0, 12)),
1769 &wrap_normalize_config
).unwrap();
1770 assert_eq
!("// A multi line comment\n // between args.", comment
);
1772 let input
= "// comment";
1775 let comment
= rewrite_comment(input
,
1777 Shape
::legacy(9, Indent
::new(0, 69)),
1778 &wrap_normalize_config
).unwrap();
1779 assert_eq
!(expected
, comment
);
1781 let comment
= rewrite_comment("/* trimmed */",
1783 Shape
::legacy(100, Indent
::new(0, 100)),
1784 &wrap_normalize_config
).unwrap();
1785 assert_eq
!("/* trimmed */", comment
);
1787 // Check that different comment style are properly recognised.
1788 let comment
= rewrite_comment(r
#"/// test1
1794 Shape
::legacy(100, Indent
::new(0, 0)),
1795 &wrap_normalize_config
).unwrap();
1796 assert_eq
!("/// test1\n/// test2\n// test3", comment
);
1798 // Check that the blank line marks the end of a commented paragraph.
1799 let comment
= rewrite_comment(r
#"// test1
1803 Shape
::legacy(100, Indent
::new(0, 0)),
1804 &wrap_normalize_config
).unwrap();
1805 assert_eq
!("// test1\n\n// test2", comment
);
1807 // Check that the blank line marks the end of a custom-commented paragraph.
1808 let comment
= rewrite_comment(r
#"//@ test1
1812 Shape
::legacy(100, Indent
::new(0, 0)),
1813 &wrap_normalize_config
).unwrap();
1814 assert_eq
!("//@ test1\n\n//@ test2", comment
);
1816 // Check that bare lines are just indented but otherwise left unchanged.
1817 let comment
= rewrite_comment(r
#"// test1
1824 Shape
::legacy(100, Indent
::new(0, 0)),
1825 &wrap_config
).unwrap();
1826 assert_eq
!("// test1\n/*\n a bare line!\n\n another bare line!\n*/", comment
);
1829 // This is probably intended to be a non-test fn, but it is not used.
1830 // We should keep this around unless it helps us test stuff to remove it.
1831 fn uncommented(text
: &str) -> String
{
1832 CharClasses
::new(text
.chars())
1833 .filter_map(|(s
, c
)| match s
{
1834 FullCodeCharKind
::Normal
| FullCodeCharKind
::InString
=> Some(c
),
1841 fn test_uncommented() {
1842 assert_eq
!(&uncommented("abc/*...*/"), "abc");
1844 &uncommented("// .... /* \n../* /* *** / */ */a/* // */c\n"),
1847 assert_eq
!(&uncommented("abc \" /* */\" qsdf"), "abc \" /* */\" qsdf");
1851 fn test_contains_comment() {
1852 assert_eq
!(contains_comment("abc"), false);
1853 assert_eq
!(contains_comment("abc // qsdf"), true);
1854 assert_eq
!(contains_comment("abc /* kqsdf"), true);
1855 assert_eq
!(contains_comment("abc \" /* */\" qsdf"), false);
1859 fn test_find_uncommented() {
1860 fn check(haystack
: &str, needle
: &str, expected
: Option
<usize>) {
1861 assert_eq
!(expected
, haystack
.find_uncommented(needle
));
1864 check("/*/ */test", "test", Some(6));
1865 check("//test\ntest", "test", Some(7));
1866 check("/* comment only */", "whatever", None
);
1868 "/* comment */ some text /* more commentary */ result",
1872 check("sup // sup", "p", Some(2));
1873 check("sup", "x", None
);
1874 check(r
#"π? /**/ π is nice!"#, r#"π is nice"#, Some(9));
1875 check("/*sup yo? \n sup*/ sup", "p", Some(20));
1876 check("hel/*lohello*/lo", "hello", None
);
1877 check("acb", "ab", None
);
1878 check(",/*A*/ ", ",", Some(0));
1879 check("abc", "abc", Some(0));
1880 check("/* abc */", "abc", None
);
1881 check("/**/abc/* */", "abc", Some(4));
1882 check("\"/* abc */\"", "abc", Some(4));
1883 check("\"/* abc", "abc", Some(4));
1887 fn test_filter_normal_code() {
1890 println!("hello, world");
1893 assert_eq
!(s
, filter_normal_code(s
));
1894 let s_with_comment
= r
#"
1897 println!("hello, world");
1900 assert_eq
!(s
, filter_normal_code(s_with_comment
));