What do you think about gofmt vs rustfmt?

Split off from What do you think about two-space indenting?

The difference between gofmt and rustfmt is that gofmt does not change line breaks. rustfmt is a pretty-printer, while gofmt is a pattern-based white space fixer. It is possible to dislike rustfmt and enjoy gofmt.

11 Likes

I'm fairly certain that gofmt does produce a full parse tree, but it's a parse tree that preserves the whitespace of the input. That's how it preserves single empty lines while collapsing consecutive empty lines.

I may not be clear on what your definition of a "pretty printer" is, but "pattern-based" clearly implies regular expressions and not a proper parse.

1 Like

What I meant is patterns over AST, like "for every two nodes which are expressions, there should be a whitespace between them", like in IntelliJ.

Such pattern based formatters walk the AST, applying a set of pattern to each pair of neigboring nodes and adjust the whitespace according to the rule that matched.

In contrast, pretty printing formatters walk the AST and print it to the output buffer, with little regard to the original spacing.

That is, one type of the formatter fixes proplemantic issues, the other type rewrites the code in one true style.

But I am actually fabulously wrong about go here. Verifying my claim with the source code, turns out it is of pretty-printer style, which just tries very hard to not mess-up newlines:

7 Likes

Note that gofmt has to do that since Go is whitespace-sensitive. E.g. you can't put the opening { brace on a new line, it must go on the same line, and statement separators are line endings, not semicolons.

1 Like

I'm in the same camp. I like gofmt and I think of it as a gentle little helper that cleans up my sloppy whitespace, but it does not have a strong opinion on how I should format my code.

I can't stand rustfmt. It's not helping. It's not smart enough to handle every case correctly, but it's absolutely uncompromising, so it destroys well-formatted code.

It's a common misconception that gofmt unifies code style. It doesn't! It just fixes provably-bad code. Rustfmt is what people imagine gofmt to be. Rustfmt is the one that intentionally eradicates all human input, and as a result is does not leave any room for common sense.

15 Likes

I agree with @kornel. Hindsight is 20/20, but I wish I had known about the two different designs back when rustfmt was created. I would have tried to advocate for gofmt's approach back then.

I do like that rustfmt enforces my line width where as gofmt does not. But, as @kornel said, rustfmt is just not smart enough to handle every case correctly. And I find it annoying to have to tag things with #[rustfmt::skip] when I need to apply common sense. In practice, I very rarely do it, and just live with what rustfmt does because it's better than having #[rustfmt::skip] everywhere.

That is to say, I recognize that there is some advantage to how rustfmt does things. But I would prefer that rustfmt give up enforcing line width and instead have no opinion on where my line breaks are, like gofmt does.

9 Likes

Code formatters may take my life, but they'll never take my linebreaks,
and whitespaces!

I fixed it for you!

1 Like

Also it'd be fine to enforce some maximum line length, but not change layout of code that fits under the limit.

In rustfmt's defense, the "replace all formatting" approach has valid use cases, e.g. it is useful for code generation tools like bindgen that don't have any human-formatted input to begin with.

But rustfmt is too harsh and not the right tool for "format on save" and "check code style in CI" use-cases. In these situations it ends up creating unwanted formatting changes, and rejects good, readable code.

2 Likes

I also prefer the gofmt behavior of allowing a range of different line-break choices. I'm hopeful that rustfmt will someday gain configuration options to allow it to behave similarly. This would make me more likely to use it in my projects.

I know this would be a significant architectural change. However, rustfmt's configurability at least makes it imaginable to add such a feature in a future version, even if it keeps its more-opinionated style by default. There's some more discussion in the issue tracker here:

6 Likes

