Skip to content

Fix non-idempotent block doc comment closer rewrite - #7017

Open
jieyouxu wants to merge 3 commits into
rust-lang:mainfrom
jieyouxu:jieyouxu/fix/non-idempotent-block-doc-comment
Open

Fix non-idempotent block doc comment closer rewrite#7017
jieyouxu wants to merge 3 commits into
rust-lang:mainfrom
jieyouxu:jieyouxu/fix/non-idempotent-block-doc-comment

Conversation

@jieyouxu

@jieyouxu jieyouxu commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #6639, where the formatting for block doc comments (both inner and outer styles) was non-idempotent on the closer, and takes 2 format passes to converge.

The symptom

Consider the example

pub mod outer {
    /**First comment.
*/
    pub struct Inner {
        pub octets: Vec<u8>,
    }
}

Previously, this was non-idempotent and required 2 passes to converge to a final formatting.

Format pass 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.

 pub mod outer {
     /**First comment.
-*/
+    */
     pub struct Inner {
         pub octets: Vec<u8>,
     }

After this pass, if you run rustfmt --check on this, rustfmt will report formatting difference:

Diff in /Users/joe.xu/Documents/repos/rustfmt/foo.rs:1:
 pub mod outer {
     /**First comment.
-    */
+     */
     pub struct Inner {
         pub octets: Vec<u8>,
     }

Format pass 2

Run rustfmt again, then formatting converges to

pub mod outer {
    /**First comment.
     */
    pub struct Inner {
        pub octets: Vec<u8>,
    }
}

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 intent expressed by the comment:

// 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

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:

    /**
    */  <- 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.

Test coverage

  • Commit 1 adds a regression test taken from Non-idempotency issue for block doc comments #6639 (+ inner block doc comment variant) that is slightly expanded to cover extra *-leading line between starter/closer lines, and a non-*-line between starter/closer lines.
  • Commit 3 adds a test to demonstrate that this patch changes how block doc comment's closer is changed under stable default format.

Formatting stability

This PR technically changes stable default formatting. However:

  1. Comment formatting IINM is explicitly excluded from formatting stability guarantees, and
  2. Block doc comment IME is comparatively rare.

Diff-Check {Edition 2024, Style Edition 2024}: https://github.com/rust-lang/rustfmt/actions/runs/31167102874/job/92830360025

AI usage disclaimer

I did not use an LLM to create changes or comments in this PR. I used an LLM to review the changes in this PR.

See <rust-lang#6639>. 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).
# The symptom

Consider the example

```rs
pub mod outer {
    /**First comment.
*/
    pub struct Inner {
        pub octets: Vec<u8>,
    }
}
```

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<u8>,
     }
```

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<u8>,
     }
```

## Format 2

Run `rustfmt` again, then formatting converges to

```rs
pub mod outer {
    /**First comment.
     */
    pub struct Inner {
        pub octets: Vec<u8>,
    }
}
```

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.
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.
@jieyouxu jieyouxu added A-comments Area: comments F-impacts-stable-default-format Expected formatting impact: affects stable default format configuration (caution) F-stability-guarantee-exempt Feature: impacts stable, but explicitly exempt from format stability guarantees. See README labels Aug 7, 2026
@rustbot rustbot added the S-waiting-on-review Status: awaiting review from the assignee but also interested parties. label Aug 7, 2026
@jieyouxu

jieyouxu commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Diff-Check

https://github.com/rust-lang/rustfmt/actions/runs/31167102874/job/92830360025

Two failures:

2026-08-07T09:47:44.184994Z ERROR check_diff: Diff found in 'rust' when formatting rust/compiler/rustc_codegen_llvm/src/common.rs
--- original
+++ modified
@@ -69,30 +69,30 @@
 }
 /*
-* A note on nomenclature of linking: "extern", "foreign", and "upcall".
-*
-* An "extern" is an LLVM symbol we wind up emitting an undefined external
-* reference to. This means "we don't have the thing in this compilation unit,
-* please make sure you link it in at runtime". This could be a reference to
-* C code found in a C library, or rust code found in a rust crate.
-*
-* Most "externs" are implicitly declared (automatically) as a result of a
-* user declaring an extern _module_ dependency; this causes the rust driver
-* to locate an extern crate, scan its compilation metadata, and emit extern
-* declarations for any symbols used by the declaring crate.
-*
-* A "foreign" is an extern that references C (or other non-rust ABI) code.
-* There is no metadata to scan for extern references so in these cases either
-* a header-digester like bindgen, or manual function prototypes, have to
-* serve as declarators. So these are usually given explicitly as prototype
-* declarations, in rust code, with ABI attributes on them noting which ABI to
-* link via.
-*
-* An "upcall" is a foreign call generated by the compiler (not corresponding
-* to any user-written call in the code) into the runtime library, to perform
-* some helper task such as bringing a task to life, allocating memory, etc.
-*
-*/
+ * A note on nomenclature of linking: "extern", "foreign", and "upcall".
+ *
+ * An "extern" is an LLVM symbol we wind up emitting an undefined external
+ * reference to. This means "we don't have the thing in this compilation unit,
+ * please make sure you link it in at runtime". This could be a reference to
+ * C code found in a C library, or rust code found in a rust crate.
+ *
+ * Most "externs" are implicitly declared (automatically) as a result of a
+ * user declaring an extern _module_ dependency; this causes the rust driver
+ * to locate an extern crate, scan its compilation metadata, and emit extern
+ * declarations for any symbols used by the declaring crate.
+ *
+ * A "foreign" is an extern that references C (or other non-rust ABI) code.
+ * There is no metadata to scan for extern references so in these cases either
+ * a header-digester like bindgen, or manual function prototypes, have to
+ * serve as declarators. So these are usually given explicitly as prototype
+ * declarations, in rust code, with ABI attributes on them noting which ABI to
+ * link via.
+ *
+ * An "upcall" is a foreign call generated by the compiler (not corresponding
+ * to any user-written call in the code) into the runtime library, to perform
+ * some helper task such as bringing a task to life, allocating memory, etc.
+ *
+ */
 /// A structure representing an active landing pad for the duration of a basic
 /// block.
