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?
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.
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
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));
}
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.
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));
};