I do find myself pulling subexpressions out into variables sometimes in an attempt to improve formatting. What's weird is a series of very similarly structured expressions will break at different points depending on the lengths of different parts, so comparing them by eye becomes harder. A local macro can make repetitive code easier to read, though. Also rustfmt won't format the code within a macro invocation if it doesn't parse as valid Rust, so if the macro definition accepts some token or sequence invalid in Rust, then the coder's formatting is maintained. Also line comments seem to be kept in place, so could be used to force linebreaks. Mostly I just accept what rustfmt does, though, so long as it is not ridiculous. However, it would be interesting to see how it would behave if it maintained LFs within expressions, or perhaps took a hint from how the coder used LFs to pick a particular way of formatting it.

1 Like

Personally, I deeply, deeply hate gofmt and really like rustfmt. rustfmt allows me to turn off the 'caring about formatting' part of my brain and just write code. gofmt lack of line breaks forces me to think about formatting, and I end up having to both think about how I want the code formatted and what I have to do to convince gofmt to not mess things up. I won't necessarily go so far as gofmt is worse than nothing, but it's awfully close.

25 Likes

Formatting the input of a function-like macro is a very dangerous thing to do, given that rustfmt "formatting" touches trailing commas and things like that, and that the macro may change of behavior in that case (often breaking by not supporting those changes).

This is also an interesting thing to mention, since a gofmt-like formatting would be orders of magnitude less likely to break / change anything when touching the input of a function-like macro.

1 Like

I disagree with a lot of the choices for formatting rustfmt does, I would have formatted many things in a „better“ way.

Nevertheless, on all projects I have the say, I enable rustfmt in strict mode in the default settings. Because I just hate the endless discussions with team mates when starting a new project (happens every single time when starting a C/C++ project), long documents about what the right formatting is, then discussions about if the document actually covers this case and what it actually means and having to learn yet another formatting style every time I want to submit a PR to some project I've never participated in.

In other words, I'll sacrifice my own preferences for the lack of tiring discussions and the fact I don't have to reconfigure my editor every time I want to fix a single bug in a project I just drive by. It significantly lowers the barrier to just send one single patch.

30 Likes

Here, here.

Just use it, out of the box and move on. Don't waste ones time, and everyone else's time bickering about trivia. Time is short, so are brain cells, use them both on something more worthwhile.

Oddly despite many here stating that rustfmt messed up their code it is not my experience. On the odd occasion I have winced at what it did it was either such a minor offence that I don't much care or it was a hint that I could write my code more readably in the first place!

Has anyone ever considered that rustfmt is trying to tell them something :slight_smile:

7 Likes

I do have an experience of rustfmt severely messing up some code. It, however, was unusual code, in a sense, it was a long table of test cases inside an array and rustfmt wanted to format some of them as single-line and some as multi-line, depending on how long they were.

For these reasons, #[rustfmt::ignore] is good enough solution, same as there are #[allow(clippy::whatever)].

5 Likes

I've found it often can't handle macros very well. I've had to disable it for my macros module. Putting this at the top of the file works (except when in the root module):

#![allow(unused_attributes)]
#![rustfmt::skip]

I don't think it changes commas, i.e. I don't think it changes the token stream that a macro would see. It just decides whether to reformat whitespace or not according to whether it looks like valid Rust code. (This is just from observing the behaviour -- I haven't looked at the source.)

(I took care in Stakker to make my macros parse as valid Rust to get rustfmt formatting for free, but I notice that for example logging crate macros often have arguments which aren't always parseable as Rust, so you can format the contents of the macro however you wish and it won't be reformatted by rustfmt (at least as it is currently). I think the choice when designing macro args could be argued both ways.)

It does, both adding, and removing in the following example.

cat <<EOF | rustfmt
enum Bar{Foo}
fn foo() { match () { () => {(); ()}, } }

EOF
enum Bar {
    Foo,
}
fn foo() {
    match () {
        () => {
            ();
            ()
        }
    }
}
1 Like

We're talking about within macro arguments, but actually even wrapping test!(...) around that code, it still reformats it. So you and @Yandros are right that there is something to be aware of.

But actually if you try it with commas, it doesn't change the commas. Compare:

echo "fn foo() { [0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]}" | rustfmt
echo "fn foo() { test!([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9])}" | rustfmt

One adds the comma, the other doesn't.

1 Like