From 04f3a60855c9ac4d2bdb7440dd38e66a675f3bf5 Mon Sep 17 00:00:00 2001 From: Jane Losare-Lusby Date: Fri, 19 Dec 2025 11:58:28 -0800 Subject: [PATCH] add flag to force newlines in let else statements --- Configurations.md | 52 ++++ src/config/mod.rs | 4 + src/config/options.rs | 1 + src/items.rs | 3 +- tests/source/let_else_force_newline.rs | 182 +++++++++++++ tests/source/let_else_force_newline_v2.rs | 56 ++++ tests/target/let_else_force_newline.rs | 308 ++++++++++++++++++++++ tests/target/let_else_force_newline_v2.rs | 87 ++++++ 8 files changed, 692 insertions(+), 1 deletion(-) create mode 100644 tests/source/let_else_force_newline.rs create mode 100644 tests/source/let_else_force_newline_v2.rs create mode 100644 tests/target/let_else_force_newline.rs create mode 100644 tests/target/let_else_force_newline_v2.rs diff --git a/Configurations.md b/Configurations.md index c6bcd29927c..e473d938994 100644 --- a/Configurations.md +++ b/Configurations.md @@ -1731,6 +1731,58 @@ use core::slice; #[cfg(feature = "alloc")] use core::slice; ``` +## `let_else_force_newline` + +Force `else` branch of a `let_else` expression to always come after a newline. + +- **Default value**: `false` +- **Possible values**: `true`, `false` +- **Stable**: No (Tracking issue: [TODO](todo)) + +#### `false` (default): + +```rust +fn main() { + let Some(w) = opt else { return Ok(()) }; + + let Some(x) = opt else { return }; + + let Some(y) = opt else { + return; + }; + + let Some(z) = some_very_very_very_very_long_name else { + return; + }; +} +``` + +#### `true`: + +```rust +fn main() { + let Some(w) = opt + else { + return Ok(()); + }; + + let Some(x) = opt + else { + return; + }; + + let Some(y) = opt + else { + return; + }; + + let Some(z) = some_very_very_very_very_long_name + else { + return; + }; +} +``` + ## `match_arm_blocks` Controls whether arm bodies are wrapped in cases where the first line of the body cannot fit on the same line as the `=>` operator. diff --git a/src/config/mod.rs b/src/config/mod.rs index 04a4e89dee7..14c8b3433c6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -161,6 +161,8 @@ create_config! { format_generated_files: FormatGeneratedFiles, false, "Format generated files"; generated_marker_line_search_limit: GeneratedMarkerLineSearchLimit, false, "Number of lines to \ check for a `@generated` marker when `format_generated_files` is enabled"; + let_else_force_newline: LetElseForceNewline, true, "Always put the else branch of let_else \ + statements on a new line."; // Options that can change the source code beyond whitespace/blocks (somewhat linty things) merge_derives: MergeDerives, true, "Merge multiple `#[derive(...)]` into a single one"; @@ -820,6 +822,7 @@ version = "One" inline_attribute_width = 0 format_generated_files = true generated_marker_line_search_limit = 5 +let_else_force_newline = false merge_derives = true use_try_shorthand = false use_field_init_shorthand = false @@ -912,6 +915,7 @@ version = "Two" inline_attribute_width = 0 format_generated_files = true generated_marker_line_search_limit = 5 +let_else_force_newline = false merge_derives = true use_try_shorthand = false use_field_init_shorthand = false diff --git a/src/config/options.rs b/src/config/options.rs index 00f9c3f7ec1..001a77c78bd 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -683,6 +683,7 @@ config_option_with_style_edition_default!( InlineAttributeWidth, usize, _ => 0; FormatGeneratedFiles, bool, _ => true; GeneratedMarkerLineSearchLimit, usize, _ => 5; + LetElseForceNewline, bool, _ => false; // Options that can change the source code beyond whitespace/blocks (somewhat linty things) MergeDerives, bool, _ => true; diff --git a/src/items.rs b/src/items.rs index 0e814644304..005545e10bb 100644 --- a/src/items.rs +++ b/src/items.rs @@ -144,7 +144,8 @@ impl Rewrite for ast::Local { result.as_str() }; let force_newline_else = pat_str.contains('\n') - || !same_line_else_kw_and_brace(init_str, context, else_kw_span, nested_shape); + || !same_line_else_kw_and_brace(init_str, context, else_kw_span, nested_shape) + || context.config.let_else_force_newline(); let else_kw = rewrite_else_kw_with_comments( force_newline_else, true, diff --git a/tests/source/let_else_force_newline.rs b/tests/source/let_else_force_newline.rs new file mode 100644 index 00000000000..4da89457c5a --- /dev/null +++ b/tests/source/let_else_force_newline.rs @@ -0,0 +1,182 @@ +// rustfmt-let_else_force_newline: true + +fn main() { + // Although this won't compile it still parses so make sure we can format empty else blocks + let Some(x) = opt else {}; + + // let-else may be formatted on a single line if they are "short" + // and only contain a single expression + let Some(x) = opt else { return }; + + let Some(x) = opt else { + return + }; + + let Some(x) = opt else { return; }; + + let Some(x) = opt else { + // nope + return; + }; + + let Some(x) = opt else { let y = 1; return y }; + + let Some(x) = y.foo("abc", fairly_long_identifier, "def", "123456", "string", "cheese") else { bar() }; + + let Some(x) = abcdef().foo("abc", some_really_really_really_long_ident, "ident", "123456").bar().baz().qux("fffffffffffffffff") else { foo_bar() }; +} + +fn with_comments_around_else_keyword() { + let Some(x) = opt /* pre else keyword block-comment */ else { return }; + + let Some(x) = opt else /* post else keyword block-comment */ { return }; + + let Some(x) = opt /* pre else keyword block-comment */ else /* post else keyword block-comment */ { return }; + + let Some(x) = opt // pre else keyword line-comment + else { return }; + + let Some(x) = opt else + // post else keyword line-comment + { return }; + + let Some(x) = opt // pre else keyword line-comment + else + // post else keyword line-comment + { return }; + +} + +fn unbreakable_initializer_expr_pre_formatting_let_else_length_near_max_width() { + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 100 (max_width) + // Post Formatting: + // The formatting is left unchanged! + let Some(x) = some_really_really_really_really_really_really_really_long_name_A else { return }; + + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 100 (max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_really_really_really_long_name___B else {return}; + + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 100 (max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_long_name_____C else {some_divergent_function()}; + + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 101 (> max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_really_really_really_long_name__D else { return }; +} + +fn unbreakable_initializer_expr_pre_formatting_length_up_to_opening_brace_near_max_width() { + // Pre Formatting: + // The length of `(indent)let pat = init else {` is 99 (< max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_really_really_really_really_long_name___E else {return}; + + // Pre Formatting: + // The length of `(indent)let pat = init else {` is 101 (> max_width) + // Post Formatting: + // The else keyword and opening brace cannot fit on the same line as the initializer expr. + // They are formatted on the next line. + let Some(x) = some_really_really_really_really_really_really_really_really_long_name_____F else {return}; +} + +fn unbreakable_initializer_expr_pre_formatting_length_through_initializer_expr_near_max_width() { + // Pre Formatting: + // The length of `(indent)let pat = init` is 99 (< max_width) + // Post Formatting: + // The else keyword and opening brace cannot fit on the same line as the initializer expr. + // They are formatted on the next line. + let Some(x) = some_really_really_really_really_really_really_really_really_really_long_name___G else {return}; + + // Pre Formatting: + // The length of `(indent)let pat = init` is 100 (max_width) + // Post Formatting: + // Break after the `=` and put the initializer expr on it's own line. + // Because the initializer expr is multi-lined the else is placed on it's own line. + let Some(x) = some_really_really_really_really_really_really_really_really_really_long_name____H else {return}; + + // Pre Formatting: + // The length of `(indent)let pat = init` is 109 (> max_width) + // Post Formatting: + // Break after the `=` and put the initializer expr on it's own line. + // Because the initializer expr is multi-lined the else is placed on it's own line. + // The initializer expr has a length of 91, which when indented on the next line + // The `(indent)init` line has a length of 99. This is the max length that the `init` can be + // before we start running into max_width issues. I suspect this is because the shape is + // accounting for the `;` at the end of the `let-else` statement. + let Some(x) = some_really_really_really_really_really_really_really_really_really_really_long_name______I else {return}; + + // Pre Formatting: + // The length of `(indent)let pat = init` is 110 (> max_width) + // Post Formatting: + // Max length issues prevent us from formatting. + // The initializer expr has a length of 92, which if it would be indented on the next line + // the `(indent)init` line has a length of 100 which == max_width of 100. + // One might expect formatting to succeed, but I suspect the reason we hit max_width issues is + // because the Shape is accounting for the `;` at the end of the `let-else` statement. + let Some(x) = some_really_really_really_really_really_really_really_really_really_really_really_long_nameJ else {return}; +} + +fn long_patterns() { + let Foo {x: Bar(..), y: FooBar(..), z: Baz(..)} = opt else { + return; + }; + + // with version=One we don't wrap long array patterns + let [aaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, cccccccccccccccccc, dddddddddddddddddd] = opt else { + return; + }; + + let ("aaaaaaaaaaaaaaaaaaa" | "bbbbbbbbbbbbbbbbb" | "cccccccccccccccccccccccc" | "dddddddddddddddd" | "eeeeeeeeeeeeeeee") = opt else { + return; + }; + + let Some(Ok((Message::ChangeColor(super::color::Color::Rgb(r, g, b)), Point { x, y, z }))) = opt else { + return; + }; +} + +fn with_trailing_try_operator() { + // Currently the trailing ? forces the else on the next line + // This may be revisited in style edition 2024 + let Some(next_bucket) = ranking_rules[cur_ranking_rule_index].next_bucket(ctx, logger, &ranking_rule_universes[cur_ranking_rule_index])? else { return }; + + // Maybe this is a workaround? + let Ok(Some(next_bucket)) = ranking_rules[cur_ranking_rule_index].next_bucket(ctx, logger, &ranking_rule_universes[cur_ranking_rule_index]) else { return }; +} + +fn issue5901() { + #[cfg(target_os = "linux")] + let Some(x) = foo else { todo!() }; + + #[cfg(target_os = "linux")] + // Some comments between attributes and let-else statement + let Some(x) = foo else { todo!() }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = foo else { todo!() }; + + // The else block will be multi-lined because attributes and comments before `let` + // are included when calculating max width + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + // Some comments between attributes and let-else statement + let Some(x) = foo() else { todo!() }; +} diff --git a/tests/source/let_else_force_newline_v2.rs b/tests/source/let_else_force_newline_v2.rs new file mode 100644 index 00000000000..b805ce971f9 --- /dev/null +++ b/tests/source/let_else_force_newline_v2.rs @@ -0,0 +1,56 @@ +// rustfmt-style_edition: 2024 +// rustfmt-let_else_force_newline: true + +fn issue5901() { + #[cfg(target_os = "linux")] + let Some(x) = foo else { todo!() }; + + #[cfg(target_os = "linux")] + // Some comments between attributes and let-else statement + let Some(x) = foo else { todo!() }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = foo else { todo!() }; + + // The else block is multi-lined + #[cfg(target_os = "linux")] + let Some(x) = foo else { return; }; + + // The else block will be single-lined because attributes and comments before `let` + // are no longer included when calculating max width + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + // Some comments between attributes and let-else statement + let Some(x) = foo else { todo!() }; + + // Some more test cases for v2 formatting with attributes + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = opt + // pre else keyword line-comment + else { return; }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = opt else + // post else keyword line-comment + { return; }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Foo {x: Bar(..), y: FooBar(..), z: Baz(..)} = opt else { + return; + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(Ok((Message::ChangeColor(super::color::Color::Rgb(r, g, b)), Point { x, y, z }))) = opt else { + return; + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = very_very_very_very_very_very_very_very_very_very_very_very_long_expression_in_assign_rhs() else { return; }; +} diff --git a/tests/target/let_else_force_newline.rs b/tests/target/let_else_force_newline.rs new file mode 100644 index 00000000000..e667893099f --- /dev/null +++ b/tests/target/let_else_force_newline.rs @@ -0,0 +1,308 @@ +// rustfmt-let_else_force_newline: true + +fn main() { + // Although this won't compile it still parses so make sure we can format empty else blocks + let Some(x) = opt + else {}; + + // let-else may be formatted on a single line if they are "short" + // and only contain a single expression + let Some(x) = opt + else { + return; + }; + + let Some(x) = opt + else { + return; + }; + + let Some(x) = opt + else { + return; + }; + + let Some(x) = opt + else { + // nope + return; + }; + + let Some(x) = opt + else { + let y = 1; + return y; + }; + + let Some(x) = y.foo( + "abc", + fairly_long_identifier, + "def", + "123456", + "string", + "cheese", + ) + else { + bar() + }; + + let Some(x) = abcdef() + .foo( + "abc", + some_really_really_really_long_ident, + "ident", + "123456", + ) + .bar() + .baz() + .qux("fffffffffffffffff") + else { + foo_bar() + }; +} + +fn with_comments_around_else_keyword() { + let Some(x) = opt + /* pre else keyword block-comment */ + else { + return; + }; + + let Some(x) = opt + else + /* post else keyword block-comment */ + { + return; + }; + + let Some(x) = opt + /* pre else keyword block-comment */ + else + /* post else keyword block-comment */ + { + return; + }; + + let Some(x) = opt + // pre else keyword line-comment + else { + return; + }; + + let Some(x) = opt + else + // post else keyword line-comment + { + return; + }; + + let Some(x) = opt + // pre else keyword line-comment + else + // post else keyword line-comment + { + return; + }; +} + +fn unbreakable_initializer_expr_pre_formatting_let_else_length_near_max_width() { + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 100 (max_width) + // Post Formatting: + // The formatting is left unchanged! + let Some(x) = some_really_really_really_really_really_really_really_long_name_A + else { + return; + }; + + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 100 (max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_really_really_really_long_name___B + else { + return; + }; + + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 100 (max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_long_name_____C + else { + some_divergent_function() + }; + + // Pre Formatting: + // The length of `(indent)let pat = init else block;` is 101 (> max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_really_really_really_long_name__D + else { + return; + }; +} + +fn unbreakable_initializer_expr_pre_formatting_length_up_to_opening_brace_near_max_width() { + // Pre Formatting: + // The length of `(indent)let pat = init else {` is 99 (< max_width) + // Post Formatting: + // The else keyword and opening brace remain on the same line as the initializer expr, + // and the else block is formatted over multiple lines because we can't fit the + // else block on the same line as the initializer expr. + let Some(x) = some_really_really_really_really_really_really_really_really_long_name___E + else { + return; + }; + + // Pre Formatting: + // The length of `(indent)let pat = init else {` is 101 (> max_width) + // Post Formatting: + // The else keyword and opening brace cannot fit on the same line as the initializer expr. + // They are formatted on the next line. + let Some(x) = some_really_really_really_really_really_really_really_really_long_name_____F + else { + return; + }; +} + +fn unbreakable_initializer_expr_pre_formatting_length_through_initializer_expr_near_max_width() { + // Pre Formatting: + // The length of `(indent)let pat = init` is 99 (< max_width) + // Post Formatting: + // The else keyword and opening brace cannot fit on the same line as the initializer expr. + // They are formatted on the next line. + let Some(x) = some_really_really_really_really_really_really_really_really_really_long_name___G + else { + return; + }; + + // Pre Formatting: + // The length of `(indent)let pat = init` is 100 (max_width) + // Post Formatting: + // Break after the `=` and put the initializer expr on it's own line. + // Because the initializer expr is multi-lined the else is placed on it's own line. + let Some(x) = + some_really_really_really_really_really_really_really_really_really_long_name____H + else { + return; + }; + + // Pre Formatting: + // The length of `(indent)let pat = init` is 109 (> max_width) + // Post Formatting: + // Break after the `=` and put the initializer expr on it's own line. + // Because the initializer expr is multi-lined the else is placed on it's own line. + // The initializer expr has a length of 91, which when indented on the next line + // The `(indent)init` line has a length of 99. This is the max length that the `init` can be + // before we start running into max_width issues. I suspect this is because the shape is + // accounting for the `;` at the end of the `let-else` statement. + let Some(x) = + some_really_really_really_really_really_really_really_really_really_really_long_name______I + else { + return; + }; + + // Pre Formatting: + // The length of `(indent)let pat = init` is 110 (> max_width) + // Post Formatting: + // Max length issues prevent us from formatting. + // The initializer expr has a length of 92, which if it would be indented on the next line + // the `(indent)init` line has a length of 100 which == max_width of 100. + // One might expect formatting to succeed, but I suspect the reason we hit max_width issues is + // because the Shape is accounting for the `;` at the end of the `let-else` statement. + let Some(x) = some_really_really_really_really_really_really_really_really_really_really_really_long_nameJ else {return}; +} + +fn long_patterns() { + let Foo { + x: Bar(..), + y: FooBar(..), + z: Baz(..), + } = opt + else { + return; + }; + + // with version=One we don't wrap long array patterns + let [aaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, cccccccccccccccccc, dddddddddddddddddd] = opt + else { + return; + }; + + let ("aaaaaaaaaaaaaaaaaaa" + | "bbbbbbbbbbbbbbbbb" + | "cccccccccccccccccccccccc" + | "dddddddddddddddd" + | "eeeeeeeeeeeeeeee") = opt + else { + return; + }; + + let Some(Ok((Message::ChangeColor(super::color::Color::Rgb(r, g, b)), Point { x, y, z }))) = + opt + else { + return; + }; +} + +fn with_trailing_try_operator() { + // Currently the trailing ? forces the else on the next line + // This may be revisited in style edition 2024 + let Some(next_bucket) = ranking_rules[cur_ranking_rule_index].next_bucket( + ctx, + logger, + &ranking_rule_universes[cur_ranking_rule_index], + )? + else { + return; + }; + + // Maybe this is a workaround? + let Ok(Some(next_bucket)) = ranking_rules[cur_ranking_rule_index].next_bucket( + ctx, + logger, + &ranking_rule_universes[cur_ranking_rule_index], + ) + else { + return; + }; +} + +fn issue5901() { + #[cfg(target_os = "linux")] + let Some(x) = foo + else { + todo!() + }; + + #[cfg(target_os = "linux")] + // Some comments between attributes and let-else statement + let Some(x) = foo + else { + todo!() + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = foo + else { + todo!() + }; + + // The else block will be multi-lined because attributes and comments before `let` + // are included when calculating max width + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + // Some comments between attributes and let-else statement + let Some(x) = foo() + else { + todo!() + }; +} diff --git a/tests/target/let_else_force_newline_v2.rs b/tests/target/let_else_force_newline_v2.rs new file mode 100644 index 00000000000..1de6bc5cc78 --- /dev/null +++ b/tests/target/let_else_force_newline_v2.rs @@ -0,0 +1,87 @@ +// rustfmt-style_edition: 2024 +// rustfmt-let_else_force_newline: true + +fn issue5901() { + #[cfg(target_os = "linux")] + let Some(x) = foo + else { + todo!() + }; + + #[cfg(target_os = "linux")] + // Some comments between attributes and let-else statement + let Some(x) = foo + else { + todo!() + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = foo + else { + todo!() + }; + + // The else block is multi-lined + #[cfg(target_os = "linux")] + let Some(x) = foo + else { + return; + }; + + // The else block will be single-lined because attributes and comments before `let` + // are no longer included when calculating max width + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + // Some comments between attributes and let-else statement + let Some(x) = foo + else { + todo!() + }; + + // Some more test cases for v2 formatting with attributes + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = opt + // pre else keyword line-comment + else { + return; + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = opt + else + // post else keyword line-comment + { + return; + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Foo { + x: Bar(..), + y: FooBar(..), + z: Baz(..), + } = opt + else { + return; + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(Ok((Message::ChangeColor(super::color::Color::Rgb(r, g, b)), Point { x, y, z }))) = + opt + else { + return; + }; + + #[cfg(target_os = "linux")] + #[cfg(target_arch = "x86_64")] + let Some(x) = + very_very_very_very_very_very_very_very_very_very_very_very_long_expression_in_assign_rhs() + else { + return; + }; +}