From e332173dfbf6cab259e3ddf3ffad4a2e19137233 Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 02:39:48 -0400 Subject: [PATCH 01/13] feat!: preserve comment in code block when comments are normalized --- src/comment.rs | 65 ++++++++++++++++++- ...ze_preserve_doc_code_comments_with_star.rs | 20 ++++++ ...preserve_doc_code_comments_without_star.rs | 23 +++++++ ...ze_preserve_doc_code_comments_with_star.rs | 14 ++++ ...preserve_doc_code_comments_without_star.rs | 17 +++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs create mode 100644 tests/source/issue-6631/normalize_preserve_doc_code_comments_without_star.rs create mode 100644 tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs create mode 100644 tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs diff --git a/src/comment.rs b/src/comment.rs index 241934a7d3d..522dac0ef40 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -908,6 +908,7 @@ fn rewrite_comment_inner( let mut rewriter = CommentRewrite::new(orig, block_style, shape, config); let line_breaks = count_newlines(orig.trim_end()); + let mut is_in_code_block = false; let lines = orig .lines() .enumerate() @@ -918,9 +919,16 @@ fn rewrite_comment_inner( line = line[..(line.len() - 2)].trim_end(); } - line + let code_block_matches = line.matches("```").count(); + if code_block_matches != 0 && code_block_matches % 2 == 1 { + is_in_code_block = !is_in_code_block; + left_trim_comment_line(line, &style) + } else if is_in_code_block { + left_trim_doc_comment_code_line(line, &style) + } else { + left_trim_comment_line(line, &style) + } }) - .map(|s| left_trim_comment_line(s, &style)) .map(|(line, has_leading_whitespace)| { if orig.starts_with("/*") && line_breaks == 0 { ( @@ -1118,6 +1126,59 @@ fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a s } } +/// Trims a single comment character and possibly a single space from the left of a string. +/// Does not trim all whitespace. If at least one space is trimmed from the left of the string, +/// this function returns true. +fn left_trim_doc_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { + enum TrimLeftDocCode<'a> { + Trimmed(&'a str), + Unmodified(&'a str), + } + fn trim_left_doc_code<'a>(line: &'a str, pat: &'_ str) -> TrimLeftDocCode<'a> { + if let Some(new_line_segment) = line.strip_prefix(pat) { + TrimLeftDocCode::Trimmed(new_line_segment) + } else { + TrimLeftDocCode::Unmodified(line) + } + } + let opener = style.opener(); + match style { + CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => { + match trim_left_doc_code(line, opener) { + TrimLeftDocCode::Trimmed(line) => (line, true), + TrimLeftDocCode::Unmodified(line) => { + match trim_left_doc_code(line, opener.trim_end()) { + TrimLeftDocCode::Trimmed(line) | TrimLeftDocCode::Unmodified(line) => { + (line, false) + } + } + } + } + } + CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => { + match trim_left_doc_code(line, opener) { + TrimLeftDocCode::Trimmed(line) => (line, true), + TrimLeftDocCode::Unmodified(line) => { + match trim_left_doc_code(line, style.line_start().trim_start()) { + TrimLeftDocCode::Trimmed(line) => (line, true), + TrimLeftDocCode::Unmodified(line) => (line, false), + } + } + } + } + CommentStyle::Custom(_) => match trim_left_doc_code(line, opener) { + TrimLeftDocCode::Trimmed(line) => (line, opener.ends_with(' ')), + TrimLeftDocCode::Unmodified(line) => { + match trim_left_doc_code(line, opener.trim_end()) { + TrimLeftDocCode::Trimmed(line) | TrimLeftDocCode::Unmodified(line) => { + (line, false) + } + } + } + }, + } +} + pub(crate) trait FindUncommented { fn find_uncommented(&self, pat: &str) -> Option; fn find_last_uncommented(&self, pat: &str) -> Option; diff --git a/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs b/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs new file mode 100644 index 00000000000..e23db69f178 --- /dev/null +++ b/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs @@ -0,0 +1,20 @@ +// rustfmt-normalize_comments: true + +/*! + * ``` + * // foo + * ``` + */ + +/** + * ``` + * // bar + * ``` + */ +struct Bar; + +/* + * ``` + * // baz + * ``` + */ diff --git a/tests/source/issue-6631/normalize_preserve_doc_code_comments_without_star.rs b/tests/source/issue-6631/normalize_preserve_doc_code_comments_without_star.rs new file mode 100644 index 00000000000..c7b2b37b7fe --- /dev/null +++ b/tests/source/issue-6631/normalize_preserve_doc_code_comments_without_star.rs @@ -0,0 +1,23 @@ +// rustfmt-normalize_comments: true + +/*! +``` +// foo +/// BAR +``` +*/ + +/** +// MEOW +``` +// bar +``` +*/ +struct Bar; + +/* +``` +// baz +/// FOO +``` +*/ diff --git a/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs b/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs new file mode 100644 index 00000000000..cabfc401be3 --- /dev/null +++ b/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs @@ -0,0 +1,14 @@ +// rustfmt-normalize_comments: true + +//! ``` +//! // foo +//! ``` + +/// ``` +/// // bar +/// ``` +struct Bar; + +// ``` +// // baz +// ``` diff --git a/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs b/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs new file mode 100644 index 00000000000..4da7da2bcca --- /dev/null +++ b/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs @@ -0,0 +1,17 @@ +// rustfmt-normalize_comments: true + +//! ``` +//! // foo +//! /// BAR +//! ``` + +/// MEOW +/// ``` +/// // bar +/// ``` +struct Bar; + +// ``` +// // baz +// /// FOO +// ``` From f456412142cee459db054a4bfaaa0135e92b0e48 Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 02:48:41 -0400 Subject: [PATCH 02/13] chore: rename function left_trim_doc_comment_code_line to left_trim_comment_code_line --- src/comment.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 522dac0ef40..443007fa454 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -924,7 +924,7 @@ fn rewrite_comment_inner( is_in_code_block = !is_in_code_block; left_trim_comment_line(line, &style) } else if is_in_code_block { - left_trim_doc_comment_code_line(line, &style) + left_trim_comment_code_line(line, &style) } else { left_trim_comment_line(line, &style) } @@ -1129,7 +1129,7 @@ fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a s /// Trims a single comment character and possibly a single space from the left of a string. /// Does not trim all whitespace. If at least one space is trimmed from the left of the string, /// this function returns true. -fn left_trim_doc_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { +fn left_trim_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { enum TrimLeftDocCode<'a> { Trimmed(&'a str), Unmodified(&'a str), From 6b6755bd7927083ba43b5145b1c5a2edf36f767a Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 03:00:25 -0400 Subject: [PATCH 03/13] chore: more descriptive function comment --- src/comment.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 443007fa454..0bcc8fc1857 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -1126,9 +1126,9 @@ fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a s } } -/// Trims a single comment character and possibly a single space from the left of a string. -/// Does not trim all whitespace. If at least one space is trimmed from the left of the string, -/// this function returns true. +/// Trims the beginning of a comment's opener or line start, leaving the rest untouched. +/// If at least one whitespace is trimmed, the second element of the tuple is true. +/// Will only ever trim one whitespace unless a custom comment style is used. fn left_trim_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { enum TrimLeftDocCode<'a> { Trimmed(&'a str), From 0d45b47c10dde304d7803521f984ced25754e4a1 Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 18:39:10 -0400 Subject: [PATCH 04/13] feat: CodeBlockTracker struct --- src/comment.rs | 22 ++++---- src/utils.rs | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 10 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 0bcc8fc1857..3a88e5ce3a3 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -11,7 +11,7 @@ use crate::rewrite::{RewriteContext, RewriteErrorExt, RewriteResult}; use crate::shape::{Indent, Shape}; use crate::string::{StringFormat, rewrite_string}; use crate::utils::{ - count_newlines, first_line_width, last_line_width, trim_left_preserve_layout, + CodeBlockTracker, count_newlines, first_line_width, last_line_width, trim_left_preserve_layout, trimmed_last_line_width, unicode_str_width, }; use crate::{ErrorKind, FormattingError}; @@ -908,7 +908,7 @@ fn rewrite_comment_inner( let mut rewriter = CommentRewrite::new(orig, block_style, shape, config); let line_breaks = count_newlines(orig.trim_end()); - let mut is_in_code_block = false; + let mut code_blocker_tracker = CodeBlockTracker::default(); let lines = orig .lines() .enumerate() @@ -919,14 +919,16 @@ fn rewrite_comment_inner( line = line[..(line.len() - 2)].trim_end(); } - let code_block_matches = line.matches("```").count(); - if code_block_matches != 0 && code_block_matches % 2 == 1 { - is_in_code_block = !is_in_code_block; - left_trim_comment_line(line, &style) - } else if is_in_code_block { - left_trim_comment_code_line(line, &style) - } else { - left_trim_comment_line(line, &style) + line + }) + .map(move |line| { + code_blocker_tracker = code_blocker_tracker.next_line(line); + match code_blocker_tracker { + CodeBlockTracker::Outside + | CodeBlockTracker::Opener + | CodeBlockTracker::Closer + | CodeBlockTracker::SingleLineCodeBlock => left_trim_comment_line(line, &style), + CodeBlockTracker::Inside => left_trim_comment_code_line(line, &style), } }) .map(|(line, has_leading_whitespace)| { diff --git a/src/utils.rs b/src/utils.rs index b676803379f..d4571ceae7a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -711,6 +711,94 @@ pub(crate) fn unicode_str_width(s: &str) -> usize { s.width() } +/// Checks whether we are in a code block, +/// and if we are, where inside the code block. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CodeBlockTracker { + /// Not inside a code block. + #[default] + Outside, + /// Code block opener and closer are on the same line. + /// Ex. ``````// ```type SomeType = usize;``` `````` + SingleLineCodeBlock, + /// Line opener to a code block. + Opener, + /// Inside a code block (excluding opener and closer). + Inside, + /// Line closer to a code block. + Closer, +} + +impl CodeBlockTracker { + /// Reads the next line of a comment and + /// updates the code block tracker accordingly. + pub(crate) fn next_line(self, line: &str) -> Self { + let code_block_matches = line.matches("```").count(); + // Check if a code block is opened or closed, + // and if opened, not closed on the same line, or vice versa. + if code_block_matches != 0 { + if code_block_matches % 2 == 1 { + match self { + CodeBlockTracker::Outside + | CodeBlockTracker::Closer + | CodeBlockTracker::SingleLineCodeBlock => { + // If we were outside a code block or a code block was previously closed, + // and now we detect another code block opener/closer, then + // this is an opener to a code block. + CodeBlockTracker::Opener + } + CodeBlockTracker::Inside | CodeBlockTracker::Opener => { + // If we were inside a code block or a code block was previously opened, + // and now we detect another code block opener/closer, then + // this is a closer to a code block. + CodeBlockTracker::Closer + } + } + } else { + // Detected a code block opener and closer. + match self { + CodeBlockTracker::Outside + | CodeBlockTracker::Inside + | CodeBlockTracker::SingleLineCodeBlock => { + // If previously detected outside, inside, or a single line code block, + // and now we detect an opener and closer, + // we are in a single line code block. + CodeBlockTracker::SingleLineCodeBlock + } + CodeBlockTracker::Opener => { + // If previously detected a code block opener, + // and now we detect an opener and closer, + // then the last code block opener/closer is an opener. + CodeBlockTracker::Opener + } + CodeBlockTracker::Closer => { + // If previously detected a code block closer, + // and now we detect an opener and closer, + // then the last code block opener/closer is a closer. + CodeBlockTracker::Closer + } + } + } + } else { + // No code block opener/closer detected. + match self { + CodeBlockTracker::Opener => { + // If previously a code block opener was detected, + // now we are inside the code block. + CodeBlockTracker::Inside + } + CodeBlockTracker::Closer | CodeBlockTracker::SingleLineCodeBlock => { + // If previously a code block closer was detected, + // now we are outside the code block. + CodeBlockTracker::Outside + } + CodeBlockTracker::Outside => CodeBlockTracker::Outside, + CodeBlockTracker::Inside => CodeBlockTracker::Inside, + } + } + } +} + #[cfg(test)] mod test { use super::*; @@ -731,4 +819,52 @@ mod test { Some("aaa\n bbb\n ccc".to_string()) ); } + + #[test] + fn test_code_block_tracker_default() { + let code_blocker_tracker = CodeBlockTracker::default(); + assert_eq!(code_blocker_tracker, CodeBlockTracker::Outside); + } + + #[test] + fn test_code_block_tracker() { + let mut code_block_tracker = CodeBlockTracker::default(); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment before a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// type SomeType = usize;"); + assert_eq!(code_block_tracker, CodeBlockTracker::Inside); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment after a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + } + + #[test] + fn test_code_block_tracker_single_line_code_block() { + let mut code_block_tracker = CodeBlockTracker::default(); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment before a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + + code_block_tracker = code_block_tracker.next_line("/// ```type SomeType = usize;```"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = + code_block_tracker.next_line("/// Ex. ``````// ```type SomeType = usize;``` ``````"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment after a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + } } From e96a1d3d19c68f708f2437b0758f3e17676a81f6 Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 23:15:11 -0400 Subject: [PATCH 05/13] fix!: single block code after code block closer fix --- src/utils.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index d4571ceae7a..27256f61c29 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -732,6 +732,14 @@ pub(crate) enum CodeBlockTracker { impl CodeBlockTracker { /// Reads the next line of a comment and /// updates the code block tracker accordingly. + /// + /// This function only cares about the last state of the line, + /// for example: + /// ```text + /// "```let i = 1;``` ```let i = 2;``` ```" + /// ``` + /// The above line will be considered an opener because + /// the last code blocker opener/closer is an opener to a new code block. pub(crate) fn next_line(self, line: &str) -> Self { let code_block_matches = line.matches("```").count(); // Check if a code block is opened or closed, @@ -774,8 +782,8 @@ impl CodeBlockTracker { CodeBlockTracker::Closer => { // If previously detected a code block closer, // and now we detect an opener and closer, - // then the last code block opener/closer is a closer. - CodeBlockTracker::Closer + // then this is a single line code block. + CodeBlockTracker::SingleLineCodeBlock } } } @@ -867,4 +875,51 @@ mod test { code_block_tracker.next_line("/// This is a comment after a code block!"); assert_eq!(code_block_tracker, CodeBlockTracker::Outside); } + + #[test] + fn test_code_block_tracker_multiple_code_blocks() { + let mut code_block_tracker = CodeBlockTracker::default(); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment before a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + + code_block_tracker = code_block_tracker.next_line("/// ```type SomeType = usize;```"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ``` In between code blocks! ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// type Meow = f32;"); + assert_eq!(code_block_tracker, CodeBlockTracker::Inside); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = code_block_tracker.next_line("/// ```type CatsOuttaTheBag = f64;```"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = code_block_tracker + .next_line("/// ```type CatsOuttaTheHome = bool;``` ```type DogsInTheHouse = i64"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ``` ```let me = \"YOU\";``` ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment after a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + } } From 0f4cda602182fb4647b8046e601a05f2db6f611b Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 23:18:19 -0400 Subject: [PATCH 06/13] chore: updated CodeBlockTracker::next_line doc comment --- src/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 27256f61c29..dc482ba5236 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -738,8 +738,8 @@ impl CodeBlockTracker { /// ```text /// "```let i = 1;``` ```let i = 2;``` ```" /// ``` - /// The above line will be considered an opener because - /// the last code blocker opener/closer is an opener to a new code block. + /// The above line will be considered an opener and not single line code block + /// because the last code block opener/closer is an opener to a new code block. pub(crate) fn next_line(self, line: &str) -> Self { let code_block_matches = line.matches("```").count(); // Check if a code block is opened or closed, From e72774cb4a6e6e2a4888f65c0fa423685312a734 Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 23:23:50 -0400 Subject: [PATCH 07/13] chore: update CodeBlockTracker::SingleLineCodeBlock doc comment --- src/utils.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index dc482ba5236..84987ace2f7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -719,7 +719,11 @@ pub(crate) enum CodeBlockTracker { #[default] Outside, /// Code block opener and closer are on the same line. - /// Ex. ``````// ```type SomeType = usize;``` `````` + /// + /// Ex. + /// ``` + /// // ```type SomeType = usize;``` + /// ``` SingleLineCodeBlock, /// Line opener to a code block. Opener, From 820ed08c2081797a6f93f96c57db6b21ad2f06b3 Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 23:39:48 -0400 Subject: [PATCH 08/13] chore: change CodeBlockTracker::SingleLineCodeBlock code block into text block --- src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 84987ace2f7..34ae14ad288 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -721,7 +721,7 @@ pub(crate) enum CodeBlockTracker { /// Code block opener and closer are on the same line. /// /// Ex. - /// ``` + /// ```text /// // ```type SomeType = usize;``` /// ``` SingleLineCodeBlock, From 17ccc83cc10f0dbff0825c48164fb5bd5f34f0b8 Mon Sep 17 00:00:00 2001 From: Albar Date: Sat, 23 May 2026 23:59:43 -0400 Subject: [PATCH 09/13] chore: rename TrimLeftDocCode to TrimLeftCodeLine --- src/comment.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 3a88e5ce3a3..2c37acf9a78 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -1132,25 +1132,25 @@ fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a s /// If at least one whitespace is trimmed, the second element of the tuple is true. /// Will only ever trim one whitespace unless a custom comment style is used. fn left_trim_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { - enum TrimLeftDocCode<'a> { + enum TrimLeftCodeLine<'a> { Trimmed(&'a str), Unmodified(&'a str), } - fn trim_left_doc_code<'a>(line: &'a str, pat: &'_ str) -> TrimLeftDocCode<'a> { + fn trim_left_doc_code<'a>(line: &'a str, pat: &'_ str) -> TrimLeftCodeLine<'a> { if let Some(new_line_segment) = line.strip_prefix(pat) { - TrimLeftDocCode::Trimmed(new_line_segment) + TrimLeftCodeLine::Trimmed(new_line_segment) } else { - TrimLeftDocCode::Unmodified(line) + TrimLeftCodeLine::Unmodified(line) } } let opener = style.opener(); match style { CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => { match trim_left_doc_code(line, opener) { - TrimLeftDocCode::Trimmed(line) => (line, true), - TrimLeftDocCode::Unmodified(line) => { + TrimLeftCodeLine::Trimmed(line) => (line, true), + TrimLeftCodeLine::Unmodified(line) => { match trim_left_doc_code(line, opener.trim_end()) { - TrimLeftDocCode::Trimmed(line) | TrimLeftDocCode::Unmodified(line) => { + TrimLeftCodeLine::Trimmed(line) | TrimLeftCodeLine::Unmodified(line) => { (line, false) } } @@ -1159,20 +1159,20 @@ fn left_trim_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> ( } CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => { match trim_left_doc_code(line, opener) { - TrimLeftDocCode::Trimmed(line) => (line, true), - TrimLeftDocCode::Unmodified(line) => { + TrimLeftCodeLine::Trimmed(line) => (line, true), + TrimLeftCodeLine::Unmodified(line) => { match trim_left_doc_code(line, style.line_start().trim_start()) { - TrimLeftDocCode::Trimmed(line) => (line, true), - TrimLeftDocCode::Unmodified(line) => (line, false), + TrimLeftCodeLine::Trimmed(line) => (line, true), + TrimLeftCodeLine::Unmodified(line) => (line, false), } } } } CommentStyle::Custom(_) => match trim_left_doc_code(line, opener) { - TrimLeftDocCode::Trimmed(line) => (line, opener.ends_with(' ')), - TrimLeftDocCode::Unmodified(line) => { + TrimLeftCodeLine::Trimmed(line) => (line, opener.ends_with(' ')), + TrimLeftCodeLine::Unmodified(line) => { match trim_left_doc_code(line, opener.trim_end()) { - TrimLeftDocCode::Trimmed(line) | TrimLeftDocCode::Unmodified(line) => { + TrimLeftCodeLine::Trimmed(line) | TrimLeftCodeLine::Unmodified(line) => { (line, false) } } From 0061b9df2e5e543ac3868047cf497a8fe22a4a88 Mon Sep 17 00:00:00 2001 From: Albar Date: Tue, 26 May 2026 13:36:07 -0400 Subject: [PATCH 10/13] feat!: rewrite left_trim_comment_line and remove CodeBlockTracker --- src/comment.rs | 115 +++-------- src/utils.rs | 195 ------------------ ...preserve_doc_code_comments_without_star.rs | 2 +- 3 files changed, 28 insertions(+), 284 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 2c37acf9a78..506109e02d7 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -11,7 +11,7 @@ use crate::rewrite::{RewriteContext, RewriteErrorExt, RewriteResult}; use crate::shape::{Indent, Shape}; use crate::string::{StringFormat, rewrite_string}; use crate::utils::{ - CodeBlockTracker, count_newlines, first_line_width, last_line_width, trim_left_preserve_layout, + count_newlines, first_line_width, last_line_width, trim_left_preserve_layout, trimmed_last_line_width, unicode_str_width, }; use crate::{ErrorKind, FormattingError}; @@ -908,7 +908,6 @@ fn rewrite_comment_inner( let mut rewriter = CommentRewrite::new(orig, block_style, shape, config); let line_breaks = count_newlines(orig.trim_end()); - let mut code_blocker_tracker = CodeBlockTracker::default(); let lines = orig .lines() .enumerate() @@ -921,16 +920,7 @@ fn rewrite_comment_inner( line }) - .map(move |line| { - code_blocker_tracker = code_blocker_tracker.next_line(line); - match code_blocker_tracker { - CodeBlockTracker::Outside - | CodeBlockTracker::Opener - | CodeBlockTracker::Closer - | CodeBlockTracker::SingleLineCodeBlock => left_trim_comment_line(line, &style), - CodeBlockTracker::Inside => left_trim_comment_code_line(line, &style), - } - }) + .map(move |line| left_trim_comment_line(line, &style)) .map(|(line, has_leading_whitespace)| { if orig.starts_with("/*") && line_breaks == 0 { ( @@ -1090,94 +1080,43 @@ fn light_rewrite_comment( .join(&format!("\n{}", offset.to_string(config))) } -/// Trims comment characters and possibly a single space from the left of a string. -/// Does not trim all whitespace. If a single space is trimmed from the left of the string, -/// this function returns true. -fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { - if line.starts_with("//! ") - || line.starts_with("/// ") - || line.starts_with("/*! ") - || line.starts_with("/** ") - { - (&line[4..], true) - } else if let CommentStyle::Custom(opener) = *style { - if let Some(stripped) = line.strip_prefix(opener) { - (stripped, true) - } else { - (&line[opener.trim_end().len()..], false) - } - } else if line.starts_with("/* ") - || line.starts_with("// ") - || line.starts_with("//!") - || line.starts_with("///") - || line.starts_with("** ") - || line.starts_with("/*!") - || (line.starts_with("/**") && !line.starts_with("/**/")) - { - (&line[3..], line.chars().nth(2).unwrap() == ' ') - } else if line.starts_with("/*") - || line.starts_with("* ") - || line.starts_with("//") - || line.starts_with("**") - { - (&line[2..], line.chars().nth(1).unwrap() == ' ') - } else if let Some(stripped) = line.strip_prefix('*') { - (stripped, false) - } else { - (line, line.starts_with(' ')) - } -} - /// Trims the beginning of a comment's opener or line start, leaving the rest untouched. /// If at least one whitespace is trimmed, the second element of the tuple is true. /// Will only ever trim one whitespace unless a custom comment style is used. -fn left_trim_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { - enum TrimLeftCodeLine<'a> { - Trimmed(&'a str), - Unmodified(&'a str), - } - fn trim_left_doc_code<'a>(line: &'a str, pat: &'_ str) -> TrimLeftCodeLine<'a> { - if let Some(new_line_segment) = line.strip_prefix(pat) { - TrimLeftCodeLine::Trimmed(new_line_segment) - } else { - TrimLeftCodeLine::Unmodified(line) - } - } +fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { let opener = style.opener(); match style { CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => { - match trim_left_doc_code(line, opener) { - TrimLeftCodeLine::Trimmed(line) => (line, true), - TrimLeftCodeLine::Unmodified(line) => { - match trim_left_doc_code(line, opener.trim_end()) { - TrimLeftCodeLine::Trimmed(line) | TrimLeftCodeLine::Unmodified(line) => { - (line, false) - } - } - } + if let Some(line) = line.strip_prefix(opener) { + (line, true) + } else if let Some(line) = line.strip_prefix(opener.trim_end()) { + (line, false) + } else { + (line, false) } } CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => { - match trim_left_doc_code(line, opener) { - TrimLeftCodeLine::Trimmed(line) => (line, true), - TrimLeftCodeLine::Unmodified(line) => { - match trim_left_doc_code(line, style.line_start().trim_start()) { - TrimLeftCodeLine::Trimmed(line) => (line, true), - TrimLeftCodeLine::Unmodified(line) => (line, false), - } - } + if let Some(line) = line.strip_prefix(opener) { + (line, true) + } else if let Some(line) = line.strip_prefix(opener.trim_end()) { + (line, false) + } else if let Some(line) = line.strip_prefix(style.line_start()) { + (line, true) + } else if let Some(line) = line.strip_prefix(style.line_start().trim_start()) { + (line, true) + } else { + (line, false) } } - CommentStyle::Custom(_) => match trim_left_doc_code(line, opener) { - TrimLeftCodeLine::Trimmed(line) => (line, opener.ends_with(' ')), - TrimLeftCodeLine::Unmodified(line) => { - match trim_left_doc_code(line, opener.trim_end()) { - TrimLeftCodeLine::Trimmed(line) | TrimLeftCodeLine::Unmodified(line) => { - (line, false) - } - } + CommentStyle::Custom(_) => { + if let Some(line) = line.strip_prefix(opener) { + (line, line.ends_with(' ')) + } else if let Some(line) = line.strip_prefix(opener.trim_end()) { + (line, false) + } else { + (line, false) } - }, + } } } diff --git a/src/utils.rs b/src/utils.rs index 34ae14ad288..b676803379f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -711,106 +711,6 @@ pub(crate) fn unicode_str_width(s: &str) -> usize { s.width() } -/// Checks whether we are in a code block, -/// and if we are, where inside the code block. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub(crate) enum CodeBlockTracker { - /// Not inside a code block. - #[default] - Outside, - /// Code block opener and closer are on the same line. - /// - /// Ex. - /// ```text - /// // ```type SomeType = usize;``` - /// ``` - SingleLineCodeBlock, - /// Line opener to a code block. - Opener, - /// Inside a code block (excluding opener and closer). - Inside, - /// Line closer to a code block. - Closer, -} - -impl CodeBlockTracker { - /// Reads the next line of a comment and - /// updates the code block tracker accordingly. - /// - /// This function only cares about the last state of the line, - /// for example: - /// ```text - /// "```let i = 1;``` ```let i = 2;``` ```" - /// ``` - /// The above line will be considered an opener and not single line code block - /// because the last code block opener/closer is an opener to a new code block. - pub(crate) fn next_line(self, line: &str) -> Self { - let code_block_matches = line.matches("```").count(); - // Check if a code block is opened or closed, - // and if opened, not closed on the same line, or vice versa. - if code_block_matches != 0 { - if code_block_matches % 2 == 1 { - match self { - CodeBlockTracker::Outside - | CodeBlockTracker::Closer - | CodeBlockTracker::SingleLineCodeBlock => { - // If we were outside a code block or a code block was previously closed, - // and now we detect another code block opener/closer, then - // this is an opener to a code block. - CodeBlockTracker::Opener - } - CodeBlockTracker::Inside | CodeBlockTracker::Opener => { - // If we were inside a code block or a code block was previously opened, - // and now we detect another code block opener/closer, then - // this is a closer to a code block. - CodeBlockTracker::Closer - } - } - } else { - // Detected a code block opener and closer. - match self { - CodeBlockTracker::Outside - | CodeBlockTracker::Inside - | CodeBlockTracker::SingleLineCodeBlock => { - // If previously detected outside, inside, or a single line code block, - // and now we detect an opener and closer, - // we are in a single line code block. - CodeBlockTracker::SingleLineCodeBlock - } - CodeBlockTracker::Opener => { - // If previously detected a code block opener, - // and now we detect an opener and closer, - // then the last code block opener/closer is an opener. - CodeBlockTracker::Opener - } - CodeBlockTracker::Closer => { - // If previously detected a code block closer, - // and now we detect an opener and closer, - // then this is a single line code block. - CodeBlockTracker::SingleLineCodeBlock - } - } - } - } else { - // No code block opener/closer detected. - match self { - CodeBlockTracker::Opener => { - // If previously a code block opener was detected, - // now we are inside the code block. - CodeBlockTracker::Inside - } - CodeBlockTracker::Closer | CodeBlockTracker::SingleLineCodeBlock => { - // If previously a code block closer was detected, - // now we are outside the code block. - CodeBlockTracker::Outside - } - CodeBlockTracker::Outside => CodeBlockTracker::Outside, - CodeBlockTracker::Inside => CodeBlockTracker::Inside, - } - } - } -} - #[cfg(test)] mod test { use super::*; @@ -831,99 +731,4 @@ mod test { Some("aaa\n bbb\n ccc".to_string()) ); } - - #[test] - fn test_code_block_tracker_default() { - let code_blocker_tracker = CodeBlockTracker::default(); - assert_eq!(code_blocker_tracker, CodeBlockTracker::Outside); - } - - #[test] - fn test_code_block_tracker() { - let mut code_block_tracker = CodeBlockTracker::default(); - - code_block_tracker = - code_block_tracker.next_line("/// This is a comment before a code block!"); - assert_eq!(code_block_tracker, CodeBlockTracker::Outside); - - code_block_tracker = code_block_tracker.next_line("/// ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Opener); - - code_block_tracker = code_block_tracker.next_line("/// type SomeType = usize;"); - assert_eq!(code_block_tracker, CodeBlockTracker::Inside); - - code_block_tracker = code_block_tracker.next_line("/// ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Closer); - - code_block_tracker = - code_block_tracker.next_line("/// This is a comment after a code block!"); - assert_eq!(code_block_tracker, CodeBlockTracker::Outside); - } - - #[test] - fn test_code_block_tracker_single_line_code_block() { - let mut code_block_tracker = CodeBlockTracker::default(); - - code_block_tracker = - code_block_tracker.next_line("/// This is a comment before a code block!"); - assert_eq!(code_block_tracker, CodeBlockTracker::Outside); - - code_block_tracker = code_block_tracker.next_line("/// ```type SomeType = usize;```"); - assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); - - code_block_tracker = - code_block_tracker.next_line("/// Ex. ``````// ```type SomeType = usize;``` ``````"); - assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); - - code_block_tracker = - code_block_tracker.next_line("/// This is a comment after a code block!"); - assert_eq!(code_block_tracker, CodeBlockTracker::Outside); - } - - #[test] - fn test_code_block_tracker_multiple_code_blocks() { - let mut code_block_tracker = CodeBlockTracker::default(); - - code_block_tracker = - code_block_tracker.next_line("/// This is a comment before a code block!"); - assert_eq!(code_block_tracker, CodeBlockTracker::Outside); - - code_block_tracker = code_block_tracker.next_line("/// ```type SomeType = usize;```"); - assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); - - code_block_tracker = code_block_tracker.next_line("/// ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Opener); - - code_block_tracker = code_block_tracker.next_line("/// ``` In between code blocks! ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Opener); - - code_block_tracker = code_block_tracker.next_line("/// type Meow = f32;"); - assert_eq!(code_block_tracker, CodeBlockTracker::Inside); - - code_block_tracker = code_block_tracker.next_line("/// ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Closer); - - code_block_tracker = code_block_tracker.next_line("/// ```type CatsOuttaTheBag = f64;```"); - assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); - - code_block_tracker = code_block_tracker.next_line("/// ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Opener); - - code_block_tracker = code_block_tracker.next_line("/// ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Closer); - - code_block_tracker = code_block_tracker - .next_line("/// ```type CatsOuttaTheHome = bool;``` ```type DogsInTheHouse = i64"); - assert_eq!(code_block_tracker, CodeBlockTracker::Opener); - - code_block_tracker = code_block_tracker.next_line("/// ``` ```let me = \"YOU\";``` ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Opener); - - code_block_tracker = code_block_tracker.next_line("/// ```"); - assert_eq!(code_block_tracker, CodeBlockTracker::Closer); - - code_block_tracker = - code_block_tracker.next_line("/// This is a comment after a code block!"); - assert_eq!(code_block_tracker, CodeBlockTracker::Outside); - } } diff --git a/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs b/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs index 4da7da2bcca..fc458847d25 100644 --- a/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs +++ b/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs @@ -5,7 +5,7 @@ //! /// BAR //! ``` -/// MEOW +/// // MEOW /// ``` /// // bar /// ``` From 8357e906450d9b68d8e3d7f762386514f783304d Mon Sep 17 00:00:00 2001 From: Albar Date: Tue, 26 May 2026 13:36:45 -0400 Subject: [PATCH 11/13] chore: remove unnecessary move --- src/comment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/comment.rs b/src/comment.rs index 506109e02d7..57882942b83 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -920,7 +920,7 @@ fn rewrite_comment_inner( line }) - .map(move |line| left_trim_comment_line(line, &style)) + .map(|line| left_trim_comment_line(line, &style)) .map(|(line, has_leading_whitespace)| { if orig.starts_with("/*") && line_breaks == 0 { ( From 7fc690bd76da598a2e417a5054f21af2e430e226 Mon Sep 17 00:00:00 2001 From: Albar Date: Tue, 26 May 2026 13:46:18 -0400 Subject: [PATCH 12/13] fix!: check opener for whitespace instead of line --- src/comment.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 57882942b83..5d97abe519a 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -920,7 +920,7 @@ fn rewrite_comment_inner( line }) - .map(|line| left_trim_comment_line(line, &style)) + .map(|s| left_trim_comment_line(s, &style)) .map(|(line, has_leading_whitespace)| { if orig.starts_with("/*") && line_breaks == 0 { ( @@ -1110,7 +1110,7 @@ fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a s } CommentStyle::Custom(_) => { if let Some(line) = line.strip_prefix(opener) { - (line, line.ends_with(' ')) + (line, opener.ends_with(' ')) } else if let Some(line) = line.strip_prefix(opener.trim_end()) { (line, false) } else { From af1edf4ad0fe09541e0996972c3e12f0130de4da Mon Sep 17 00:00:00 2001 From: Albar Date: Wed, 27 May 2026 13:40:12 -0400 Subject: [PATCH 13/13] fix!: check for missing prefix strip --- src/comment.rs | 4 ++++ .../normalize_preserve_doc_code_comments_with_star.rs | 5 +++++ .../normalize_preserve_doc_code_comments_with_star.rs | 2 ++ 3 files changed, 11 insertions(+) diff --git a/src/comment.rs b/src/comment.rs index 5d97abe519a..b4d34a12f17 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -1083,6 +1083,8 @@ fn light_rewrite_comment( /// Trims the beginning of a comment's opener or line start, leaving the rest untouched. /// If at least one whitespace is trimmed, the second element of the tuple is true. /// Will only ever trim one whitespace unless a custom comment style is used. +/// +/// **NOTE**: this function assumes the beginning of the line is trimmed. fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { let opener = style.opener(); match style { @@ -1104,6 +1106,8 @@ fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a s (line, true) } else if let Some(line) = line.strip_prefix(style.line_start().trim_start()) { (line, true) + } else if let Some(line) = line.strip_prefix(style.line_start().trim()) { + (line, false) } else { (line, false) } diff --git a/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs b/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs index e23db69f178..1f33117385c 100644 --- a/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs +++ b/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs @@ -18,3 +18,8 @@ struct Bar; * // baz * ``` */ + +/* + * + * boop + */ diff --git a/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs b/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs index cabfc401be3..13a9be7a628 100644 --- a/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs +++ b/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs @@ -12,3 +12,5 @@ struct Bar; // ``` // // baz // ``` + +// boop