From ca3703138193d1f3567f37a88d7e70988f03c07e Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Tue, 23 Jun 2026 17:17:57 +0800 Subject: [PATCH 1/3] Add failing regression test for non-idempotent block doc comments See . Compared to the reported MCVE, the test case added: - Adds another layer of outermost block doc comment for the outer module. - Also exercises inner block doc comments (the reported example involves only outer block doc comments). --- .../inner-block-doc-comment.rs | 9 +++++++++ .../outer-block-doc-comment.rs | 13 +++++++++++++ .../inner-block-doc-comment.rs | 9 +++++++++ .../outer-block-doc-comment.rs | 13 +++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 tests/source/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs create mode 100644 tests/source/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs create mode 100644 tests/target/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs create mode 100644 tests/target/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs diff --git a/tests/source/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs b/tests/source/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs new file mode 100644 index 00000000000..d46cf3688ec --- /dev/null +++ b/tests/source/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs @@ -0,0 +1,9 @@ +/*!First comment. +*/ +pub mod outer { + /*!Second comment. +*/ + pub struct Inner { + pub octets: Vec, + } +} diff --git a/tests/source/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs b/tests/source/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs new file mode 100644 index 00000000000..b1c8e1c8518 --- /dev/null +++ b/tests/source/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs @@ -0,0 +1,13 @@ +/**First comment. +* +*/ +pub mod outer { + /**Second comment. +foo +*/ + pub struct Inner { + /**Third comment. +*/ + pub octets: Vec, + } +} diff --git a/tests/target/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs b/tests/target/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs new file mode 100644 index 00000000000..660b855dadd --- /dev/null +++ b/tests/target/6639-non-idempotent-block-doc-comments/inner-block-doc-comment.rs @@ -0,0 +1,9 @@ +/*!First comment. + */ +pub mod outer { + /*!Second comment. + */ + pub struct Inner { + pub octets: Vec, + } +} diff --git a/tests/target/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs b/tests/target/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs new file mode 100644 index 00000000000..13bd081a846 --- /dev/null +++ b/tests/target/6639-non-idempotent-block-doc-comments/outer-block-doc-comment.rs @@ -0,0 +1,13 @@ +/**First comment. + * + */ +pub mod outer { + /**Second comment. + foo + */ + pub struct Inner { + /**Third comment. + */ + pub octets: Vec, + } +} From bbc713a33774846e16a19062b5ad5c3b1da3562e Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Fri, 7 Aug 2026 16:26:23 +0800 Subject: [PATCH 2/3] Make block doc comment closer rewrite idempotent # The symptom Consider the example ```rs pub mod outer { /**First comment. */ pub struct Inner { pub octets: Vec, } } ``` Previously, this was *non-idempotent* and required **2** passes to converge to a final formatting. ## Format 1 Format pass 1 tries to realign the block doc comment closer with the starter `/**`. Notice that the closer `*/` aligns with `/**` without a leading whitespace in the closer. ```diff pub mod outer { /**First comment. -*/ + */ pub struct Inner { pub octets: Vec, } ``` After this pass, if you run `rustfmt --check` on this, rustfmt will report formatting difference: ```text Diff in /Users/joe.xu/Documents/repos/rustfmt/foo.rs:1: pub mod outer { /**First comment. - */ + */ pub struct Inner { pub octets: Vec, } ``` ## Format 2 Run `rustfmt` again, then formatting converges to ```rs pub mod outer { /**First comment. */ pub struct Inner { pub octets: Vec, } } ``` Notice that the closer `*/`'s `*` now has a leading whitespace, aligning it with the first `*` in the starter `/**`. # Analysis What went wrong? The formatting pathways hit here are fairly localized, and mostly concern the pathway `identify_comment -> light_rewrite_comment`. The `light_rewrite_comment` helper has a bug where the implementation doesn't agree with the indent expressed by the comment: ```text // This is basically just l.trim(), but in the case that a line starts // with `*` we want to leave one space before it, so it aligns with the // `*` in `/*`. ``` Take the same `/**First comment.\n*/` example, the execution trace broadly looks like: ``` rustfmt_nightly::comment::identify_comment{} TRACE rustfmt_nightly::comment style=DoubleBullet TRACE rustfmt_nightly::comment block comment TRACE rustfmt_nightly::comment has_bare_lines=false, first_group_ending=20 TRACE rustfmt_nightly::comment first_group="/**First comment.\n*/", rest="" TRACE rustfmt_nightly::comment !normalize_comments && !wrap_comments && !(is_doc_comment && format_code_in_doc_comments) rustfmt_nightly::comment::light_rewrite_comment{orig="/**First comment.\n*/", offset=Indent { block_indent: 4, alignment: 0 }, is_doc_comment=true} TRACE rustfmt_nightly::comment first_non_whitespace=0 TRACE rustfmt_nightly::comment left_trimmed="/**First comment." TRACE rustfmt_nightly::comment first_non_whitespace=0 TRACE rustfmt_nightly::comment left_trimmed="*/" ``` The previous implementation was ```rs let first_non_whitespace = l.find(|c| !char::is_whitespace(c)); let left_trimmed = if let Some(fnw) = first_non_whitespace { if l.as_bytes()[fnw] == b'*' && fnw > 0 { &l[fnw - 1..] } else { &l[fnw..] } } else { "" }; ``` The problem was that, when the closer line has no leading whitespace, i.e. `l = "*/"`, `first_non_whitespace` would trigger on `*`, and so `fnw = 0`. This would hit the else branch `&l[fnw..]`, which eventually produces an closer alignment without a leading whitespace: ```rs /** */ <- missing leading whitespace ``` # Fix The key logical fix here is to drop the `fnw > 0` condition, and in the `*`-leading branch, pad a whitespace and directly use the leading-whitespace trimmed portion `&l[fnw..]`. I used a `Cow` here because the leading-whitespace padding would need a `format!` allocation, whereas the other branch need only a string slice and no additional allocation. *Maybe* it's not worth the extra complexity and we should just use `String`, but yeah. --- src/comment.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 05d7310122a..40e42b53ed1 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -1074,16 +1074,26 @@ fn light_rewrite_comment( // `*` in `/*`. let first_non_whitespace = l.find(|c| !char::is_whitespace(c)); let left_trimmed = if let Some(fnw) = first_non_whitespace { - if l.as_bytes()[fnw] == b'*' && fnw > 0 { - &l[fnw - 1..] + if l.as_bytes()[fnw] == b'*' { + Cow::Owned(format!(" {}", &l[fnw..])) } else { - &l[fnw..] + Cow::Borrowed(&l[fnw..]) } } else { - "" + Cow::Borrowed("") }; + // Preserve markdown's double-space line break syntax in doc comment. - trim_end_unless_two_whitespaces(left_trimmed, is_doc_comment) + match left_trimmed { + Cow::Borrowed(left_trimmed) => Cow::Borrowed(trim_end_unless_two_whitespaces( + left_trimmed, + is_doc_comment, + )), + Cow::Owned(left_trimmed) => { + let trimmed = trim_end_unless_two_whitespaces(&left_trimmed, is_doc_comment); + Cow::Owned(trimmed.to_string()) + } + } }) .join(&format!("\n{}", offset.to_string(config))) } From e95bcc40f425616f5b9216b55ca4b268c7d77315 Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Fri, 7 Aug 2026 17:07:24 +0800 Subject: [PATCH 3/3] Add test to demonstrate a stable default formatting change For better or worse, for outer block doc comments, this patch does unfortunately change the comment formatting for cases like ``` $ cat foo.rs /** */ mod foo {} ``` Stable rustfmt considers this already well-formatted. ```bash $ rustfmt +stable --version rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14) ``` ```bash $ rustfmt +stable foo.rs --check --config-path=/dev/null ``` With this patch, we only consider the closer well-formatted if its asterisk `*` is aligned with the first `*` in the opener. ``` $ rustfmt-dev foo.rs --check --config-path=/dev/null Diff in /Users/joe.xu/Documents/repos/rustfmt/foo.rs:1: /** -*/ + */ mod foo {} ``` I believe this is acceptable, since: 1. Comments are explicitly carved out from stable formatting stability guarantees, and 2. This impacts block doc comments, which IME is extremely rarely used. --- .../naive-outer-block-comment.rs | 3 +++ .../naive-outer-block-comment.rs | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/source/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs create mode 100644 tests/target/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs diff --git a/tests/source/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs b/tests/source/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs new file mode 100644 index 00000000000..581dea9e3ab --- /dev/null +++ b/tests/source/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs @@ -0,0 +1,3 @@ +/** +*/ +mod foo {} diff --git a/tests/target/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs b/tests/target/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs new file mode 100644 index 00000000000..42f39ee29ae --- /dev/null +++ b/tests/target/6639-non-idempotent-block-doc-comments/naive-outer-block-comment.rs @@ -0,0 +1,3 @@ +/** + */ +mod foo {}