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 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
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 S-waiting-on-review Status: awaiting review from the assignee but also interested parties.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-idempotency issue for block doc comments

3 participants