Repository: rust-lang/rustfmt (rust)

Issue:
rustfmt removes comment between doc comment and generic params
This was an issue discovered when implementing a fix for #5122 in PR https://github.com/rust-lang/rustfmt/pull/5124#issuecomment-1030806191. The issue is reproducible on `rustfmt 1.4.38-nightly (8572b343 2022-04-24)` and uses the `rustfmt.toml` defined in the rustfmt repo.

```toml
# rustfmt.toml
error_on_line_overflow = true
error_on_unformatted = true
version = "Two"
```

This input:

```rust
// Non-doc pre-comment of Foo
/// doc of Foo
// Non-doc post-comment of Foo
struct Foo<
    // Non-doc pre-comment of 'a
    /// doc of 'a
    // Non-doc post-comment of 'a
    'a,
    // Non-doc pre-comment of T
    /// doc of T
    // Non-doc post-comment of T
    T,
    // Non-doc pre-comment of N
    /// doc of N
    // Non-doc post-comment of N
    const N: item,
>;
```

Gets formatted as follows:

```rust
// Non-doc pre-comment of Foo
/// doc of Foo
// Non-doc post-comment of Foo
struct Foo<
    // Non-doc pre-comment of 'a
    /// doc of 'a
    'a,
    // Non-doc pre-comment of T
    /// doc of T
    T,
    // Non-doc pre-comment of N
    /// doc of N
    const N: item,
>;
```

Pull request description:
Recover comments between attrs and generic param
Fixes #5320.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 24166d51..5b0fd5ae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -468,12 +468,6 @@ version = "0.6.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
 
-[[package]]
-name = "rustc-workspace-hack"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc71d2faa173b74b232dedc235e3ee1696581bb132fc116fa3626d6151a1a8fb"
-
 [[package]]
 name = "rustfmt-config_proc_macro"
 version = "0.3.0"
