diff --git a/src/chains.rs b/src/chains.rs index 90adb67ad43..db974e68ec1 100644 --- a/src/chains.rs +++ b/src/chains.rs @@ -566,8 +566,13 @@ impl Rewrite for Chain { formatter.format_root(&self.parent, context, shape)?; if let Some(result) = formatter.pure_root() { - return wrap_str(result, context.config.max_width(), shape) - .max_width_error(shape.width, self.parent.span); + return wrap_str( + context.config.style_edition(), + result, + context.config.max_width(), + shape, + ) + .max_width_error(shape.width, self.parent.span); } let first = self.children.first().unwrap_or(&self.parent); @@ -582,7 +587,13 @@ impl Rewrite for Chain { formatter.format_last_child(context, shape, child_shape)?; let result = formatter.join_rewrites(context, child_shape)?; - wrap_str(result, context.config.max_width(), shape).max_width_error(shape.width, full_span) + wrap_str( + context.config.style_edition(), + result, + context.config.max_width(), + shape, + ) + .max_width_error(shape.width, full_span) } } @@ -973,7 +984,12 @@ impl<'a> ChainFormatter for ChainFormatterVisual<'a> { .visual_indent(self.offset) .sub_width(self.offset, item.span)?; let rewrite = item.rewrite_result(context, child_shape)?; - if filtered_str_fits(&rewrite, context.config.max_width(), shape) { + if filtered_str_fits( + context.config.style_edition(), + &rewrite, + context.config.max_width(), + shape, + ) { root_rewrite.push_str(&rewrite); } else { // We couldn't fit in at the visual indent, try the last diff --git a/src/expr.rs b/src/expr.rs index b79137c4442..25274de6663 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -277,6 +277,7 @@ pub(crate) fn format_expr( ast::ExprKind::MacCall(ref mac) => { rewrite_macro(mac, context, shape, MacroPosition::Expression).or_else(|_| { wrap_str( + context.config.style_edition(), context.snippet(expr.span).to_owned(), context.config.max_width(), shape, @@ -1340,6 +1341,7 @@ pub(crate) fn rewrite_literal( token::LitKind::Integer => rewrite_int_lit(context, token_lit, span, shape), token::LitKind::Float => rewrite_float_lit(context, token_lit, span, shape), _ => wrap_str( + context.config.style_edition(), context.snippet(span).to_owned(), context.config.max_width(), shape, @@ -1360,8 +1362,13 @@ fn rewrite_string_lit(context: &RewriteContext<'_>, span: Span, shape: Shape) -> { return Ok(string_lit.to_owned()); } else { - return wrap_str(string_lit.to_owned(), context.config.max_width(), shape) - .max_width_error(shape.width, span); + return wrap_str( + context.config.style_edition(), + string_lit.to_owned(), + context.config.max_width(), + shape, + ) + .max_width_error(shape.width, span); } } @@ -1396,6 +1403,7 @@ fn rewrite_int_lit( }; if let Some(hex_lit) = hex_lit { return wrap_str( + context.config.style_edition(), format!( "0x{}{}", hex_lit, @@ -1409,6 +1417,7 @@ fn rewrite_int_lit( } wrap_str( + context.config.style_edition(), context.snippet(span).to_owned(), context.config.max_width(), shape, @@ -1427,6 +1436,7 @@ fn rewrite_float_lit( FloatLiteralTrailingZero::Preserve ) { return wrap_str( + context.config.style_edition(), context.snippet(span).to_owned(), context.config.max_width(), shape, @@ -1468,6 +1478,7 @@ fn rewrite_float_lit( "" }; wrap_str( + context.config.style_edition(), format!( "{}{}{}{}{}", integer_part, @@ -2321,7 +2332,12 @@ fn choose_rhs( match (orig_rhs, new_rhs) { (Ok(ref orig_rhs), Ok(ref new_rhs)) - if !filtered_str_fits(&new_rhs, context.config.max_width(), new_shape) => + if !filtered_str_fits( + context.config.style_edition(), + &new_rhs, + context.config.max_width(), + new_shape, + ) => { Ok(format!("{before_space_str}{orig_rhs}")) } diff --git a/src/macros.rs b/src/macros.rs index 1c5d804c6d4..ed7a2c8bca9 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1352,7 +1352,12 @@ impl MacroBranch { } }; - if !filtered_str_fits(&new_body_snippet.snippet, config.max_width(), shape) { + if !filtered_str_fits( + config.style_edition(), + &new_body_snippet.snippet, + config.max_width(), + shape, + ) { return Err(RewriteError::ExceedsMaxWidth { configured_width: shape.width, span: self.span, diff --git a/src/pairs.rs b/src/pairs.rs index 48948b88b3b..9d84d0d9628 100644 --- a/src/pairs.rs +++ b/src/pairs.rs @@ -102,7 +102,12 @@ fn rewrite_pairs_one_line( return None; } - wrap_str(result, context.config.max_width(), shape) + wrap_str( + context.config.style_edition(), + result, + context.config.max_width(), + shape, + ) } fn rewrite_pairs_multiline( diff --git a/src/string.rs b/src/string.rs index 3b971188cd5..69ef23eb3f0 100644 --- a/src/string.rs +++ b/src/string.rs @@ -150,7 +150,12 @@ pub(crate) fn rewrite_string<'a>( } result.push_str(fmt.closer); - wrap_str(result, fmt.config.max_width(), fmt.shape) + wrap_str( + fmt.config.style_edition(), + result, + fmt.config.max_width(), + fmt.shape, + ) } /// Returns the index to the end of the URL if the split at index of the given string includes a diff --git a/src/utils.rs b/src/utils.rs index b676803379f..aea609a7c12 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -400,36 +400,99 @@ macro_rules! skip_out_of_file_lines_range_visitor { // Wraps String in an Option. Returns Some when the string adheres to the // Rewrite constraints defined for the Rewrite trait and None otherwise. -pub(crate) fn wrap_str(s: String, max_width: usize, shape: Shape) -> Option { - if filtered_str_fits(&s, max_width, shape) { +pub(crate) fn wrap_str( + style_edition: StyleEdition, + s: String, + max_width: usize, + shape: Shape, +) -> Option { + if filtered_str_fits(style_edition, &s, max_width, shape) { Some(s) } else { None } } -pub(crate) fn filtered_str_fits(snippet: &str, max_width: usize, shape: Shape) -> bool { +pub(crate) fn filtered_str_fits( + style_edition: StyleEdition, + snippet: &str, + max_width: usize, + shape: Shape, +) -> bool { + use crate::comment::{FullCodeCharKind, LineClasses}; + let snippet = &filter_normal_code(snippet); if !snippet.is_empty() { - // First line must fits with `shape.width`. - if first_line_width(snippet) > shape.width { - return false; - } + let line_classes = { + if style_edition >= StyleEdition::Edition2027 { + // Collect line classifications to check for string content. + let line_classes: Vec<_> = LineClasses::new(snippet).collect(); + + // First line must fit with `shape.width`, unless it's a multi-line string + // literal that starts on this line - string content cannot be shortened. + let first_line_width = first_line_width(snippet); + if first_line_width > shape.width { + // Only allow the exception for multi-line strings (StartString indicates + // the string continues on subsequent lines). + let is_multiline = line_classes.len() > 1; + let first_is_multiline_string = is_multiline + && line_classes + .first() + .is_some_and(|(kind, _)| *kind == FullCodeCharKind::StartString); + if !first_is_multiline_string { + return false; + } + } + line_classes + } else { + // First line must fits with `shape.width`. + if first_line_width(snippet) > shape.width { + return false; + } + vec![] + } + }; + // If the snippet does not include newline, we are done. if is_single_line(snippet) { return true; } + // The other lines must fit within the maximum width. - if snippet - .lines() - .skip(1) - .any(|line| unicode_str_width(line) > max_width) - { - return false; + if style_edition < StyleEdition::Edition2027 { + if snippet + .lines() + .skip(1) + .any(|line| unicode_str_width(line) > max_width) + { + return false; + } } + + // Exception: lines that are inside or end a multi-line string literal + // may exceed max_width since string content cannot be reformatted. + let mut last_line_is_string = false; + for (i, (kind, line)) in line_classes.iter().enumerate() { + if i == 0 { + continue; // First line already checked above + } + // Track if the last line is string content + last_line_is_string = + *kind == FullCodeCharKind::InString || *kind == FullCodeCharKind::EndString; + + if unicode_str_width(line) > max_width { + // Allow lines that are string continuations (InString) or + // end a string (EndString) to exceed max_width. + if !last_line_is_string { + return false; + } + } + } + // A special check for the last line, since the caller may - // place trailing characters on this line. - if last_line_width(snippet) > shape.used_width() + shape.width { + // place trailing characters on this line. Skip this check if the + // last line is string content (which cannot be reformatted). + if !last_line_is_string && last_line_width(snippet) > shape.used_width() + shape.width { return false; } } diff --git a/tests/source/issue-6769.rs b/tests/source/issue-6769.rs new file mode 100644 index 00000000000..3a825e87ce8 --- /dev/null +++ b/tests/source/issue-6769.rs @@ -0,0 +1,39 @@ +// rustfmt-style_edition: 2027 + +fn test() { + let foo = [ + (b"Welcome to Ringboard!" as &[u8], &em), + ( + b"Ringboard is a fast, efficient, and composable clipboard manager for Linux." + , &em, + ), + ( + b"It supports both Wayland and X11 along with multiple clients to manage your \ + clipboard history." + , &em, + ), + ( + b"Clients include a standard GUI, an interactive TUI, and a CLI for all your scripting \ + needs." + , &em, + ), + ( + "Ringboard can copy arbitrary bytes—that includes images!".as_bytes(), + &em, + ), + ( + b"Plaintext and RegEx search are available for fast entry retrieval.", + &em, + ), + (b"Enjoy this image from our AI overlords:", &em), + ( + include_bytes!("../logo.jpeg") as &[u8], + &MimeType::from("image/jpeg").unwrap(), + ), + ( + b"Finally, it's worth mentioning that Ringboard is extremely efficient, performant, \ + and scalable." + , &em, + ), + ]; +} diff --git a/tests/source/issue-6778.rs b/tests/source/issue-6778.rs new file mode 100644 index 00000000000..7b6290d4f7d --- /dev/null +++ b/tests/source/issue-6778.rs @@ -0,0 +1,28 @@ +// rustfmt-style_edition: 2027 + +#![allow(dead_code)] + +struct Parameter { + required: bool, + description: &'static str, +} + +pub struct Test; + +impl Test { + fn parameters(&self) -> &'static [Parameter] { + &[ + Parameter { + required: true, + description: "Foo", + }, + Parameter { + required: false, + description: "Bar +This string is exactly 100 chars long. Delete one character to make it 99 chars long and it'll work!", + }, + ] + } +} + +fn main() {} diff --git a/tests/target/issue-6769.rs b/tests/target/issue-6769.rs new file mode 100644 index 00000000000..517e2c4b381 --- /dev/null +++ b/tests/target/issue-6769.rs @@ -0,0 +1,39 @@ +// rustfmt-style_edition: 2027 + +fn test() { + let foo = [ + (b"Welcome to Ringboard!" as &[u8], &em), + ( + b"Ringboard is a fast, efficient, and composable clipboard manager for Linux.", + &em, + ), + ( + b"It supports both Wayland and X11 along with multiple clients to manage your \ + clipboard history.", + &em, + ), + ( + b"Clients include a standard GUI, an interactive TUI, and a CLI for all your scripting \ + needs.", + &em, + ), + ( + "Ringboard can copy arbitrary bytes—that includes images!".as_bytes(), + &em, + ), + ( + b"Plaintext and RegEx search are available for fast entry retrieval.", + &em, + ), + (b"Enjoy this image from our AI overlords:", &em), + ( + include_bytes!("../logo.jpeg") as &[u8], + &MimeType::from("image/jpeg").unwrap(), + ), + ( + b"Finally, it's worth mentioning that Ringboard is extremely efficient, performant, \ + and scalable.", + &em, + ), + ]; +} diff --git a/tests/target/issue-6778.rs b/tests/target/issue-6778.rs new file mode 100644 index 00000000000..63188099633 --- /dev/null +++ b/tests/target/issue-6778.rs @@ -0,0 +1,28 @@ +// rustfmt-style_edition: 2027 + +#![allow(dead_code)] + +struct Parameter { + required: bool, + description: &'static str, +} + +pub struct Test; + +impl Test { + fn parameters(&self) -> &'static [Parameter] { + &[ + Parameter { + required: true, + description: "Foo", + }, + Parameter { + required: false, + description: "Bar +This string is exactly 100 chars long. Delete one character to make it 99 chars long and it'll work!", + }, + ] + } +} + +fn main() {}