1use std::{borrow::Cow, iter};
4
5use itertools::{Itertools as _, MultiPeek, multipeek};
6use rustc_span::Span;
7use tracing::{debug, trace};
8
9use crate::config::Config;
10use crate::rewrite::{RewriteContext, RewriteErrorExt, RewriteResult};
11use crate::shape::{Indent, Shape};
12use crate::string::{StringFormat, rewrite_string};
13use crate::utils::{
14 count_newlines, first_line_width, last_line_width, trim_left_preserve_layout,
15 trimmed_last_line_width, unicode_str_width,
16};
17use crate::{ErrorKind, FormattingError};
18
19fn is_custom_comment(comment: &str) -> bool {
20 if !comment.starts_with("//") {
21 false
22 } else if let Some(c) = comment.chars().nth(2) {
23 !c.is_alphanumeric() && !c.is_whitespace()
24 } else {
25 false
26 }
27}
28
29#[derive(Copy, Clone, PartialEq, Eq)]
30pub(crate) enum CommentStyle<'a> {
31 DoubleSlash,
32 TripleSlash,
33 Doc,
34 SingleBullet,
35 DoubleBullet,
36 Exclamation,
37 Custom(&'a str),
38}
39
40fn custom_opener(s: &str) -> &str {
41 s.lines().next().map_or("", |first_line| {
42 first_line
43 .find(' ')
44 .map_or(first_line, |space_index| &first_line[0..=space_index])
45 })
46}
47
48impl<'a> CommentStyle<'a> {
49 pub(crate) fn is_line_comment(&self) -> bool {
51 matches!(
52 self,
53 CommentStyle::DoubleSlash
54 | CommentStyle::TripleSlash
55 | CommentStyle::Doc
56 | CommentStyle::Custom(_)
57 )
58 }
59
60 pub(crate) fn is_block_comment(&self) -> bool {
62 matches!(
63 self,
64 CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation
65 )
66 }
67
68 pub(crate) fn is_doc_comment(&self) -> bool {
70 matches!(*self, CommentStyle::TripleSlash | CommentStyle::Doc)
71 }
72
73 pub(crate) fn opener(&self) -> &'a str {
74 match *self {
75 CommentStyle::DoubleSlash => "// ",
76 CommentStyle::TripleSlash => "/// ",
77 CommentStyle::Doc => "//! ",
78 CommentStyle::SingleBullet => "/* ",
79 CommentStyle::DoubleBullet => "/** ",
80 CommentStyle::Exclamation => "/*! ",
81 CommentStyle::Custom(opener) => opener,
82 }
83 }
84
85 pub(crate) fn closer(&self) -> &'a str {
86 match *self {
87 CommentStyle::DoubleSlash
88 | CommentStyle::TripleSlash
89 | CommentStyle::Custom(..)
90 | CommentStyle::Doc => "",
91 CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
92 " */"
93 }
94 }
95 }
96
97 pub(crate) fn line_start(&self) -> &'a str {
98 match *self {
99 CommentStyle::DoubleSlash => "// ",
100 CommentStyle::TripleSlash => "/// ",
101 CommentStyle::Doc => "//! ",
102 CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
103 " * "
104 }
105 CommentStyle::Custom(opener) => opener,
106 }
107 }
108
109 pub(crate) fn to_str_tuplet(&self) -> (&'a str, &'a str, &'a str) {
110 (self.opener(), self.closer(), self.line_start())
111 }
112}
113
114pub(crate) 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
144pub(crate) fn is_last_comment_block(s: &str) -> bool {
146 s.trim_end().ends_with("*/")
147}
148
149pub(crate) fn combine_strs_with_missing_comments(
155 context: &RewriteContext<'_>,
156 prev_str: &str,
157 next_str: &str,
158 span: Span,
159 shape: Shape,
160 allow_extend: bool,
161) -> RewriteResult {
162 trace!(
163 "combine_strs_with_missing_comments `{}` `{}` {:?} {:?}",
164 prev_str, next_str, span, shape
165 );
166
167 let mut result =
168 String::with_capacity(prev_str.len() + next_str.len() + shape.indent.width() + 128);
169 result.push_str(prev_str);
170 let mut allow_one_line = !prev_str.contains('\n') && !next_str.contains('\n');
171 let first_sep =
172 if prev_str.is_empty() || next_str.is_empty() || trimmed_last_line_width(prev_str) == 0 {
173 ""
174 } else {
175 " "
176 };
177 let mut one_line_width = last_line_width(prev_str, context.config.tab_spaces())
178 + first_line_width(next_str)
179 + first_sep.len();
180
181 let config = context.config;
182 let indent = shape.indent;
183 let missing_comment = rewrite_missing_comment(span, shape, context)?;
184
185 if missing_comment.is_empty() {
186 if allow_extend && one_line_width <= shape.width {
187 result.push_str(first_sep);
188 } else if !prev_str.is_empty() {
189 result.push_str(&indent.to_string_with_newline(config))
190 }
191 result.push_str(next_str);
192 return Ok(result);
193 }
194
195 let original_snippet = context.snippet(span);
201 let prefer_same_line = if let Some(pos) = original_snippet.find('/') {
202 !original_snippet[..pos].contains('\n')
203 } else {
204 !original_snippet.contains('\n')
205 };
206
207 one_line_width -= first_sep.len();
208 let first_sep = if prev_str.is_empty() || missing_comment.is_empty() {
209 Cow::from("")
210 } else {
211 let one_line_width = last_line_width(prev_str, context.config.tab_spaces())
212 + first_line_width(&missing_comment)
213 + 1;
214 if prefer_same_line && one_line_width <= shape.width {
215 Cow::from(" ")
216 } else {
217 indent.to_string_with_newline(config)
218 }
219 };
220 result.push_str(&first_sep);
221 result.push_str(&missing_comment);
222
223 let second_sep = if missing_comment.is_empty() || next_str.is_empty() {
224 Cow::from("")
225 } else if missing_comment.starts_with("//") {
226 indent.to_string_with_newline(config)
227 } else {
228 one_line_width += missing_comment.len() + first_sep.len() + 1;
229 allow_one_line &= !missing_comment.starts_with("//") && !missing_comment.contains('\n');
230 if prefer_same_line && allow_one_line && one_line_width <= shape.width {
231 Cow::from(" ")
232 } else {
233 indent.to_string_with_newline(config)
234 }
235 };
236 result.push_str(&second_sep);
237 result.push_str(next_str);
238
239 Ok(result)
240}
241
242pub(crate) fn rewrite_doc_comment(orig: &str, shape: Shape, config: &Config) -> RewriteResult {
243 identify_comment(orig, false, shape, config, true)
244}
245
246pub(crate) fn rewrite_comment(
247 orig: &str,
248 block_style: bool,
249 shape: Shape,
250 config: &Config,
251) -> RewriteResult {
252 identify_comment(orig, block_style, shape, config, false)
253}
254
255fn identify_comment(
256 orig: &str,
257 block_style: bool,
258 shape: Shape,
259 config: &Config,
260 is_doc_comment: bool,
261) -> RewriteResult {
262 let style = comment_style(orig, false);
263
264 fn compute_len(orig: &str, line: &str) -> usize {
267 if orig.len() > line.len() {
268 if orig.as_bytes()[line.len()] == b'\r' {
269 line.len() + 2
270 } else {
271 line.len() + 1
272 }
273 } else {
274 line.len()
275 }
276 }
277
278 fn consume_same_line_comments(
284 style: CommentStyle<'_>,
285 orig: &str,
286 line_start: &str,
287 ) -> (bool, usize) {
288 let mut first_group_ending = 0;
289 let mut hbl = false;
290
291 for line in orig.lines() {
292 let trimmed_line = line.trim_start();
293 if trimmed_line.is_empty() {
294 hbl = true;
295 break;
296 } else if trimmed_line.starts_with(line_start)
297 || comment_style(trimmed_line, false) == style
298 {
299 first_group_ending += compute_len(&orig[first_group_ending..], line);
300 } else {
301 break;
302 }
303 }
304 (hbl, first_group_ending)
305 }
306
307 let (has_bare_lines, first_group_ending) = match style {
308 CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => {
309 let line_start = style.line_start().trim_start();
310 consume_same_line_comments(style, orig, line_start)
311 }
312 CommentStyle::Custom(opener) => {
313 let trimmed_opener = opener.trim_end();
314 consume_same_line_comments(style, orig, trimmed_opener)
315 }
316 CommentStyle::DoubleBullet | CommentStyle::SingleBullet | CommentStyle::Exclamation => {
318 let closer = style.closer().trim_start();
319 let mut count = orig.matches(closer).count();
320 let mut closing_symbol_offset = 0;
321 let mut hbl = false;
322 let mut first = true;
323 for line in orig.lines() {
324 closing_symbol_offset += compute_len(&orig[closing_symbol_offset..], line);
325 let mut trimmed_line = line.trim_start();
326 if !trimmed_line.starts_with('*')
327 && !trimmed_line.starts_with("//")
328 && !trimmed_line.starts_with("/*")
329 {
330 hbl = true;
331 }
332
333 if first {
335 let opener = style.opener().trim_end();
336 trimmed_line = &trimmed_line[opener.len()..];
337 first = false;
338 }
339 if trimmed_line.ends_with(closer) {
340 count -= 1;
341 if count == 0 {
342 break;
343 }
344 }
345 }
346 (hbl, closing_symbol_offset)
347 }
348 };
349
350 let (first_group, rest) = orig.split_at(first_group_ending);
351 let rewritten_first_group =
352 if !config.normalize_comments() && has_bare_lines && style.is_block_comment() {
353 trim_left_preserve_layout(first_group, shape.indent, config).unknown_error()?
354 } else if !config.normalize_comments()
355 && !config.wrap_comments()
356 && !(
357 is_doc_comment && config.format_code_in_doc_comments()
360 )
361 {
362 light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)
363 } else {
364 rewrite_comment_inner(
365 first_group,
366 block_style,
367 style,
368 shape,
369 config,
370 is_doc_comment || style.is_doc_comment(),
371 )?
372 };
373 if rest.is_empty() {
374 Ok(rewritten_first_group)
375 } else {
376 identify_comment(
377 rest.trim_start(),
378 block_style,
379 shape,
380 config,
381 is_doc_comment,
382 )
383 .map(|rest_str| {
384 format!(
385 "{}\n{}{}{}",
386 rewritten_first_group,
387 if has_bare_lines && style.is_line_comment() {
389 "\n"
390 } else {
391 ""
392 },
393 shape.indent.to_string(config),
394 rest_str
395 )
396 })
397 }
398}
399
400enum CodeBlockAttribute {
402 Rust,
403 NotRust,
404}
405
406impl CodeBlockAttribute {
407 fn new(attributes: &str) -> CodeBlockAttribute {
411 for attribute in attributes.split(',') {
412 match attribute.trim() {
413 "" | "rust" | "should_panic" | "no_run" | "edition2015" | "edition2018"
414 | "edition2021" => (),
415 "ignore" | "compile_fail" | "text" => return CodeBlockAttribute::NotRust,
416 _ => return CodeBlockAttribute::NotRust,
417 }
418 }
419 CodeBlockAttribute::Rust
420 }
421}
422
423struct ItemizedBlock {
434 lines: Vec<String>,
436 indent: usize,
438 opener: String,
440 line_start: String,
442}
443
444impl ItemizedBlock {
445 fn get_marker_length(trimmed: &str) -> Option<usize> {
466 let itemized_start = ["* ", "- ", "> ", "+ "];
469 if itemized_start.iter().any(|s| trimmed.starts_with(s)) {
470 return Some(2); }
472
473 for suffix in [". ", ") "] {
476 if let Some((prefix, _)) = trimmed.split_once(suffix) {
477 let has_leading_digits = (1..=2).contains(&prefix.len())
478 && prefix.chars().all(|c| char::is_ascii_digit(&c));
479 if has_leading_digits {
480 return Some(prefix.len() + suffix.len());
481 }
482 }
483 }
484
485 None }
487
488 fn new(line: &str) -> Option<ItemizedBlock> {
491 let marker_length = ItemizedBlock::get_marker_length(line.trim_start())?;
492 let space_to_marker = line.chars().take_while(|c| c.is_whitespace()).count();
493 let mut indent = space_to_marker + marker_length;
494 let mut line_start = " ".repeat(indent);
495
496 if line.trim_start().starts_with('>') {
498 line_start = itemized_block_quote_start(line, line_start, 2);
503 indent = line_start.len();
504 }
505 Some(ItemizedBlock {
506 lines: vec![line[indent..].to_string()],
507 indent,
508 opener: line[..indent].to_string(),
509 line_start,
510 })
511 }
512
513 fn create_string_format<'a>(&'a self, fmt: &'a StringFormat<'_>) -> StringFormat<'a> {
515 StringFormat {
516 opener: "",
517 closer: "",
518 line_start: "",
519 line_end: "",
520 shape: Shape::legacy(fmt.shape.width.saturating_sub(self.indent), Indent::empty()),
521 trim_end: true,
522 config: fmt.config,
523 }
524 }
525
526 fn add_line(&mut self, line: &str) -> bool {
529 if ItemizedBlock::get_marker_length(line.trim_start()).is_none()
530 && self.indent <= line.chars().take_while(|c| c.is_whitespace()).count()
531 {
532 self.lines.push(line.to_string());
533 return true;
534 }
535 false
536 }
537
538 fn trimmed_block_as_string(&self) -> String {
540 self.lines.iter().fold(String::new(), |mut acc, line| {
541 acc.push_str(line.trim_start());
542 acc.push(' ');
543 acc
544 })
545 }
546
547 fn original_block_as_string(&self) -> String {
549 self.lines.join("\n")
550 }
551}
552
553fn itemized_block_quote_start(line: &str, mut line_start: String, remove_indent: usize) -> String {
557 let quote_level = line
558 .chars()
559 .take_while(|c| !c.is_alphanumeric())
560 .fold(0, |acc, c| if c == '>' { acc + 1 } else { acc });
561
562 for _ in 0..remove_indent {
563 line_start.pop();
564 }
565
566 for _ in 0..quote_level {
567 line_start.push_str("> ");
568 }
569 line_start
570}
571
572struct CommentRewrite<'a> {
573 result: String,
574 code_block_buffer: String,
575 is_prev_line_multi_line: bool,
576 code_block_attr: Option<CodeBlockAttribute>,
577 item_block: Option<ItemizedBlock>,
578 comment_line_separator: String,
579 indent_str: String,
580 max_width: usize,
581 fmt_indent: Indent,
582 fmt: StringFormat<'a>,
583
584 opener: String,
585 closer: String,
586 line_start: String,
587 style: CommentStyle<'a>,
588}
589
590impl<'a> CommentRewrite<'a> {
591 fn new(
592 orig: &'a str,
593 block_style: bool,
594 shape: Shape,
595 config: &'a Config,
596 ) -> CommentRewrite<'a> {
597 let ((opener, closer, line_start), style) = if block_style {
598 (
599 CommentStyle::SingleBullet.to_str_tuplet(),
600 CommentStyle::SingleBullet,
601 )
602 } else {
603 let style = comment_style(orig, config.normalize_comments());
604 (style.to_str_tuplet(), style)
605 };
606
607 let max_width = shape
608 .width
609 .checked_sub(closer.len() + opener.len())
610 .unwrap_or(1);
611 let indent_str = shape.indent.to_string_with_newline(config).to_string();
612
613 let mut cr = CommentRewrite {
614 result: String::with_capacity(orig.len() * 2),
615 code_block_buffer: String::with_capacity(128),
616 is_prev_line_multi_line: false,
617 code_block_attr: None,
618 item_block: None,
619 comment_line_separator: format!("{indent_str}{line_start}"),
620 max_width,
621 indent_str,
622 fmt_indent: shape.indent,
623
624 fmt: StringFormat {
625 opener: "",
626 closer: "",
627 line_start,
628 line_end: "",
629 shape: Shape::legacy(max_width, shape.indent),
630 trim_end: true,
631 config,
632 },
633
634 opener: opener.to_owned(),
635 closer: closer.to_owned(),
636 line_start: line_start.to_owned(),
637 style,
638 };
639 cr.result.push_str(opener);
640 cr
641 }
642
643 fn join_block(s: &str, sep: &str) -> String {
644 let mut result = String::with_capacity(s.len() + 128);
645 let mut iter = s.lines().peekable();
646 while let Some(line) = iter.next() {
647 result.push_str(line);
648 result.push_str(match iter.peek() {
649 Some(&"") => sep.trim_end(),
650 Some(..) => sep,
651 None => "",
652 });
653 }
654 result
655 }
656
657 fn buffer_contains_comment(&self) -> bool {
661 self.result.len() != self.opener.len()
664 }
665
666 fn finish(mut self) -> String {
667 if !self.code_block_buffer.is_empty() {
668 self.result.push_str(&self.comment_line_separator);
671 self.result.push_str(&Self::join_block(
672 &trim_custom_comment_prefix(&self.code_block_buffer),
673 &self.comment_line_separator,
674 ));
675 }
676
677 if let Some(ref ib) = self.item_block {
678 self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
680 let item_fmt = ib.create_string_format(&self.fmt);
681
682 if self.buffer_contains_comment() {
684 self.result.push_str(&self.comment_line_separator);
685 }
686
687 self.result.push_str(&ib.opener);
688 match rewrite_string(
689 &ib.trimmed_block_as_string(),
690 &item_fmt,
691 self.max_width.saturating_sub(ib.indent),
692 ) {
693 Some(s) => self.result.push_str(&Self::join_block(
694 &s,
695 &format!("{}{}", self.comment_line_separator, ib.line_start),
696 )),
697 None => self.result.push_str(&Self::join_block(
698 &ib.original_block_as_string(),
699 &self.comment_line_separator,
700 )),
701 };
702 }
703
704 self.result.push_str(&self.closer);
705 if self.result.ends_with(&self.opener) && self.opener.ends_with(' ') {
706 self.result.pop();
708 }
709
710 self.result
711 }
712
713 fn handle_line(
714 &mut self,
715 orig: &'a str,
716 i: usize,
717 line: &'a str,
718 has_leading_whitespace: bool,
719 is_doc_comment: bool,
720 ) -> bool {
721 let num_newlines = count_newlines(orig);
722 let is_last = i == num_newlines;
723 let needs_new_comment_line = if self.style.is_block_comment() {
724 num_newlines > 0 || self.buffer_contains_comment()
725 } else {
726 self.buffer_contains_comment()
727 };
728
729 if let Some(ref mut ib) = self.item_block {
730 if ib.add_line(line) {
731 return false;
732 }
733 self.is_prev_line_multi_line = false;
734 self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
735 let item_fmt = ib.create_string_format(&self.fmt);
736
737 if needs_new_comment_line {
739 self.result.push_str(&self.comment_line_separator);
740 }
741
742 self.result.push_str(&ib.opener);
743 match rewrite_string(
744 &ib.trimmed_block_as_string(),
745 &item_fmt,
746 self.max_width.saturating_sub(ib.indent),
747 ) {
748 Some(s) => self.result.push_str(&Self::join_block(
749 &s,
750 &format!("{}{}", self.comment_line_separator, ib.line_start),
751 )),
752 None => self.result.push_str(&Self::join_block(
753 &ib.original_block_as_string(),
754 &self.comment_line_separator,
755 )),
756 };
757 } else if self.code_block_attr.is_some() {
758 if line.starts_with("```") {
759 let code_block = match self.code_block_attr.as_ref().unwrap() {
760 CodeBlockAttribute::Rust
761 if self.fmt.config.format_code_in_doc_comments()
762 && !self.code_block_buffer.trim().is_empty() =>
763 {
764 let mut config = self.fmt.config.clone();
765 config.set().wrap_comments(false);
766 let comment_max_width = config
767 .doc_comment_code_block_width()
768 .min(config.max_width());
769 config.set().max_width(comment_max_width);
770 if let Some(comment_use_small_heuristics) = config
771 .doc_comment_code_block_small_heuristics()
772 .to_heuristics()
773 {
774 config
775 .set()
776 .use_small_heuristics(comment_use_small_heuristics);
777 }
778 if let Some(s) =
779 crate::format_code_block(&self.code_block_buffer, &config, false)
780 {
781 trim_custom_comment_prefix(&s.snippet)
782 } else {
783 trim_custom_comment_prefix(&self.code_block_buffer)
784 }
785 }
786 _ => trim_custom_comment_prefix(&self.code_block_buffer),
787 };
788 if !code_block.is_empty() {
789 self.result.push_str(&self.comment_line_separator);
790 self.result
791 .push_str(&Self::join_block(&code_block, &self.comment_line_separator));
792 }
793 self.code_block_buffer.clear();
794 self.result.push_str(&self.comment_line_separator);
795 self.result.push_str(line);
796 self.code_block_attr = None;
797 } else {
798 self.code_block_buffer
799 .push_str(&hide_sharp_behind_comment(line));
800 self.code_block_buffer.push('\n');
801 }
802 return false;
803 }
804
805 self.code_block_attr = None;
806 self.item_block = None;
807 if let Some(stripped) = line.strip_prefix("```") {
808 self.code_block_attr = Some(CodeBlockAttribute::new(stripped))
809 } else if self.fmt.config.wrap_comments() {
810 if let Some(ib) = ItemizedBlock::new(line) {
811 self.item_block = Some(ib);
812 return false;
813 }
814 }
815
816 if self.result == self.opener {
817 let force_leading_whitespace = &self.opener == "/* " && count_newlines(orig) == 0;
818 if !has_leading_whitespace && !force_leading_whitespace && self.result.ends_with(' ') {
819 self.result.pop();
820 }
821 if line.is_empty() {
822 return false;
823 }
824 } else if self.is_prev_line_multi_line && !line.is_empty() {
825 self.result.push(' ')
826 } else if is_last && line.is_empty() {
827 if !self.closer.is_empty() {
829 self.result.push_str(&self.indent_str);
830 }
831 return true;
832 } else {
833 self.result.push_str(&self.comment_line_separator);
834 if !has_leading_whitespace && self.result.ends_with(' ') {
835 self.result.pop();
836 }
837 }
838
839 let is_markdown_header_doc_comment = is_doc_comment && line.starts_with('#');
840
841 let should_wrap_comment = self.fmt.config.wrap_comments()
849 && !is_markdown_header_doc_comment
850 && unicode_str_width(line) > self.fmt.shape.width
851 && !has_url(line)
852 && !is_table_item(line);
853
854 if should_wrap_comment {
855 match rewrite_string(line, &self.fmt, self.max_width) {
856 Some(ref s) => {
857 self.is_prev_line_multi_line = s.contains('\n');
858 self.result.push_str(s);
859 }
860 None if self.is_prev_line_multi_line => {
861 self.result.pop();
864 self.result.push_str(&self.comment_line_separator);
865 self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
866 match rewrite_string(line, &self.fmt, self.max_width) {
867 Some(ref s) => {
868 self.is_prev_line_multi_line = s.contains('\n');
869 self.result.push_str(s);
870 }
871 None => {
872 self.is_prev_line_multi_line = false;
873 self.result.push_str(line);
874 }
875 }
876 }
877 None => {
878 self.is_prev_line_multi_line = false;
879 self.result.push_str(line);
880 }
881 }
882
883 self.fmt.shape = if self.is_prev_line_multi_line {
884 let offset = 1 + last_line_width(&self.result, self.fmt.config.tab_spaces())
886 - self.line_start.len();
887 Shape {
888 width: self.max_width.saturating_sub(offset),
889 indent: self.fmt_indent,
890 offset: self.fmt.shape.offset + offset,
891 }
892 } else {
893 Shape::legacy(self.max_width, self.fmt_indent)
894 };
895 } else {
896 if line.is_empty() && self.result.ends_with(' ') && !is_last {
897 self.result.pop();
899 }
900 if self.code_block_attr.is_some() && self.is_prev_line_multi_line {
901 self.result.push_str(&self.comment_line_separator);
902 }
903 self.result.push_str(line);
904 self.fmt.shape = Shape::legacy(self.max_width, self.fmt_indent);
905 self.is_prev_line_multi_line = false;
906 }
907
908 false
909 }
910}
911
912fn rewrite_comment_inner(
913 orig: &str,
914 block_style: bool,
915 style: CommentStyle<'_>,
916 shape: Shape,
917 config: &Config,
918 is_doc_comment: bool,
919) -> RewriteResult {
920 let mut rewriter = CommentRewrite::new(orig, block_style, shape, config);
921
922 let line_breaks = count_newlines(orig.trim_end());
923 let lines = orig
924 .lines()
925 .enumerate()
926 .map(|(i, mut line)| {
927 line = trim_end_unless_two_whitespaces(line.trim_start(), is_doc_comment);
928 if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
930 line = line[..(line.len() - 2)].trim_end();
931 }
932
933 line
934 })
935 .map(|s| left_trim_comment_line(s, &style))
936 .map(|(line, has_leading_whitespace)| {
937 if orig.starts_with("/*") && line_breaks == 0 {
938 (
939 line.trim_start(),
940 has_leading_whitespace || config.normalize_comments(),
941 )
942 } else {
943 (line, has_leading_whitespace || config.normalize_comments())
944 }
945 });
946
947 for (i, (line, has_leading_whitespace)) in lines.enumerate() {
948 if rewriter.handle_line(orig, i, line, has_leading_whitespace, is_doc_comment) {
949 break;
950 }
951 }
952
953 Ok(rewriter.finish())
954}
955
956const RUSTFMT_CUSTOM_COMMENT_PREFIX: &str = "//#### ";
957
958fn hide_sharp_behind_comment(s: &str) -> Cow<'_, str> {
959 let s_trimmed = s.trim();
960 if s_trimmed.starts_with("# ") || s_trimmed == "#" {
961 Cow::from(format!("{RUSTFMT_CUSTOM_COMMENT_PREFIX}{s}"))
962 } else {
963 Cow::from(s)
964 }
965}
966
967fn trim_custom_comment_prefix(s: &str) -> String {
968 s.lines()
969 .map(|line| {
970 let left_trimmed = line.trim_start();
971 if left_trimmed.starts_with(RUSTFMT_CUSTOM_COMMENT_PREFIX) {
972 left_trimmed.trim_start_matches(RUSTFMT_CUSTOM_COMMENT_PREFIX)
973 } else {
974 line
975 }
976 })
977 .collect::<Vec<_>>()
978 .join("\n")
979}
980
981fn has_url(s: &str) -> bool {
983 let reference_link_url = static_regex!(r"^\[.+\]\s?:");
991
992 s.contains("https://")
994 || s.contains("http://")
995 || s.contains("ftp://")
996 || s.contains("file://")
997 || reference_link_url.is_match(s)
998}
999
1000fn is_table_item(mut s: &str) -> bool {
1002 s = s.trim_start();
1005 return s.starts_with('|')
1006 && match s.rfind('|') {
1007 Some(0) | None => false,
1008 _ => true,
1009 };
1010}
1011
1012pub(crate) fn rewrite_missing_comment(
1015 span: Span,
1016 shape: Shape,
1017 context: &RewriteContext<'_>,
1018) -> RewriteResult {
1019 let missing_snippet = context.snippet(span);
1020 let trimmed_snippet = missing_snippet.trim();
1021 let pos = trimmed_snippet.find('/');
1023 if !trimmed_snippet.is_empty() && pos.is_some() {
1024 rewrite_comment(trimmed_snippet, false, shape, context.config)
1025 } else {
1026 Ok(String::new())
1027 }
1028}
1029
1030pub(crate) fn recover_missing_comment_in_span(
1034 span: Span,
1035 shape: Shape,
1036 context: &RewriteContext<'_>,
1037 used_width: usize,
1038) -> RewriteResult {
1039 let missing_comment = rewrite_missing_comment(span, shape, context)?;
1040 if missing_comment.is_empty() {
1041 Ok(String::new())
1042 } else {
1043 let missing_snippet = context.snippet(span);
1044 let pos = missing_snippet.find('/').unknown_error()?;
1045 let total_width = missing_comment.len() + used_width + 1;
1047 let force_new_line_before_comment =
1048 missing_snippet[..pos].contains('\n') || total_width > context.config.max_width();
1049 let sep = if force_new_line_before_comment {
1050 shape.indent.to_string_with_newline(context.config)
1051 } else {
1052 Cow::from(" ")
1053 };
1054 Ok(format!("{sep}{missing_comment}"))
1055 }
1056}
1057
1058fn trim_end_unless_two_whitespaces(s: &str, is_doc_comment: bool) -> &str {
1060 if is_doc_comment && s.ends_with(" ") {
1061 s
1062 } else {
1063 s.trim_end()
1064 }
1065}
1066
1067fn light_rewrite_comment(
1069 orig: &str,
1070 offset: Indent,
1071 config: &Config,
1072 is_doc_comment: bool,
1073) -> String {
1074 orig.lines()
1075 .map(|l| {
1076 let first_non_whitespace = l.find(|c| !char::is_whitespace(c));
1080 let left_trimmed = if let Some(fnw) = first_non_whitespace {
1081 if l.as_bytes()[fnw] == b'*' {
1082 Cow::Owned(format!(" {}", &l[fnw..]))
1083 } else {
1084 Cow::Borrowed(&l[fnw..])
1085 }
1086 } else {
1087 Cow::Borrowed("")
1088 };
1089
1090 match left_trimmed {
1092 Cow::Borrowed(left_trimmed) => Cow::Borrowed(trim_end_unless_two_whitespaces(
1093 left_trimmed,
1094 is_doc_comment,
1095 )),
1096 Cow::Owned(left_trimmed) => {
1097 let trimmed = trim_end_unless_two_whitespaces(&left_trimmed, is_doc_comment);
1098 Cow::Owned(trimmed.to_string())
1099 }
1100 }
1101 })
1102 .join(&format!("\n{}", offset.to_string(config)))
1103}
1104
1105fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) {
1109 if line.starts_with("//! ")
1110 || line.starts_with("/// ")
1111 || line.starts_with("/*! ")
1112 || line.starts_with("/** ")
1113 {
1114 (&line[4..], true)
1115 } else if let CommentStyle::Custom(opener) = *style {
1116 if let Some(stripped) = line.strip_prefix(opener) {
1117 (stripped, true)
1118 } else {
1119 (&line[opener.trim_end().len()..], false)
1120 }
1121 } else if line.starts_with("/* ")
1122 || line.starts_with("// ")
1123 || line.starts_with("//!")
1124 || line.starts_with("///")
1125 || line.starts_with("** ")
1126 || line.starts_with("/*!")
1127 || (line.starts_with("/**") && !line.starts_with("/**/"))
1128 {
1129 (&line[3..], line.chars().nth(2).unwrap() == ' ')
1130 } else if line.starts_with("/*")
1131 || line.starts_with("* ")
1132 || line.starts_with("//")
1133 || line.starts_with("**")
1134 {
1135 (&line[2..], line.chars().nth(1).unwrap() == ' ')
1136 } else if let Some(stripped) = line.strip_prefix('*') {
1137 (stripped, false)
1138 } else {
1139 (line, line.starts_with(' '))
1140 }
1141}
1142
1143pub(crate) trait FindUncommented {
1144 fn find_uncommented(&self, pat: &str) -> Option<usize>;
1145 fn find_last_uncommented(&self, pat: &str) -> Option<usize>;
1146}
1147
1148impl FindUncommented for str {
1149 fn find_uncommented(&self, pat: &str) -> Option<usize> {
1150 let mut needle_iter = pat.chars();
1151 for (kind, (i, b)) in CharClasses::new(self.char_indices()) {
1152 match needle_iter.next() {
1153 None => {
1154 return Some(i - pat.len());
1155 }
1156 Some(c) => match kind {
1157 FullCodeCharKind::Normal | FullCodeCharKind::InString if b == c => {}
1158 _ => {
1159 needle_iter = pat.chars();
1160 }
1161 },
1162 }
1163 }
1164
1165 match needle_iter.next() {
1167 Some(_) => None,
1168 None => Some(self.len() - pat.len()),
1169 }
1170 }
1171
1172 fn find_last_uncommented(&self, pat: &str) -> Option<usize> {
1173 if let Some(left) = self.find_uncommented(pat) {
1174 let mut result = left;
1175 while let Some(next) = self[(result + 1)..].find_last_uncommented(pat) {
1177 result += next + 1;
1178 }
1179 Some(result)
1180 } else {
1181 None
1182 }
1183 }
1184}
1185
1186pub(crate) fn find_comment_end(s: &str) -> Option<usize> {
1191 let mut iter = CharClasses::new(s.char_indices());
1192 for (kind, (i, _c)) in &mut iter {
1193 if kind == FullCodeCharKind::Normal || kind == FullCodeCharKind::InString {
1194 return Some(i);
1195 }
1196 }
1197
1198 if iter.status == CharClassesStatus::Normal {
1200 Some(s.len())
1201 } else {
1202 None
1203 }
1204}
1205
1206pub(crate) fn contains_comment(text: &str) -> bool {
1208 CharClasses::new(text.chars()).any(|(kind, _)| kind.is_comment())
1209}
1210
1211pub(crate) struct CharClasses<T>
1212where
1213 T: Iterator,
1214 T::Item: RichChar,
1215{
1216 base: MultiPeek<T>,
1217 status: CharClassesStatus,
1218}
1219
1220pub(crate) trait RichChar {
1221 fn get_char(&self) -> char;
1222}
1223
1224impl RichChar for char {
1225 fn get_char(&self) -> char {
1226 *self
1227 }
1228}
1229
1230impl RichChar for (usize, char) {
1231 fn get_char(&self) -> char {
1232 self.1
1233 }
1234}
1235
1236#[derive(PartialEq, Eq, Debug, Clone, Copy)]
1237enum CharClassesStatus {
1238 Normal,
1239 LitString,
1241 LitStringEscape,
1242 LitRawString(u32),
1244 RawStringPrefix(u32),
1245 RawStringSuffix(u32),
1246 LitChar,
1247 LitCharEscape,
1248 BlockComment(u32),
1251 StringInBlockComment(u32),
1254 BlockCommentOpening(u32),
1257 BlockCommentClosing(u32),
1260 LineComment,
1262}
1263
1264#[derive(PartialEq, Eq, Debug, Clone, Copy)]
1266pub(crate) enum CodeCharKind {
1267 Normal,
1268 Comment,
1269}
1270
1271#[derive(PartialEq, Eq, Debug, Clone, Copy)]
1275pub(crate) enum FullCodeCharKind {
1276 Normal,
1277 StartComment,
1279 InComment,
1282 EndComment,
1284 StartStringCommented,
1286 EndStringCommented,
1288 InStringCommented,
1290 StartString,
1292 EndString,
1294 InString,
1296}
1297
1298impl FullCodeCharKind {
1299 pub(crate) fn is_comment(self) -> bool {
1300 match self {
1301 FullCodeCharKind::StartComment
1302 | FullCodeCharKind::InComment
1303 | FullCodeCharKind::EndComment
1304 | FullCodeCharKind::StartStringCommented
1305 | FullCodeCharKind::InStringCommented
1306 | FullCodeCharKind::EndStringCommented => true,
1307 _ => false,
1308 }
1309 }
1310
1311 pub(crate) fn inside_comment(self) -> bool {
1313 match self {
1314 FullCodeCharKind::InComment
1315 | FullCodeCharKind::StartStringCommented
1316 | FullCodeCharKind::InStringCommented
1317 | FullCodeCharKind::EndStringCommented => true,
1318 _ => false,
1319 }
1320 }
1321
1322 pub(crate) fn is_string(self) -> bool {
1323 self == FullCodeCharKind::InString || self == FullCodeCharKind::StartString
1324 }
1325
1326 pub(crate) fn is_commented_string(self) -> bool {
1328 self == FullCodeCharKind::InStringCommented
1329 || self == FullCodeCharKind::StartStringCommented
1330 }
1331
1332 fn to_codecharkind(self) -> CodeCharKind {
1333 if self.is_comment() {
1334 CodeCharKind::Comment
1335 } else {
1336 CodeCharKind::Normal
1337 }
1338 }
1339}
1340
1341impl<T> CharClasses<T>
1342where
1343 T: Iterator,
1344 T::Item: RichChar,
1345{
1346 pub(crate) fn new(base: T) -> CharClasses<T> {
1347 CharClasses {
1348 base: multipeek(base),
1349 status: CharClassesStatus::Normal,
1350 }
1351 }
1352}
1353
1354fn is_raw_string_prefix<T>(iter: &mut MultiPeek<T>) -> bool
1358where
1359 T: Iterator,
1360 T::Item: RichChar,
1361{
1362 while let Some(c) = iter.peek() {
1363 match c.get_char() {
1364 '#' => continue,
1365 '"' => return true,
1366 _ => return false,
1367 }
1368 }
1369 false
1370}
1371
1372fn is_raw_string_suffix<T>(iter: &mut MultiPeek<T>, count: u32) -> bool
1373where
1374 T: Iterator,
1375 T::Item: RichChar,
1376{
1377 for _ in 0..count {
1378 match iter.peek() {
1379 Some(c) if c.get_char() == '#' => continue,
1380 _ => return false,
1381 }
1382 }
1383 true
1384}
1385
1386impl<T> Iterator for CharClasses<T>
1387where
1388 T: Iterator,
1389 T::Item: RichChar,
1390{
1391 type Item = (FullCodeCharKind, T::Item);
1392
1393 fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> {
1394 let item = self.base.next()?;
1395 let chr = item.get_char();
1396 let mut char_kind = FullCodeCharKind::Normal;
1397 self.status = match self.status {
1398 CharClassesStatus::LitRawString(sharps) => {
1399 char_kind = FullCodeCharKind::InString;
1400 match chr {
1401 '"' => {
1402 if sharps == 0 {
1403 char_kind = FullCodeCharKind::Normal;
1404 CharClassesStatus::Normal
1405 } else if is_raw_string_suffix(&mut self.base, sharps) {
1406 CharClassesStatus::RawStringSuffix(sharps)
1407 } else {
1408 CharClassesStatus::LitRawString(sharps)
1409 }
1410 }
1411 _ => CharClassesStatus::LitRawString(sharps),
1412 }
1413 }
1414 CharClassesStatus::RawStringPrefix(sharps) => {
1415 char_kind = FullCodeCharKind::InString;
1416 match chr {
1417 '#' => CharClassesStatus::RawStringPrefix(sharps + 1),
1418 '"' => CharClassesStatus::LitRawString(sharps),
1419 _ => CharClassesStatus::Normal, }
1421 }
1422 CharClassesStatus::RawStringSuffix(sharps) => {
1423 match chr {
1424 '#' => {
1425 if sharps == 1 {
1426 CharClassesStatus::Normal
1427 } else {
1428 char_kind = FullCodeCharKind::InString;
1429 CharClassesStatus::RawStringSuffix(sharps - 1)
1430 }
1431 }
1432 _ => CharClassesStatus::Normal, }
1434 }
1435 CharClassesStatus::LitString => {
1436 char_kind = FullCodeCharKind::InString;
1437 match chr {
1438 '"' => CharClassesStatus::Normal,
1439 '\\' => CharClassesStatus::LitStringEscape,
1440 _ => CharClassesStatus::LitString,
1441 }
1442 }
1443 CharClassesStatus::LitStringEscape => {
1444 char_kind = FullCodeCharKind::InString;
1445 CharClassesStatus::LitString
1446 }
1447 CharClassesStatus::LitChar => match chr {
1448 '\\' => CharClassesStatus::LitCharEscape,
1449 '\'' => CharClassesStatus::Normal,
1450 _ => CharClassesStatus::LitChar,
1451 },
1452 CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar,
1453 CharClassesStatus::Normal => match chr {
1454 'r' => match self.base.peek().map(RichChar::get_char) {
1455 Some('"') => {
1456 char_kind = FullCodeCharKind::InString;
1457 CharClassesStatus::RawStringPrefix(0)
1458 }
1459 Some('#') if is_raw_string_prefix(&mut self.base) => {
1462 char_kind = FullCodeCharKind::InString;
1463 CharClassesStatus::RawStringPrefix(0)
1464 }
1465 _ => CharClassesStatus::Normal,
1466 },
1467 '"' => {
1468 char_kind = FullCodeCharKind::InString;
1469 CharClassesStatus::LitString
1470 }
1471 '\'' => {
1472 match self.base.peek() {
1474 Some(next) if next.get_char() == '\\' => {
1475 self.status = CharClassesStatus::LitChar;
1476 return Some((char_kind, item));
1477 }
1478 _ => (),
1479 }
1480
1481 match self.base.peek() {
1482 Some(next) if next.get_char() == '\'' => CharClassesStatus::LitChar,
1483 _ => CharClassesStatus::Normal,
1484 }
1485 }
1486 '/' => match self.base.peek() {
1487 Some(next) if next.get_char() == '*' => {
1488 self.status = CharClassesStatus::BlockCommentOpening(1);
1489 return Some((FullCodeCharKind::StartComment, item));
1490 }
1491 Some(next) if next.get_char() == '/' => {
1492 self.status = CharClassesStatus::LineComment;
1493 return Some((FullCodeCharKind::StartComment, item));
1494 }
1495 _ => CharClassesStatus::Normal,
1496 },
1497 _ => CharClassesStatus::Normal,
1498 },
1499 CharClassesStatus::StringInBlockComment(deepness) => {
1500 char_kind = FullCodeCharKind::InStringCommented;
1501 if chr == '"' {
1502 CharClassesStatus::BlockComment(deepness)
1503 } else if chr == '*' && self.base.peek().map(RichChar::get_char) == Some('/') {
1504 char_kind = FullCodeCharKind::InComment;
1505 CharClassesStatus::BlockCommentClosing(deepness - 1)
1506 } else {
1507 CharClassesStatus::StringInBlockComment(deepness)
1508 }
1509 }
1510 CharClassesStatus::BlockComment(deepness) => {
1511 assert_ne!(deepness, 0);
1512 char_kind = FullCodeCharKind::InComment;
1513 match self.base.peek() {
1514 Some(next) if next.get_char() == '/' && chr == '*' => {
1515 CharClassesStatus::BlockCommentClosing(deepness - 1)
1516 }
1517 Some(next) if next.get_char() == '*' && chr == '/' => {
1518 CharClassesStatus::BlockCommentOpening(deepness + 1)
1519 }
1520 _ if chr == '"' => CharClassesStatus::StringInBlockComment(deepness),
1521 _ => self.status,
1522 }
1523 }
1524 CharClassesStatus::BlockCommentOpening(deepness) => {
1525 assert_eq!(chr, '*');
1526 self.status = CharClassesStatus::BlockComment(deepness);
1527 return Some((FullCodeCharKind::InComment, item));
1528 }
1529 CharClassesStatus::BlockCommentClosing(deepness) => {
1530 assert_eq!(chr, '/');
1531 if deepness == 0 {
1532 self.status = CharClassesStatus::Normal;
1533 return Some((FullCodeCharKind::EndComment, item));
1534 } else {
1535 self.status = CharClassesStatus::BlockComment(deepness);
1536 return Some((FullCodeCharKind::InComment, item));
1537 }
1538 }
1539 CharClassesStatus::LineComment => match chr {
1540 '\n' => {
1541 self.status = CharClassesStatus::Normal;
1542 return Some((FullCodeCharKind::EndComment, item));
1543 }
1544 _ => {
1545 self.status = CharClassesStatus::LineComment;
1546 return Some((FullCodeCharKind::InComment, item));
1547 }
1548 },
1549 };
1550 Some((char_kind, item))
1551 }
1552}
1553
1554pub(crate) struct LineClasses<'a> {
1557 base: iter::Peekable<CharClasses<std::str::Chars<'a>>>,
1558 kind: FullCodeCharKind,
1559}
1560
1561impl<'a> LineClasses<'a> {
1562 pub(crate) fn new(s: &'a str) -> Self {
1563 LineClasses {
1564 base: CharClasses::new(s.chars()).peekable(),
1565 kind: FullCodeCharKind::Normal,
1566 }
1567 }
1568}
1569
1570impl<'a> Iterator for LineClasses<'a> {
1571 type Item = (FullCodeCharKind, String);
1572
1573 fn next(&mut self) -> Option<Self::Item> {
1574 self.base.peek()?;
1575
1576 let mut line = String::new();
1577
1578 let start_kind = match self.base.peek() {
1579 Some((kind, _)) => *kind,
1580 None => unreachable!(),
1581 };
1582
1583 for (kind, c) in self.base.by_ref() {
1584 self.kind = kind;
1586 if c == '\n' {
1587 self.kind = match (start_kind, kind) {
1588 (FullCodeCharKind::Normal, FullCodeCharKind::InString) => {
1589 FullCodeCharKind::StartString
1590 }
1591 (FullCodeCharKind::InString, FullCodeCharKind::Normal) => {
1592 FullCodeCharKind::EndString
1593 }
1594 (FullCodeCharKind::InComment, FullCodeCharKind::InStringCommented) => {
1595 FullCodeCharKind::StartStringCommented
1596 }
1597 (FullCodeCharKind::InStringCommented, FullCodeCharKind::InComment) => {
1598 FullCodeCharKind::EndStringCommented
1599 }
1600 _ => kind,
1601 };
1602 break;
1603 }
1604 line.push(c);
1605 }
1606
1607 if line.ends_with('\r') {
1609 line.pop();
1610 }
1611
1612 Some((self.kind, line))
1613 }
1614}
1615
1616struct UngroupedCommentCodeSlices<'a> {
1620 slice: &'a str,
1621 iter: iter::Peekable<CharClasses<std::str::CharIndices<'a>>>,
1622}
1623
1624impl<'a> UngroupedCommentCodeSlices<'a> {
1625 fn new(code: &'a str) -> UngroupedCommentCodeSlices<'a> {
1626 UngroupedCommentCodeSlices {
1627 slice: code,
1628 iter: CharClasses::new(code.char_indices()).peekable(),
1629 }
1630 }
1631}
1632
1633impl<'a> Iterator for UngroupedCommentCodeSlices<'a> {
1634 type Item = (CodeCharKind, usize, &'a str);
1635
1636 fn next(&mut self) -> Option<Self::Item> {
1637 let (kind, (start_idx, _)) = self.iter.next()?;
1638 match kind {
1639 FullCodeCharKind::Normal | FullCodeCharKind::InString => {
1640 while let Some(&(char_kind, _)) = self.iter.peek() {
1642 if char_kind.is_comment() {
1643 break;
1644 }
1645 let _ = self.iter.next();
1646 }
1647 }
1648 FullCodeCharKind::StartComment => {
1649 loop {
1651 match self.iter.next() {
1652 Some((kind, ..)) if kind.inside_comment() => continue,
1653 _ => break,
1654 }
1655 }
1656 }
1657 _ => panic!(),
1658 }
1659 let slice = match self.iter.peek() {
1660 Some(&(_, (end_idx, _))) => &self.slice[start_idx..end_idx],
1661 None => &self.slice[start_idx..],
1662 };
1663 Some((
1664 if kind.is_comment() {
1665 CodeCharKind::Comment
1666 } else {
1667 CodeCharKind::Normal
1668 },
1669 start_idx,
1670 slice,
1671 ))
1672 }
1673}
1674
1675pub(crate) struct CommentCodeSlices<'a> {
1679 slice: &'a str,
1680 last_slice_kind: CodeCharKind,
1681 last_slice_end: usize,
1682}
1683
1684impl<'a> CommentCodeSlices<'a> {
1685 pub(crate) fn new(slice: &'a str) -> CommentCodeSlices<'a> {
1686 CommentCodeSlices {
1687 slice,
1688 last_slice_kind: CodeCharKind::Comment,
1689 last_slice_end: 0,
1690 }
1691 }
1692}
1693
1694impl<'a> Iterator for CommentCodeSlices<'a> {
1695 type Item = (CodeCharKind, usize, &'a str);
1696
1697 fn next(&mut self) -> Option<Self::Item> {
1698 if self.last_slice_end == self.slice.len() {
1699 return None;
1700 }
1701
1702 let mut sub_slice_end = self.last_slice_end;
1703 let mut first_whitespace = None;
1704 let subslice = &self.slice[self.last_slice_end..];
1705 let mut iter = CharClasses::new(subslice.char_indices());
1706
1707 for (kind, (i, c)) in &mut iter {
1708 let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal
1709 && &subslice[..2] == "//"
1710 && [' ', '\t'].contains(&c);
1711
1712 if is_comment_connector && first_whitespace.is_none() {
1713 first_whitespace = Some(i);
1714 }
1715
1716 if kind.to_codecharkind() == self.last_slice_kind && !is_comment_connector {
1717 let last_index = match first_whitespace {
1718 Some(j) => j,
1719 None => i,
1720 };
1721 sub_slice_end = self.last_slice_end + last_index;
1722 break;
1723 }
1724
1725 if !is_comment_connector {
1726 first_whitespace = None;
1727 }
1728 }
1729
1730 if let (None, true) = (iter.next(), sub_slice_end == self.last_slice_end) {
1731 sub_slice_end = match first_whitespace {
1733 Some(i) => self.last_slice_end + i,
1734 None => self.slice.len(),
1735 };
1736 }
1737
1738 let kind = match self.last_slice_kind {
1739 CodeCharKind::Comment => CodeCharKind::Normal,
1740 CodeCharKind::Normal => CodeCharKind::Comment,
1741 };
1742 let res = (
1743 kind,
1744 self.last_slice_end,
1745 &self.slice[self.last_slice_end..sub_slice_end],
1746 );
1747 self.last_slice_end = sub_slice_end;
1748 self.last_slice_kind = kind;
1749
1750 Some(res)
1751 }
1752}
1753
1754pub(crate) fn recover_comment_removed(
1756 new: String,
1757 span: Span,
1758 context: &RewriteContext<'_>,
1759) -> String {
1760 let snippet = context.snippet(span);
1761 if snippet != new && changed_comment_content(snippet, &new) {
1762 if context.config.error_on_unformatted() {
1764 context.report.append(
1765 context.psess.span_to_filename(span),
1766 vec![FormattingError::from_span(
1767 span,
1768 context.psess,
1769 ErrorKind::LostComment,
1770 )],
1771 );
1772 }
1773 snippet.to_owned()
1774 } else {
1775 new
1776 }
1777}
1778
1779pub(crate) fn filter_normal_code(code: &str) -> String {
1780 let mut buffer = String::with_capacity(code.len());
1781 LineClasses::new(code).for_each(|(kind, line)| match kind {
1782 FullCodeCharKind::Normal
1783 | FullCodeCharKind::StartString
1784 | FullCodeCharKind::InString
1785 | FullCodeCharKind::EndString => {
1786 buffer.push_str(&line);
1787 buffer.push('\n');
1788 }
1789 _ => (),
1790 });
1791 if !code.ends_with('\n') && buffer.ends_with('\n') {
1792 buffer.pop();
1793 }
1794 buffer
1795}
1796
1797fn changed_comment_content(orig: &str, new: &str) -> bool {
1804 let code_comment_content = |code| {
1806 let slices = UngroupedCommentCodeSlices::new(code);
1807 slices
1808 .filter(|(kind, _, _)| *kind == CodeCharKind::Comment)
1809 .flat_map(|(_, _, s)| CommentReducer::new(s))
1810 };
1811 let res = code_comment_content(orig).ne(code_comment_content(new));
1812 debug!(
1813 "comment::changed_comment_content: {}\norig: '{}'\nnew: '{}'\nraw_old: {}\nraw_new: {}",
1814 res,
1815 orig,
1816 new,
1817 code_comment_content(orig).collect::<String>(),
1818 code_comment_content(new).collect::<String>()
1819 );
1820 res
1821}
1822
1823struct CommentReducer<'a> {
1828 is_block: bool,
1829 at_start_line: bool,
1830 iter: std::str::Chars<'a>,
1831}
1832
1833impl<'a> CommentReducer<'a> {
1834 fn new(comment: &'a str) -> CommentReducer<'a> {
1835 let is_block = comment.starts_with("/*");
1836 let comment = remove_comment_header(comment);
1837 CommentReducer {
1838 is_block,
1839 at_start_line: false,
1841 iter: comment.chars(),
1842 }
1843 }
1844}
1845
1846impl<'a> Iterator for CommentReducer<'a> {
1847 type Item = char;
1848
1849 fn next(&mut self) -> Option<Self::Item> {
1850 loop {
1851 let mut c = self.iter.next()?;
1852 if self.is_block && self.at_start_line {
1853 while c.is_whitespace() {
1854 c = self.iter.next()?;
1855 }
1856 if c == '*' {
1858 c = self.iter.next()?;
1859 }
1860 } else if c == '\n' {
1861 self.at_start_line = true;
1862 }
1863 if !c.is_whitespace() {
1864 return Some(c);
1865 }
1866 }
1867 }
1868}
1869
1870fn remove_comment_header(comment: &str) -> &str {
1871 if comment.starts_with("///") || comment.starts_with("//!") {
1872 &comment[3..]
1873 } else if let Some(stripped) = comment.strip_prefix("//") {
1874 stripped
1875 } else if (comment.starts_with("/**") && !comment.starts_with("/**/"))
1876 || comment.starts_with("/*!")
1877 {
1878 &comment[3..comment.len() - 2]
1879 } else {
1880 assert!(
1881 comment.starts_with("/*"),
1882 "string '{comment}' is not a comment"
1883 );
1884 &comment[2..comment.len() - 2]
1885 }
1886}
1887
1888#[cfg(test)]
1889mod test {
1890 use super::*;
1891
1892 #[test]
1893 fn char_classes() {
1894 let mut iter = CharClasses::new("//\n\n".chars());
1895
1896 assert_eq!((FullCodeCharKind::StartComment, '/'), iter.next().unwrap());
1897 assert_eq!((FullCodeCharKind::InComment, '/'), iter.next().unwrap());
1898 assert_eq!((FullCodeCharKind::EndComment, '\n'), iter.next().unwrap());
1899 assert_eq!((FullCodeCharKind::Normal, '\n'), iter.next().unwrap());
1900 assert_eq!(None, iter.next());
1901 }
1902
1903 #[test]
1904 fn comment_code_slices() {
1905 let input = "code(); /* test */ 1 + 1";
1906 let mut iter = CommentCodeSlices::new(input);
1907
1908 assert_eq!((CodeCharKind::Normal, 0, "code(); "), iter.next().unwrap());
1909 assert_eq!(
1910 (CodeCharKind::Comment, 8, "/* test */"),
1911 iter.next().unwrap()
1912 );
1913 assert_eq!((CodeCharKind::Normal, 18, " 1 + 1"), iter.next().unwrap());
1914 assert_eq!(None, iter.next());
1915 }
1916
1917 #[test]
1918 fn comment_code_slices_two() {
1919 let input = "// comment\n test();";
1920 let mut iter = CommentCodeSlices::new(input);
1921
1922 assert_eq!((CodeCharKind::Normal, 0, ""), iter.next().unwrap());
1923 assert_eq!(
1924 (CodeCharKind::Comment, 0, "// comment\n"),
1925 iter.next().unwrap()
1926 );
1927 assert_eq!(
1928 (CodeCharKind::Normal, 11, " test();"),
1929 iter.next().unwrap()
1930 );
1931 assert_eq!(None, iter.next());
1932 }
1933
1934 #[test]
1935 fn comment_code_slices_three() {
1936 let input = "1 // comment\n // comment2\n\n";
1937 let mut iter = CommentCodeSlices::new(input);
1938
1939 assert_eq!((CodeCharKind::Normal, 0, "1 "), iter.next().unwrap());
1940 assert_eq!(
1941 (CodeCharKind::Comment, 2, "// comment\n // comment2\n"),
1942 iter.next().unwrap()
1943 );
1944 assert_eq!((CodeCharKind::Normal, 29, "\n"), iter.next().unwrap());
1945 assert_eq!(None, iter.next());
1946 }
1947
1948 #[test]
1949 #[rustfmt::skip]
1950 fn format_doc_comments() {
1951 let mut wrap_normalize_config: crate::config::Config = Default::default();
1952 wrap_normalize_config.set().wrap_comments(true);
1953 wrap_normalize_config.set().normalize_comments(true);
1954
1955 let mut wrap_config: crate::config::Config = Default::default();
1956 wrap_config.set().wrap_comments(true);
1957
1958 let comment = rewrite_comment(" //test",
1959 true,
1960 Shape::legacy(100, Indent::new(0, 100)),
1961 &wrap_normalize_config).unwrap();
1962 assert_eq!("/* test */", comment);
1963
1964 let comment = rewrite_comment("// comment on a",
1965 false,
1966 Shape::legacy(10, Indent::empty()),
1967 &wrap_normalize_config).unwrap();
1968 assert_eq!("// comment\n// on a", comment);
1969
1970 let comment = rewrite_comment("// A multi line comment\n // between args.",
1971 false,
1972 Shape::legacy(60, Indent::new(0, 12)),
1973 &wrap_normalize_config).unwrap();
1974 assert_eq!("// A multi line comment\n // between args.", comment);
1975
1976 let input = "// comment";
1977 let expected =
1978 "/* comment */";
1979 let comment = rewrite_comment(input,
1980 true,
1981 Shape::legacy(9, Indent::new(0, 69)),
1982 &wrap_normalize_config).unwrap();
1983 assert_eq!(expected, comment);
1984
1985 let comment = rewrite_comment("/* trimmed */",
1986 true,
1987 Shape::legacy(100, Indent::new(0, 100)),
1988 &wrap_normalize_config).unwrap();
1989 assert_eq!("/* trimmed */", comment);
1990
1991 let comment = rewrite_comment(r#"/// test1
1993 /// test2
1994 /*
1995 * test3
1996 */"#,
1997 false,
1998 Shape::legacy(100, Indent::new(0, 0)),
1999 &wrap_normalize_config).unwrap();
2000 assert_eq!("/// test1\n/// test2\n// test3", comment);
2001
2002 let comment = rewrite_comment(r#"// test1
2004
2005 // test2"#,
2006 false,
2007 Shape::legacy(100, Indent::new(0, 0)),
2008 &wrap_normalize_config).unwrap();
2009 assert_eq!("// test1\n\n// test2", comment);
2010
2011 let comment = rewrite_comment(r#"//@ test1
2013
2014 //@ test2"#,
2015 false,
2016 Shape::legacy(100, Indent::new(0, 0)),
2017 &wrap_normalize_config).unwrap();
2018 assert_eq!("//@ test1\n\n//@ test2", comment);
2019
2020 let comment = rewrite_comment(r#"// test1
2022 /*
2023 a bare line!
2024
2025 another bare line!
2026 */"#,
2027 false,
2028 Shape::legacy(100, Indent::new(0, 0)),
2029 &wrap_config).unwrap();
2030 assert_eq!("// test1\n/*\n a bare line!\n\n another bare line!\n*/", comment);
2031 }
2032
2033 fn uncommented(text: &str) -> String {
2036 CharClasses::new(text.chars())
2037 .filter_map(|(s, c)| match s {
2038 FullCodeCharKind::Normal | FullCodeCharKind::InString => Some(c),
2039 _ => None,
2040 })
2041 .collect()
2042 }
2043
2044 #[test]
2045 fn test_uncommented() {
2046 assert_eq!(&uncommented("abc/*...*/"), "abc");
2047 assert_eq!(
2048 &uncommented("// .... /* \n../* /* *** / */ */a/* // */c\n"),
2049 "..ac\n"
2050 );
2051 assert_eq!(&uncommented("abc \" /* */\" qsdf"), "abc \" /* */\" qsdf");
2052 }
2053
2054 #[test]
2055 fn test_contains_comment() {
2056 assert_eq!(contains_comment("abc"), false);
2057 assert_eq!(contains_comment("abc // qsdf"), true);
2058 assert_eq!(contains_comment("abc /* kqsdf"), true);
2059 assert_eq!(contains_comment("abc \" /* */\" qsdf"), false);
2060 }
2061
2062 #[test]
2063 fn test_find_uncommented() {
2064 fn check(haystack: &str, needle: &str, expected: Option<usize>) {
2065 assert_eq!(expected, haystack.find_uncommented(needle));
2066 }
2067
2068 check("/*/ */test", "test", Some(6));
2069 check("//test\ntest", "test", Some(7));
2070 check("/* comment only */", "whatever", None);
2071 check(
2072 "/* comment */ some text /* more commentary */ result",
2073 "result",
2074 Some(46),
2075 );
2076 check("sup // sup", "p", Some(2));
2077 check("sup", "x", None);
2078 check(r#"π? /**/ π is nice!"#, r#"π is nice"#, Some(9));
2079 check("/*sup yo? \n sup*/ sup", "p", Some(20));
2080 check("hel/*lohello*/lo", "hello", None);
2081 check("acb", "ab", None);
2082 check(",/*A*/ ", ",", Some(0));
2083 check("abc", "abc", Some(0));
2084 check("/* abc */", "abc", None);
2085 check("/**/abc/* */", "abc", Some(4));
2086 check("\"/* abc */\"", "abc", Some(4));
2087 check("\"/* abc", "abc", Some(4));
2088 }
2089
2090 #[test]
2091 fn test_filter_normal_code() {
2092 let s = r#"
2093fn main() {
2094 println!("hello, world");
2095}
2096"#;
2097 assert_eq!(s, filter_normal_code(s));
2098 let s_with_comment = r#"
2099fn main() {
2100 // hello, world
2101 println!("hello, world");
2102}
2103"#;
2104 assert_eq!(s, filter_normal_code(s_with_comment));
2105 }
2106
2107 #[test]
2108 fn test_itemized_block_first_line_handling() {
2109 fn run_test(
2110 test_input: &str,
2111 expected_line: &str,
2112 expected_indent: usize,
2113 expected_opener: &str,
2114 expected_line_start: &str,
2115 ) {
2116 let block = ItemizedBlock::new(test_input).unwrap();
2117 assert_eq!(1, block.lines.len(), "test_input: {test_input:?}");
2118 assert_eq!(expected_line, &block.lines[0], "test_input: {test_input:?}");
2119 assert_eq!(expected_indent, block.indent, "test_input: {test_input:?}");
2120 assert_eq!(expected_opener, &block.opener, "test_input: {test_input:?}");
2121 assert_eq!(
2122 expected_line_start, &block.line_start,
2123 "test_input: {test_input:?}"
2124 );
2125 }
2126
2127 run_test("- foo", "foo", 2, "- ", " ");
2128 run_test("* foo", "foo", 2, "* ", " ");
2129 run_test("> foo", "foo", 2, "> ", "> ");
2130
2131 run_test("1. foo", "foo", 3, "1. ", " ");
2132 run_test("12. foo", "foo", 4, "12. ", " ");
2133 run_test("1) foo", "foo", 3, "1) ", " ");
2134 run_test("12) foo", "foo", 4, "12) ", " ");
2135
2136 run_test(" - foo", "foo", 6, " - ", " ");
2137
2138 run_test("0. foo", "foo", 3, "0. ", " ");
2140 run_test("01. foo", "foo", 4, "01. ", " ");
2141 }
2142
2143 #[test]
2144 fn test_itemized_block_nonobvious_markers_are_rejected() {
2145 let test_inputs = vec![
2146 "word. rest of the paragraph.",
2151 "a. maybe this is a list item? maybe not?",
2152 "iv. maybe this is a list item? maybe not?",
2153 "123. only 2-digit numbers are recognized as item markers.",
2161 "123) giving some coverage to parens as well.",
2163 "a) giving some coverage to parens as well.",
2164 "1.Not a list item.",
2167 "1.2.3. Not a list item.",
2168 "1)Not a list item.",
2169 "-Not a list item.",
2170 "+Not a list item.",
2171 "+1 not a list item.",
2172 "-1. Not a list item.",
2174 "-1 Not a list item.",
2175 ". Not a list item.",
2177 ") Not a list item.",
2178 ];
2179 for line in test_inputs.iter() {
2180 let maybe_block = ItemizedBlock::new(line);
2181 assert!(
2182 maybe_block.is_none(),
2183 "The following line shouldn't be classified as a list item: {line}"
2184 );
2185 }
2186 }
2187}