@@ -502,7 +496,6 @@ dependencies = [
  "lazy_static",
  "log",
  "regex",
- "rustc-workspace-hack",
  "rustfmt-config_proc_macro",
  "serde",
  "serde_json",
diff --git a/Cargo.toml b/Cargo.toml
index 87ce59d0..12ed6545 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -59,11 +59,6 @@ unicode_categories = "0.1"
 
 rustfmt-config_proc_macro = { version = "0.3", path = "config_proc_macro" }
 
-# A noop dependency that changes in the Rust repository, it's a bit of a hack.
-# See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust`
-# for more information.
-rustc-workspace-hack = "1.0.0"
-
 # Rustc dependencies are loaded from the sysroot, Cargo doesn't know about them.
 
 [package.metadata.rust-analyzer]
diff --git a/src/overflow.rs b/src/overflow.rs
index af0b9543..d81bf24d 100644
--- a/src/overflow.rs
+++ b/src/overflow.rs
@@ -589,6 +589,8 @@ impl<'a> Context<'a> {
 
     fn rewrite_items(&self) -> Option<(bool, String)> {
         let span = self.items_span();
+        debug!("items: {:?}", self.items);
+
         let items = itemize_list(
             self.context.snippet_provider,
             self.items.iter(),
@@ -603,6 +605,8 @@ impl<'a> Context<'a> {
         );
         let mut list_items: Vec<_> = items.collect();
 
+        debug!("items: {list_items:?}");
+
         // Try letting the last argument overflow to the next line with block
         // indentation. If its first line fits on one line with the other arguments,
         // we format the function arguments horizontally.
diff --git a/src/types.rs b/src/types.rs
index 01e2fb6e..b4be6546 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -572,49 +572,41 @@ impl Rewrite for ast::GenericBounds {
 
 impl Rewrite for ast::GenericParam {
     fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> {
-        let mut result = String::with_capacity(128);
         // FIXME: If there are more than one attributes, this will force multiline.
-        match self.attrs.rewrite(context, shape) {
-            Some(ref rw) if !rw.is_empty() => {
-                result.push_str(rw);
-                // When rewriting generic params, an extra newline should be put
-                // if the attributes end with a doc comment
-                if let Some(true) = self.attrs.last().map(|a| a.is_doc_comment()) {
-                    result.push_str(&shape.indent.to_string_with_newline(context.config));
-                } else {
-                    result.push(' ');
-                }
-            }
-            _ => (),
-        }
+        let mut result = self.attrs.rewrite(context, shape).unwrap_or(String::new());
+        let has_attrs = !result.is_empty();
 
-        if let ast::GenericParamKind::Const {
+        let mut param = String::with_capacity(128);
+
+        let param_start = if let ast::GenericParamKind::Const {
             ref ty,
-            kw_span: _,
+            kw_span,
             default,
         } = &self.kind
         {
-            result.push_str("const ");
-            result.push_str(rewrite_ident(context, self.ident));
-            result.push_str(": ");
-            result.push_str(&ty.rewrite(context, shape)?);
+            param.push_str("const ");
+            param.push_str(rewrite_ident(context, self.ident));
+            param.push_str(": ");
+            param.push_str(&ty.rewrite(context, shape)?);
             if let Some(default) = default {
                 let eq_str = match context.config.type_punctuation_density() {
                     TypeDensity::Compressed => "=",
                     TypeDensity::Wide => " = ",
                 };
-                result.push_str(eq_str);
-                let budget = shape.width.checked_sub(result.len())?;
+                param.push_str(eq_str);
+                let budget = shape.width.checked_sub(param.len())?;
                 let rewrite = default.rewrite(context, Shape::legacy(budget, shape.indent))?;
-                result.push_str(&rewrite);
+                param.push_str(&rewrite);
             }
+            kw_span.lo()
         } else {
-            result.push_str(rewrite_ident(context, self.ident));
-        }
+            param.push_str(rewrite_ident(context, self.ident));
+            self.ident.span.lo()
+        };
 
         if !self.bounds.is_empty() {
-            result.push_str(type_bound_colon(context));
-            result.push_str(&self.bounds.rewrite(context, shape)?)
+            param.push_str(type_bound_colon(context));
+            param.push_str(&self.bounds.rewrite(context, shape)?)
         }
         if let ast::GenericParamKind::Type {
             default: Some(ref def),
@@ -624,11 +616,33 @@ impl Rewrite for ast::GenericParam {
                 TypeDensity::Compressed => "=",
                 TypeDensity::Wide => " = ",
             };
-            result.push_str(eq_str);
-            let budget = shape.width.checked_sub(result.len())?;
+            param.push_str(eq_str);
+            let budget = shape.width.checked_sub(param.len())?;
             let rewrite =
-                def.rewrite(context, Shape::legacy(budget, shape.indent + result.len()))?;
-            result.push_str(&rewrite);
+                def.rewrite(context, Shape::legacy(budget, shape.indent + param.len()))?;
+            param.push_str(&rewrite);
+        }
+
+        if let Some(last_attr) = self.attrs.last().filter(|last_attr| {
+            contains_comment(context.snippet(mk_sp(last_attr.span.hi(), param_start)))
+        }) {
+            result = combine_strs_with_missing_comments(
+                context,
+                &result,
+                &param,
+                mk_sp(last_attr.span.hi(), param_start),
+                shape,
+                !last_attr.is_doc_comment(),
+            )?;
+        } else {
+            // When rewriting generic params, an extra newline should be put
+            // if the attributes end with a doc comment
+            if let Some(true) = self.attrs.last().map(|a| a.is_doc_comment()) {
+                result.push_str(&shape.indent.to_string_with_newline(context.config));
+            } else if has_attrs {
+                result.push(' ');
+            }
+            result.push_str(&param);
         }
 
         Some(result)


Test patch:
diff --git a/tests/target/doc-of-generic-item.rs b/tests/target/doc-of-generic-item.rs
index 2efc5e09..80886e74 100644
--- a/tests/target/doc-of-generic-item.rs
+++ b/tests/target/doc-of-generic-item.rs
@@ -12,3 +12,21 @@ struct Foo<
     /// doc of N
     const N: item,
 >;
+
+// Non-doc pre-comment of Foo
+/// doc of Foo
+// Non-doc post-comment of Foo
+struct Foo<
+    // Non-doc pre-comment of 'a
+    /// doc of 'a
+    // Non-doc post-comment of 'a
+    'a,
+    // Non-doc pre-comment of T
+    /// doc of T
+    // Non-doc post-comment of T
+    T,
+    // Non-doc pre-comment of N
+    /// doc of N
+    // Non-doc post-comment of N
+    const N: item,
+>;