rustfmt icon indicating copy to clipboard operation
rustfmt copied to clipboard

Adding an attribute to an `unsafe` block changes its formatting

Open tgross35 opened this issue 1 year ago • 7 comments

This is the rustfmt result for an unsafe block:

fn main() {
    // #[cfg(not(fake_flag))]
    unsafe { println!() };
}

But uncomment the attribute, and it reformats it as:

fn main() {
    #[cfg(not(fake_flag))]
    unsafe {
        println!()
    };
}

Changing the attribute probably should not change the formatting.

rustfmt 1.7.0-nightly (5119208f 2024-03-02)

tgross35 avatar Mar 05 '24 10:03 tgross35

A similar issue has popped up in other cases too. #5901 and #5662 immediately come to mind, but there might be others in the backlog.

I haven't looked into this, but my hunch is that rustfmt considers the newline between the attribute and the block as an indication that the block can't be written on one line.

ytmimi avatar Mar 05 '24 14:03 ytmimi

I'm assuming this affects all blocks, not just unsafe blocks? What about async blocks for example?

ytmimi avatar Mar 05 '24 14:03 ytmimi

Good call, looks like async and probably others are the same

fn main() {
    // #[cfg(not(fake_flag))]
    async { println!() };
}
fn main() {
    #[cfg(not(fake_flag))]
    async {
        println!()
    };
}

Probably indeed the same issue as #5662 so close this if you see fit. I just noticed because the diff for https://github.com/rust-lang/rust/pull/121894 looked kind of funny :)

tgross35 avatar Mar 05 '24 17:03 tgross35

I think its the same underlying problem as #5662, but it's occurring in a different parts of the codebase. rustfmt rewrites attrs before rewriting the AST node those attrs are associated with so I think this is a distinct issue to the others I linked.

ytmimi avatar Mar 05 '24 18:03 ytmimi

I did a little more digging into this one. I don't know if this is actually a bug, and I'm no longer convinced this is related to https://github.com/rust-lang/rustfmt/issues/5662. It seems that it might be intentional behavior. I've tracked this down to is_simple_block, which returns false if the block has attributes.

https://github.com/rust-lang/rustfmt/blob/fbe04244f8256b6dad38d8bcaac96c8a3a754a04/src/expr.rs#L1219-L1228

It's not clear to me why that's the case. It seems that this logic was introduced in #2075 and merged in 0dca70f29004d107d3e707b667aca2b8e6f331a6.

If we wanted to change that behavior we'd need to gate the change.

ytmimi avatar Aug 17 '24 02:08 ytmimi

Ah, thanks for the history. I have to imagine this specific case wasn't intentional - https://github.com/rust-lang/rustfmt/issues/2073 and all the tests added at https://github.com/rust-lang/rustfmt/commit/0dca70f29004d107d3e707b667aca2b8e6f331a6#diff-56f48f88e4ce65cd696cc52c563399f2f751aaab2dfa3f8cd51a582b19e77070 only relate to adding attributes within let expressions. There isn't anything there for unsafe blocks without an expression, or a let (let bindings do not have the problem, see the below example).

This seems worth fixing to me. I have run into this a few times since writing this issue and it is always surprising that a noninvasive attribute affects the formatting of the thing it applies to, and that commenting the line affects the formatting.

I think an argument could be made that this is a bugfix so doesn't require gating - at least, it doesn't seem worth supporting the current behavior forever. This pattern also seems relatively rare, only a handful of uses in std https://github.com/search?q=repo%3Arust-lang%2Frust+%2F%5E%5Cs*%23%5C%5B.%5Cn%5Cs%28async%7Cconst%7Cunsafe%29+%5C%7B%28.%5Cn%29%7B2%7D%5Cs%5C%7D%2F+lang%3Arust&type=code.

Formatted example, for reference:

/* Block with `unsafe` / `async` / `const` */

// Problematic example
#[cfg(target_os = "linux")]
unsafe {
    println!("hi")
};

// Commenting the line affects the rest of the block
// #[cfg(target_os = "linux")]
unsafe { println!("hi") };

// Attribute has no effect for `let` expressions
#[cfg(not(target_os = "linux"))]
let x = unsafe { println!("hi") };

// Attribute has no effect for `let` expressions
// #[cfg(not(target_os = "linux"))]
let x = unsafe { println!("hi") };

/* Block without `unsafe` / `async` / `const` */

// Formatting with the attribute...
#[cfg(target_os = "linux")]
{
    println!("hi")
};

// ...is exactly the same as formatting without the attribute
// #[cfg(target_os = "linux")]
{
    println!("hi")
};

// Attribute has no effect for `let` expressions
#[cfg(not(target_os = "linux"))]
let x = { println!("hi") };

// Attribute has no effect for `let` expressions
// #[cfg(not(target_os = "linux"))]
let x = { println!("hi") };

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=55737cc7296d7c56d227939fca3bb669

tgross35 avatar Aug 17 '24 07:08 tgross35

Because of rustfmt's stability guarantee we'd need to gate this change since changing

#[cfg(target_os = "linux")]
unsafe {
    println!("hi")
};

to

#[cfg(target_os = "linux")]
unsafe { println!("hi")};

would be a breaking formatting change.

ytmimi avatar Aug 17 '24 15:08 ytmimi