2026-08-07T09:47:44.185216Z ERROR check_diff: Diff found in 'rust' when formatting rust/compiler/rustc_mir_transform/src/inline.rs
--- original
+++ modified
@@ -1175,7 +1175,7 @@
  * Integrates blocks from the callee function into the calling function.
  * Updates block indices, references to locals and other control flow
  * stuff.
-*/
+ */
 struct Integrator<'a, 'tcx> {
     args: &'a [Local],
     new_locals: RangeFrom<Local>,
2026-08-07T09:47:44.186055Z ERROR check_diff: 2 formatting diffs found 💔

I feel like the new formatting is actually more correct, at least that's how the Java block doc comments I've seen are formatted. According to the comment, I think the new formatting is actually the intended formatting? AFAICT the style guide doesn't specify this.

@ytmimi ytmimi self-assigned this Aug 7, 2026
Comment on lines +5 to +7
/**Second comment.
foo
*/

@ytmimi ytmimi Aug 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm things didn't get aligned in this case. Was that expected?

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, let me double check

@ytmimi

ytmimi commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Two failures:

Diff-Check

https://github.com/rust-lang/rustfmt/actions/runs/31167102874/job/92830360025

Two failures:

Overall I think this new alignment makes sense. Should we turn these failures into formatting test cases?

@jieyouxu jieyouxu added S-waiting-on-author Status: awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: awaiting review from the assignee but also interested parties. labels Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-comments Area: comments F-impacts-stable-default-format Expected formatting impact: affects stable default format configuration (caution) F-stability-guarantee-exempt Feature: impacts stable, but explicitly exempt from format stability guarantees. See README S-waiting-on-author Status: awaiting some action (such as code changes or more information) from the author.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-idempotency issue for block doc comments

3 participants