From d6d774bc37f5795231771108f9978c91fbfd4a8e Mon Sep 17 00:00:00 2001 From: Jess Izen Date: Sun, 3 May 2026 18:01:34 +0000 Subject: [PATCH 1/4] refactor: remove trailing space from format_visibility format_visibility returned e.g. "pub(crate) " with a trailing space. Every call site either relied on this space as a separator or (in format_header) explicitly trimmed it off. Remove the trailing space from format_visibility and add it explicitly at each call site. No change in formatted output. This fixes the root cause of #6880: tuple struct field prefixes no longer carry a trailing space into rewrite_assign_rhs, so wrapping the type to the next line can no longer leave trailing whitespace. --- src/imports.rs | 8 +++- src/items.rs | 60 ++++++++++++++++++------ src/macros.rs | 4 +- src/utils.rs | 4 +- src/visitor.rs | 3 ++ tests/target/struct_field_doc_comment.rs | 8 ++-- 6 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/imports.rs b/src/imports.rs index 2f26791639a..1415fb6034e 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -344,13 +344,17 @@ impl UseTree { let vis = self.visibility.as_ref().map_or(Cow::from(""), |vis| { crate::utils::format_visibility(context, vis) }); + let vis_separator = if vis.is_empty() { "" } else { " " }; let use_str = self - .rewrite_result(context, shape.offset_left(vis.len(), self.span())?) + .rewrite_result( + context, + shape.offset_left(vis.len() + vis_separator.len(), self.span())?, + ) .map(|s| { if s.is_empty() { s } else { - format!("{}use {};", vis, s) + format!("{vis}{vis_separator}use {s};") } })?; match self.attrs { diff --git a/src/items.rs b/src/items.rs index a2c0e8e0f50..ab412610ff8 100644 --- a/src/items.rs +++ b/src/items.rs @@ -353,7 +353,11 @@ impl<'a> FnSig<'a> { fn to_str(&self, context: &RewriteContext<'_>) -> String { let mut result = String::with_capacity(128); // Vis defaultness constness unsafety abi. - result.push_str(&*format_visibility(context, self.visibility)); + let vis = format_visibility(context, self.visibility); + result.push_str(&*vis); + if !vis.is_empty() { + result.push(' '); + } result.push_str(format_defaultness(self.defaultness)); result.push_str(format_constness(self.constness)); self.coroutine_kind @@ -956,7 +960,11 @@ fn format_impl_ref_and_type( } = iimpl; let mut result = String::with_capacity(128); - result.push_str(&format_visibility(context, &item.vis)); + let vis = format_visibility(context, &item.vis); + result.push_str(&vis); + if !vis.is_empty() { + result.push(' '); + } if let Some(of_trait) = of_trait.as_deref() { result.push_str(format_defaultness(of_trait.defaultness)); @@ -1165,9 +1173,10 @@ pub(crate) fn format_trait( } = *trait_; let mut result = String::with_capacity(128); + let vis = format_visibility(context, &item.vis); + let vis_separator = if vis.is_empty() { "" } else { " " }; let header = format!( - "{}{}{}{}trait ", - format_visibility(context, &item.vis), + "{vis}{vis_separator}{}{}{}trait ", format_constness(constness), format_safety(safety), format_auto(is_auto), @@ -1376,8 +1385,9 @@ pub(crate) fn format_trait_alias( let g_shape = shape.offset_left(6, span)?.sub_width(2, span)?; let generics_str = rewrite_generics(context, alias, &ta.generics, g_shape)?; let vis_str = format_visibility(context, vis); + let vis_separator = if vis_str.is_empty() { "" } else { " " }; let constness = format_constness(ta.constness); - let lhs = format!("{vis_str}{constness}trait {generics_str} ="); + let lhs = format!("{vis_str}{vis_separator}{constness}trait {generics_str} ="); // 1 = ";" let trait_alias_bounds = TraitAliasBounds { generic_bounds: &ta.bounds, @@ -1749,7 +1759,12 @@ fn rewrite_ty( ) -> RewriteResult { let mut result = String::with_capacity(128); let TyAliasRewriteInfo(context, indent, generics, after_where_clause, ident, span) = *rw_info; - result.push_str(&format!("{}type ", format_visibility(context, vis))); + let vis = format_visibility(context, vis); + if !vis.is_empty() { + result.push_str(&format!("{vis} type ")); + } else { + result.push_str("type "); + } let ident_str = rewrite_ident(context, ident); if generics.params.is_empty() { @@ -1887,15 +1902,18 @@ pub(crate) fn rewrite_struct_field_prefix( field: &ast::FieldDef, ) -> RewriteResult { let vis = format_visibility(context, &field.vis); + let vis_separator = if vis.is_empty() { "" } else { " " }; let safety = format_safety(field.safety); let type_annotation_spacing = type_annotation_spacing(context.config); Ok(match field.ident { Some(name) => format!( - "{vis}{safety}{}{}:", + "{vis}{vis_separator}{safety}{}{}:", rewrite_ident(context, name), type_annotation_spacing.0 ), - None => format!("{vis}{safety}"), + None => format!("{vis}{vis_separator}{safety}") + .trim_end() + .to_string(), }) } @@ -1936,6 +1954,8 @@ pub(crate) fn rewrite_struct_field( }; let mut spacing = String::from(if field.ident.is_some() { type_annotation_spacing.1 + } else if !prefix.is_empty() { + " " } else { "" }); @@ -1970,6 +1990,13 @@ pub(crate) fn rewrite_struct_field( let is_prefix_empty = prefix.is_empty(); // We must use multiline. We are going to put attributes and a field on different lines. + // Trim trailing whitespace from the prefix for tuple struct fields to avoid leaving it + // behind when the type wraps to the next line (e.g. "pub(crate) " -> "pub(crate)"). + let prefix = if field.ident.is_none() { + prefix.trim_end().to_string() + } else { + prefix + }; let field_str = rewrite_assign_rhs(context, prefix, &*field.ty, &RhsAssignKind::Ty, shape)?; // Remove a leading white-space from `rewrite_assign_rhs()` when rewriting a tuple struct. let field_str = if is_prefix_empty { @@ -2116,9 +2143,10 @@ fn rewrite_static( } let colon = colon_spaces(context.config); + let vis = format_visibility(context, static_parts.vis); + let vis_separator = if vis.is_empty() { "" } else { " " }; let mut prefix = format!( - "{}{}{}{} {}{}{}", - format_visibility(context, static_parts.vis), + "{vis}{vis_separator}{}{}{} {}{}{}", static_parts.defaultness.map_or("", format_defaultness), format_safety(static_parts.safety), static_parts.prefix, @@ -3307,7 +3335,7 @@ fn format_header( let mut result = String::with_capacity(128); let shape = Shape::indented(offset, context.config); - result.push_str(format_visibility(context, vis).trim()); + result.push_str(&format_visibility(context, vis)); // Check for a missing comment between the visibility and the item name. let after_vis = vis.span.hi(); @@ -3493,11 +3521,11 @@ impl Rewrite for ast::ForeignItem { // FIXME(#21): we're dropping potential comments in between the // function kw here. let vis = format_visibility(context, &self.vis); + let vis_separator = if vis.is_empty() { "" } else { " " }; let safety = format_safety(static_foreign_item.safety); let mut_str = format_mutability(static_foreign_item.mutability); let prefix = format!( - "{}{}static {}{}:", - vis, + "{vis}{vis_separator}{}static {}{}:", safety, mut_str, rewrite_ident(context, static_foreign_item.ident) @@ -3580,7 +3608,11 @@ pub(crate) fn rewrite_mod( attrs_shape: Shape, ) -> RewriteResult { let mut result = String::with_capacity(32); - result.push_str(&*format_visibility(context, &item.vis)); + let vis = format_visibility(context, &item.vis); + result.push_str(&*vis); + if !vis.is_empty() { + result.push(' '); + } result.push_str("mod "); result.push_str(rewrite_ident(context, ident)); result.push(';'); diff --git a/src/macros.rs b/src/macros.rs index 2d56021069c..5ab347fb8b1 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1454,10 +1454,10 @@ fn format_lazy_static( for (i, (vis, id, ty, expr)) in parsed_elems.iter().enumerate() { // Rewrite as a static item. let vis = crate::utils::format_visibility(context, vis); + let vis_separator = if vis.is_empty() { "" } else { " " }; let mut stmt = String::with_capacity(128); stmt.push_str(&format!( - "{}static ref {}: {} =", - vis, + "{vis}{vis_separator}static ref {}: {} =", id, ty.rewrite_result(context, nested_shape)? )); diff --git a/src/utils.rs b/src/utils.rs index b676803379f..7e7101dc059 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -55,7 +55,7 @@ pub(crate) fn format_visibility( vis: &Visibility, ) -> Cow<'static, str> { match vis.kind { - VisibilityKind::Public => Cow::from("pub "), + VisibilityKind::Public => Cow::from("pub"), VisibilityKind::Inherited => Cow::from(""), VisibilityKind::Restricted { ref path, .. } => { let Path { ref segments, .. } = **path; @@ -69,7 +69,7 @@ pub(crate) fn format_visibility( let path = segments_iter.collect::>().join("::"); let in_str = if is_keyword(&path) { "" } else { "in " }; - Cow::from(format!("pub({in_str}{path}) ")) + Cow::from(format!("pub({in_str}{path})")) } } } diff --git a/src/visitor.rs b/src/visitor.rs index 4072a1d8697..84cdbf2e161 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -983,6 +983,9 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { ) { let vis_str = utils::format_visibility(&self.get_context(), vis); self.push_str(&*vis_str); + if !vis_str.is_empty() { + self.push_str(" "); + } self.push_str(format_safety(safety)); self.push_str("mod "); // Calling `to_owned()` to work around borrow checker. diff --git a/tests/target/struct_field_doc_comment.rs b/tests/target/struct_field_doc_comment.rs index ebb01a668f4..0bc074cf08e 100644 --- a/tests/target/struct_field_doc_comment.rs +++ b/tests/target/struct_field_doc_comment.rs @@ -25,17 +25,17 @@ struct MyTuple( struct MyTuple( #[cfg(unix)] // some comment - pub u64, - #[cfg(not(unix))] /*block comment */ pub(crate) u32, + pub u64, + #[cfg(not(unix))] /*block comment */ pub(crate) u32, ); struct MyTuple( /// Doc Comments /* TODO note to add more to Doc Comments */ - pub u32, + pub u32, /// Doc Comments // TODO note - pub(crate) u64, + pub(crate) u64, ); struct MyStruct { From 8c559eeff166c742ca56441618b3dbc2ad262417 Mon Sep 17 00:00:00 2001 From: Jess Izen Date: Sun, 3 May 2026 18:16:16 +0000 Subject: [PATCH 2/4] test: add regression test for #5703 Covers trailing whitespace on tuple struct fields with parenthesized visibility qualifiers (pub(crate), pub(super), pub(in path)) when the type wraps to the next line. Also includes pub with wrapping, named struct fields, and a short tuple field as controls. Fixes #5703 Fixes #6880 --- tests/source/issue-5703.rs | 21 +++++++++++++++++++++ tests/target/issue-5703.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/source/issue-5703.rs create mode 100644 tests/target/issue-5703.rs diff --git a/tests/source/issue-5703.rs b/tests/source/issue-5703.rs new file mode 100644 index 00000000000..df7dafe846f --- /dev/null +++ b/tests/source/issue-5703.rs @@ -0,0 +1,21 @@ +struct PubCrate( + pub(crate) std::collections::HashMap, +); + +struct PubSuper( + pub(super) std::collections::HashMap, +); + +struct PubInPath( + pub(in some::module) std::collections::HashMap, +); + +struct PubPlain( + pub std::collections::HashMap, +); + +struct NamedPubCrate { + pub(crate) field: std::collections::HashMap, +} + +struct ShortPubCrate(pub(crate) u32); diff --git a/tests/target/issue-5703.rs b/tests/target/issue-5703.rs new file mode 100644 index 00000000000..83b43985541 --- /dev/null +++ b/tests/target/issue-5703.rs @@ -0,0 +1,28 @@ +struct PubCrate( + pub(crate) + std::collections::HashMap, +); + +struct PubSuper( + pub(super) + std::collections::HashMap, +); + +struct PubInPath( + pub(in some::module) + std::collections::HashMap, +); + +struct PubPlain( + pub std::collections::HashMap< + some::module::path::TypeNameAaaaaaaaaaa, + some::module::path::TypeNameB, + >, +); + +struct NamedPubCrate { + pub(crate) field: + std::collections::HashMap, +} + +struct ShortPubCrate(pub(crate) u32); From de4c4544f6b2885d308c9d6dfd458f9a3825aa74 Mon Sep 17 00:00:00 2001 From: Jess Izen Date: Sun, 3 May 2026 18:16:42 +0000 Subject: [PATCH 3/4] test: add regression test for #5525 Covers the double space between pub and extern "C" in tuple struct fields when arguments wrap to multiple lines. Fixes #5525 --- tests/source/issue-5525.rs | 7 +++++++ tests/target/issue-5525.rs | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 tests/source/issue-5525.rs create mode 100644 tests/target/issue-5525.rs diff --git a/tests/source/issue-5525.rs b/tests/source/issue-5525.rs new file mode 100644 index 00000000000..ab56ed43f91 --- /dev/null +++ b/tests/source/issue-5525.rs @@ -0,0 +1,7 @@ +pub struct SomeCallback( + pub extern "C" fn( + long_argument_name_to_avoid_wrap: u32, + second_long_argument_name: u32, + third_long_argument_name: u32, + ), +); diff --git a/tests/target/issue-5525.rs b/tests/target/issue-5525.rs new file mode 100644 index 00000000000..ab56ed43f91 --- /dev/null +++ b/tests/target/issue-5525.rs @@ -0,0 +1,7 @@ +pub struct SomeCallback( + pub extern "C" fn( + long_argument_name_to_avoid_wrap: u32, + second_long_argument_name: u32, + third_long_argument_name: u32, + ), +); From 0742c12d514beb10ccc49d9f1d33615f9cc937c8 Mon Sep 17 00:00:00 2001 From: Jess Izen Date: Sun, 3 May 2026 18:17:05 +0000 Subject: [PATCH 4/4] test: add regression test for #5997 Covers the double space between pub and type in a newtype when an attribute is followed by a comment. Fixes #5997 --- tests/source/issue-5997.rs | 11 +++++++++++ tests/target/issue-5997.rs | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/source/issue-5997.rs create mode 100644 tests/target/issue-5997.rs diff --git a/tests/source/issue-5997.rs b/tests/source/issue-5997.rs new file mode 100644 index 00000000000..d2b547bf196 --- /dev/null +++ b/tests/source/issue-5997.rs @@ -0,0 +1,11 @@ +pub struct Newtype( + /// Doc + #[doc()] // + pub Vec, +); + +pub struct Newtype2( + /// Doc + #[doc()] + pub Vec, +); diff --git a/tests/target/issue-5997.rs b/tests/target/issue-5997.rs new file mode 100644 index 00000000000..0ba9acf28d7 --- /dev/null +++ b/tests/target/issue-5997.rs @@ -0,0 +1,11 @@ +pub struct Newtype( + /// Doc + #[doc()] // + pub Vec, +); + +pub struct Newtype2( + /// Doc + #[doc()] + pub Vec, +);