Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rustfmt formats different arms of the same match inconsistently #3995

Open
RalfJung opened this issue Jan 3, 2020 · 19 comments
Open

rustfmt formats different arms of the same match inconsistently #3995

RalfJung opened this issue Jan 3, 2020 · 19 comments
Labels
a-matches match arms, patterns, blocks, etc p-medium poor-formatting

Comments

@RalfJung
Copy link
Member

RalfJung commented Jan 3, 2020

The big rustc format made some code I care about much less readable. Here's an example:

-            ref mut local @ LocalValue::Live(Operand::Immediate(_)) |
-            ref mut local @ LocalValue::Uninitialized => {
-                Ok(Ok(local))
-            }
+            ref mut local @ LocalValue::Live(Operand::Immediate(_))
+            | ref mut local @ LocalValue::Uninitialized => Ok(Ok(local)),

The actual code running here for this branch got moved onto the same line as the last line of a pattern. That makes it quite hard to visually identify this code -- compared to the old version of the code, the visual separation of pattern and code got lost.

@RalfJung
Copy link
Member Author

RalfJung commented Jan 3, 2020

More generally, I disagree with the way rustfmt eagerly turns

pat =>
    very_long_expr,
pat =>
    short_expr

into

pat =>
    very_long_expr,
pat => short_expr

and it also turns

pat => {
  stmt;
  expr
}
pat => {
  expr
}

into

pat => {
  stmt;
  expr
}
pat => expr
// or expr might be on its own line if it is long enough

In a long match, I think it is a good idea to consistently either have pattern and expression on the same line, or on different lines, for all arms in that match. The visual similarities make it much easier to parse this code. But unfortunately, running rustfmt on such manually formatted code turns something symmetric into an inconsistent chaos. One really bad example is this: before -> after. (Yes, the write! arguments were not formatted properly before, and I am totally fine with those being formatted like rustfmt does. This is about the match arms, not the write! arguments.)

IMO, rustfmt should honor my choice to start expr in a new line, and never remove that newline. That would make it maintain good manual formatting, unlike now where it actively makes code less consistently formatted.

Alternatively, if you want to enforce some rule, my proposal would be to only ever have expr on the same line as pat if we can do this for every branch of the match (so all branches have just one pattern, and the expression is short enough). That would ensure consistency.

@RalfJung RalfJung changed the title Don't put code into the line after => after a multi-line pattern rustfmt puts too much code into the line after => Jan 3, 2020
@RalfJung
Copy link
Member Author

RalfJung commented Jan 3, 2020

