Repository: rust-lang/rustfmt (rust)

Issue:
rustfmt always inserts newline before `else` in let-else statement with attribute
## Repro

This is a code rustfmt formatted.

```rust
fn main() {
    #[cfg(target_os = "linux")]
    let x = 42
    else {
        todo!()
    };

    // #[cfg(target_os = "linux")]
    let x = 42 else { todo!() };
}
```

With some attribute on a let-else statement, rustfmt always inserts newline before `else`. When removing the attribute, this doesn't happen and rustfmt works as intended.

## Expected behavior

My expectation is that rustfmt does not change formatting looking at the existence of attribute:

```rust
fn main() {
    #[cfg(target_os = "linux")]
    let x = 42 else { todo!() };

    // #[cfg(target_os = "linux")]
    let x = 42 else { todo!() };
}
```

## Environment

- rustfmt: 1.6.0-stable (5680fa18 2023-08-23)
  - Installed via rustup's stable toolchain
- Rust: 1.72.0
- OS: Windows 10

<!-- TRIAGEBOT_START -->

<!-- TRIAGEBOT_ASSIGN_START -->

<!-- TRIAGEBOT_ASSIGN_DATA_START$${"user":"rhysd"}$$TRIAGEBOT_ASSIGN_DATA_END -->

<!-- TRIAGEBOT_ASSIGN_END -->
<!-- TRIAGEBOT_END -->

Pull request:
Fix checking if newline is needed before `else` in let-else statement
Fixes #5901

Patch:
diff --git a/src/items.rs b/src/items.rs
index 4d82e192..48c37df8 100644
--- a/src/items.rs
+++ b/src/items.rs
@@ -75,6 +75,7 @@ impl Rewrite for ast::Local {
                 false,
             )?
         };
+        let let_kw_offset = result.len() - "let ".len();
 
         // 4 = "let ".len()
         let pat_shape = shape.offset_left(4)?;
@@ -127,8 +128,15 @@ impl Rewrite for ast::Local {
 
             if let Some(block) = else_block {
                 let else_kw_span = init.span.between(block.span);
+                // Strip attributes and comments to check if newline is needed before the else
+                // keyword from the initializer part. (#5901)
+                let init_str = if context.config.version() == Version::Two {
+                    &result[let_kw_offset..]
+                } else {
+                    result.as_str()
+                };
                 let force_newline_else = pat_str.contains('\n')
-                    || !same_line_else_kw_and_brace(&result, context, else_kw_span, nested_shape);
+                    || !same_line_else_kw_and_brace(init_str, context, else_kw_span, nested_shape);
                 let else_kw = rewrite_else_kw_with_comments(
                     force_newline_else,
                     true,
@@ -146,11 +154,16 @@ impl Rewrite for ast::Local {
                     std::cmp::min(shape.width, context.config.single_line_let_else_max_width());
 
                 // If available_space hits zero we know for sure this will be a multi-lined block
-                let available_space = max_width.saturating_sub(result.len());
+                let assign_str_with_else_kw = if context.config.version() == Version::Two {
+                    &result[let_kw_offset..]
+                } else {
+                    result.as_str()
+                };
+                let available_space = max_width.saturating_sub(assign_str_with_else_kw.len());
 
                 let allow_single_line = !force_newline_else
                     && available_space > 0
-                    && allow_single_line_let_else_block(&result, block);
+                    && allow_single_line_let_else_block(assign_str_with_else_kw, block);
 
                 let mut rw_else_block =
                     rewrite_let_else_block(block, allow_single_line, context, shape)?;


Test patch:
diff --git a/tests/source/let_else.rs b/tests/source/let_else.rs
index 85b3604a..cb2859e8 100644
--- a/tests/source/let_else.rs
+++ b/tests/source/let_else.rs
@@ -160,3 +160,23 @@ fn with_trailing_try_operator() {
     // 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_v2.rs b/tests/source/let_else_v2.rs
new file mode 100644
index 00000000..a420fbcf
--- /dev/null
+++ b/tests/source/let_else_v2.rs
@@ -0,0 +1,56 @@
+// rustfmt-version: Two
+// rustfmt-single_line_let_else_max_width: 100
+
+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.rs b/tests/target/let_else.rs
index 6554a096..f6560e85 100644
--- a/tests/target/let_else.rs
+++ b/tests/target/let_else.rs
@@ -252,3 +252,34 @@ fn with_trailing_try_operator() {
         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_v2.rs b/tests/target/let_else_v2.rs
new file mode 100644
index 00000000..b25ac160
--- /dev/null
+++ b/tests/target/let_else_v2.rs
@@ -0,0 +1,73 @@
+// rustfmt-version: Two
+// rustfmt-single_line_let_else_max_width: 100
+
+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;
+    };
+}