Single-line if in match arm? (rustfmt)

Is there a specific length limit for match arms in rustfmt?

If have code like this:

    let num = match chars.next().unwrap_or('?') {
        '1' => 0,
        '2' => 2,
        '3' => if maj { 4 } else { 3 },
        // ... more cases
        other => return Err(InvalidStuff(c));
    }

But cargo fmt changes it like this, and does so even if I set single_line_if_else_max_width to a large value or use_small_heuristics = "max".

    let num = match chars.next().unwrap_or('?') {
        '1' => 0,
        '2' => 2,
        '3' => {
            if maj {
                4
            } else {
                3
            }
        }
       c => return Err(InvalidStuff(c));
    }

Outside of the match, a single line if is fine, even if I (artificially) make it longer:

    let _foo_bar_baz_value = if maj && true { 2 + 2 } else { 1 + 2 };

Is there anything I can do to get rustfmt to accept the single line if? Is this a bug in rustfmt, or is there a good reason for this behaviour?

Rust fmt is implementing the guidance from the rust style guide here.

https://doc.rust-lang.org/beta/style-guide/expressions.html#match

Do you refer to this?

If the body is a single expression with no line comments and not a control flow expression, start it on the same line as the left-hand side. If not, then it must be in a block.

I'm not sure why a control flow expression is mentioned as a reason for a block (maybe that can be changed?), but even if it is, I think the code should possibly be formatted as:

    let num = match chars.next().unwrap_or('?') {
        '1' => 0,
        '2' => 2,
        '3' => {
            if maj { 4 } else { 3 }
        }
        c => return Err(InvalidStuff(c));
    }

After reading Expressions - The Rust Style Guide I even think making the if itself multiline obscures the fact that it is in an expression context, as not beeing in an expression context would be a reson for it to not appear on a single line.

Maybe the match "hides" the expression context and that is the problem? If so, I still think it is a bug in rustfmt, as I can see the expression context.

Some more data; I can get rustfmt to agree on a single line like this:

    let num = match chars.next().unwrap_or('?') {
        '1' => 0,
        '2' => 2,
        '3' => maj.then_some(4).unwrap_or(3),
        // ... more cases
        other => return Err(InvalidStuff(c));
    }

But if I write it like that, clippy complains that an if .. else .. would be a clearer way to state my intent. And I fully agree on that.

There's also this rule: Expressions - The Rust Style Guide

And this issue: "if expression" in implicit return position (?) considered a statement · Issue #4351 · rust-lang/rustfmt · GitHub

I think it should format it as one line in a block according to the style guide, which rustfmt gets wrong[1]. But I also think the style guide was written assuming all control flow is multi-line, and should be changed to exempt single-line control flow (the only one is short ifs).

Semi-related, if you update to edition 2024, then this:

pub fn m(maj: bool) -> i32 {
    if maj {
        4
    } else {
        3
    }
}

Turns into this:

pub fn m(maj: bool) -> i32 {
    if maj { 4 } else { 3 }
}

No changes occur for the match.

Edit: if you add a comment it puts the if on one line in edition 2024

'3' => {
	// comment
	if maj { 4 } else { 3 }
}

So I'd say the multi-line if is definitely wrong.

Another one:

'3' => unsafe { if maj { 4 } else { 3 } },

And if you label the block, it does this:

'3' => 'a: {
	if maj { 4 } else { 3 }
}

  1. in edition 2024 ↩︎

1 Like

Thanks! After reading your example, I make a simplified "workaround" that I'm almost happy with:

    #[allow(unused_parens)]
    let num = match chars.next().unwrap_or('?') {
        '1' => 0,
        '2' => 2,
        '3' => (if maj { 4 } else { 3 }),
        // ... more cases
        other => return Err(InvalidStuff(c));
    }

:cowboy_hat_face:

Also, after reading about the change regarding return-pos conditional expression, I'm even more convinced that the code should format to the sinlge line variant in the first place.

1 Like

You can also use #[rustfmt::skip] in more complicated cases. Be weary. This is a big hammer and has a lot of its own problems.

    let num = match chars.next().unwrap_or('?') {
        '1' => 0,
        '2' => 2,
        #[rustfmt::skip]
        '3' => if maj { 4 } else { 3 },
        // ... more cases
        other => return Err(InvalidStuff(c));
    };