(Moved to separate issue: #4004)

original comment

Ah, and I just realized that yet another formatting choice of rustfmt that I strongly dislike is also related: rustfmt will turn

pat =>
    fun(
        a,
        b,
        c,
        d,
    )

into

pat => fun(
    a,
    b,
    c,
    d,
)

I think this is a total no-go. In a long match, like for example this, that means that some of the indented lines are statements but some are just function arguments. When skimming that match, it is very easy to miss the fact that there is a function call after the =>, and then nothing makes any sense.

We also don't generate code like this

if foo { fun(
    a,
    b,
    c,
    d,
)}

so why is it okay to do the equivalent things for match arms?

@RalfJung
Copy link
Member Author

RalfJung commented Jan 3, 2020

(Moved to separate issue: #4005)

original comment

Another way in which rustfmt introduces inconsistency into long match is by putting multiple patterns on the same line, so we end up with matches like this:

        match ty.kind {
            // Types without identity.
            ty::Bool
            | ty::Char
            | ty::Int(_)
            | ty::Uint(_)
            | ty::Float(_)
            | ty::Str
            | ty::Array(_, _)
            | ty::Slice(_)
            | ty::RawPtr(_)
            | ty::Ref(_, _, _)
            | ty::FnPtr(_)
            | ty::Never
            | ty::Tuple(_)
            | ty::Dynamic(_, _) => self.pretty_print_type(ty),

            // Placeholders (all printed as `_` to uniformize them).
            ty::Param(_) | ty::Bound(..) | ty::Placeholder(_) | ty::Infer(_) | ty::Error => {
                write!(self, "_")?;
                Ok(self)
            }

Before rustfmt ran, this was neatly formatted with one pattern per line and never mixing patterns and code on the same line. Now it's all over the place. Should I open a separate bug for this?

@RalfJung
Copy link
Member Author

RalfJung commented Jan 3, 2020

Here's an example diff of what rustfmt does where I disagree with every single change it is making: rust-lang/rust@ff5b102. These all introduce inconsistencies in match formatting.

And here's how I manually formatted a long match, I had to disable rustfmt for that as there seems to be no way to make rustfmt not destroy the formatting: rust-lang/rust@e44f3e7

@varkor
Copy link
Member

varkor commented Jan 4, 2020

Regarding the third comment, isn't this incorrect behaviour as per the specification?

I was under the impression that

pat =>
    fun(
        a,
        b,
        c,
        d,
    )

should be reformatted into

pat => {
    fun(
        a,
        b,
        c,
        d,
    )
}

as it's not a single-line expression.

@RalfJung
Copy link
Member Author

RalfJung commented Jan 8, 2020

@varkor good catch, I don't see any justification for putting the first line of a multi-line expression onto the line of the =>. I moved that to a separate issue: #4004.

@RalfJung
Copy link
Member Author

RalfJung commented Jan 8, 2020

Turns out this was already a concern back when the formatting got implemented:

we should consider wrapping either all arms in blocks or none to improve consistency within a match. Looking at the current results, I think this makes sense. It's not implemented in this PR, but I think we should have an option for this at least.

AFAIK, no such option exists yet. And indeed I agree entirely. I'd maybe add one thing: even when none of the branches are wrapped, there's still the choice of putting the expression on the same line as the =>, or into a new line. The simplest solution would be to always put it on a new line -- I think that would overall be an improvement today, though some matches would suffer. If we go with having two styles of non-wrapped match arms, then that, too, should be consistent within a match: either all arms should use the line of the =>, or none of them should.

@RalfJung RalfJung changed the title rustfmt puts too much code into the line after => rustfmt formats different arms of the same match inconsistently Jan 8, 2020
@RalfJung
Copy link
Member Author

RalfJung commented Jan 8, 2020

I moved formatting of pattern alternatives to a separate issue: #4005. So this one is now just about making the body of the various match arms of the same match more consistent.

@RalfJung
Copy link
Member Author

Here's another example of what I consider bad match formatting:

                        let kind = match rvalue {
                            Rvalue::Ref(_, borrow_kind, _)
                                if borrow_kind.allows_two_phase_borrow() =>
                            {
                                RetagKind::TwoPhase
                            }
                            Rvalue::AddressOf(..) => RetagKind::Raw,
                            _ => RetagKind::Default,
                        };

Notice how all match arms are just an enum constructor, but one of them gets curly braces forcibly added around it.

@scottmcm
Copy link
Member

if we can do this for every branch of the match

👍

This is my core complaint with rustfmt, I think. It seems to regularly lose consistency between items in sequences, be that array literals, function arguments, match arms, whatever. I'd definitely like to see it make consistent choices across them, rather than the ones that today seem to be made in isolation.

@stephanemagnenat
Copy link

Any update on that? Looking at the close issue above, it seems that the question is more at the level of the style guide rather than in the implementation of rustfmt, but nonetheless as a user I find it very annoying that different arms of match get formatted differently: this breaks symmetry and leads to less readable code IMHO.

@tgross35
Copy link
Contributor

tgross35 commented Apr 3, 2023

Just to add a differing viewpoint - I actually prefer in many cases that rustfmt does what it currently does re: making some statements on one line vs. making all multiline. For things like Display impls on larger enums, it often seems like there are only one or two long/multiline blocks, and the rest fit on one. If having a single multiline block forced all to go multiline, vertical space of the match statement is effectively tripled.

https://github.com/pluots/zspell/blob/71ae77932f2cc67f593d846a26a9ffd7cf6d0412/crates/zspell/src/affix.rs#L276-L367 for example, I agree with what rustfmt does. 61 matches fit on 91 lines this way, but forcing them to braced multiline would make the total double to 183 lines. (could have been smaller if I used use Enum::*, but that's beside the point).

My personal rule would be that the default should be whichever matches 50% of the statements; if a majority fit on one line, make that the default and have others be an exception. If a majority take >1 line, then make them all be that way. Now of course that can't actually be a rule (code churn...) But I don't think that 1 or 2 multiline expressions should change entire large match statements (I guess that would cause churn too).

Haven't seen this discussed really though, so maybe it's not in the question anymore. Just wanted to add some 2¢.

@TheDudeFromCI
Copy link

This would be a nice feature. Match blocks have always been the one feature about rust formatting that always felt off.

@RalfJung
Copy link
Member Author

I recently had a situation where I changed an enum variant and that changed formatting of all the other enum variants as well. Does that mean rustfmt already has the ability to take other variants/arms into account when formatting? I am not sure if the behavior I saw was a bug or precedent for doing something similar with match arms as well.

@luisschwab
Copy link

luisschwab commented Oct 17, 2024

Psycho behaviour by rustfmt:

let (network_magic, port, seeds, filename) = match &args.network {
    Some(network) => match network.as_str() {
        "mainnet" => (MAGIC_MAINNET, PORT_MAINNET, SEEDS_MAINNET, "mainnet-nodes.txt"),
        "testnet" => (MAGIC_TESTNET,PORT_TESTNET,SEEDS_TESTNET, "testnet-nodes.txt"),
        "signet" => (MAGIC_SIGNET, PORT_SIGNET, SEEDS_SIGNET, "signet-nodes.txt"),
        "regtest" => (MAGIC_REGTEST,PORT_REGTEST, &["localhost"][..], "regtest-nodes.txt"),
        _ => std::process::exit(1),
    },
    None => (MAGIC_MAINNET,PORT_MAINNET,SEEDS_MAINNET,"mainnet-nodes.txt"),
};

becomes:

let (network_magic, port, seeds, filename) = match &args.network {
    Some(network) => match network.as_str() {
        "mainnet" => (
            MAGIC_MAINNET,
            PORT_MAINNET,
            SEEDS_MAINNET,
            "mainnet-nodes.txt",
        ),
        "testnet" => (
            MAGIC_TESTNET,
            PORT_TESTNET,
            SEEDS_TESTNET,
            "testnet-nodes.txt",
        ),
        "signet" => (MAGIC_SIGNET, PORT_SIGNET, SEEDS_SIGNET, "signet-nodes.txt"),
        "regtest" => (
            MAGIC_REGTEST,
            PORT_REGTEST,
            &["localhost"][..],
            "regtest-nodes.txt",
        ),
        _ => std::process::exit(1),
    },
    None => (
        MAGIC_MAINNET,
        PORT_MAINNET,
        SEEDS_MAINNET,
        "mainnet-nodes.txt",
    ),
};

@calebcartwright
Copy link
Member

Psycho behaviour by rustfmt:

This seems like an unnecessary and unhelpful characterization. Your snippet isn't complete and directly reproducible so I'll assume you are running rustfmt with the default configuration values, but the behavior you are complaining about isn't related to match expression formatting regardless.

To demonstrate, let's show the tuple without any match context:

fn foo() {
    let x = (MAGIC_TESTNET, PORT_TESTNET, SEEDS_TESTNET, "testnet-nodes.txt");
}

And then let's consider the relevant rules dictated by the Rust Style Guide
The rules for tuples state:

Build a tuple or tuple struct as you would call a function.

So we look at the rules for function calls:

If the function call is not small, it would otherwise over-run the max width, or any argument or the callee is multi-line, then format the call across multiple lines.

The threshold for whether a function call (or tuple) qualifies as "small" is controlled by the fn_call_width setting that has a default value of 60. The tuple in this example, and most of the tuples you had in your match arms are all longer than 60, and are therefore formatted as multilines.

The Style Guide rules for match expressions does not have a clause that says "but ignore all the other rules in this style guide to keep visual consistency across match arms".

There is nothing "psycho" here.. rustfmt is simply, and correctly, applying the Style Guide rules for tuples

@stephanemagnenat
Copy link

Is there a process to propose amending the style guide? I can imagine something like saying that all match arms should be of the formatting style of the biggest of them, in order to ensure consistency.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 18, 2024

I have opened an issue in the style-team repo: rust-lang/style-team#196.
However, I don't know how active the style team is right now.

Psycho behaviour by rustfmt:

That's not the right way to talk to other people, please rethink your choice of words. Maybe cool down for an hour or two before pressing the "submit" button when you are writing a bugreport in frustration.

And then let's consider the relevant rules dictated by the Rust Style Guide

I think most people will reasonably assume that the rustfmt issue tracker is the place to file problems with the way rustfmt formats, without caring much whether the source for this behavior is the style guide, or a rustfmt bug where it doesn't match the style guide. In the rustc repo, we don't use completely different issue trackers for "the compiler doesn't follow documented behavior" and "the compiler follows documented behavior but I think that is the wrong behavior". We generally can't expect users to even know which of these categories their issue falls into. So it's a bit surprising to see this split on the rustfmt/style side, and it will surely lead to users reporting issues in the wrong repo.

@calebcartwright
Copy link
Member

Is there a process to propose amending the style guide?

Yes, see RFC 3338 for more details, and there's a number of changes coming as part of Rust's 2024 Edition

https://github.com/rust-lang/rust/blob/master/src/doc/style-guide/src/editions.md#rust-2024-style-edition

Though in general I'd say that modifying the style guide and default behavior is a more difficult road than adding opt-in behavior to rustfmt via a config option.

The behavior Ralf originally requested/reported in this issue of being able to have consistency around the match arms (not the contents of the arms as was unrelatedly requested by some others, but consistency across all arms on whether they are wrapped in braces or have newlines added/removed between the operator and the start arm body) is something that could absolutely be implemented within rustfmt and made available to users via a config option. I don't think it would be an exceptionally difficult nor large implementation; it's just not something that's gotten much attention.

I have opened an issue in the style-team repo: rust-lang/style-team#196.
However, I don't know how active the style team is right now.

t-style is plenty active, though like t-rustfmt and ostensibly most teams, focus is centered around the 2024 edition. I've nominated the issue you opened to get it added to future t-style meeting agenda's, though I doubt it's something we'll spend many cycles on until after we're done with 2024

I think most people will reasonably assume that the rustfmt issue tracker is the place to file problems with the way rustfmt formats, without caring much whether the source for this behavior is the style guide, or a rustfmt bug where it doesn't match the style guide.

Agreed, and I'll just add that we aren't feigning surprise on this, we aren't criticizing anyone for not knowing that, nor for making a reasonable, but incorrect assumption, that rustfmt is the final authority on style. It's common for people to not know there's a standard style, and it's common for us to inform/educate users that there is an official spec rustfmt adheres to, and that's okay; it's why we include links.

However, at the same time I do think too many people come to the table with their own personal stylistic preferences, make an (imo unreasonable) assumption that rustfmt will always format exactly according to their personal preferences, and that any deviations from those subjective preferences trigger some rather emotive reactions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a-matches match arms, patterns, blocks, etc p-medium poor-formatting
Projects
None yet
Development

No branches or pull requests

10 participants