my paper cuts with rustfmt - and what to do about them
rustfmt occasionally fails me. Of cause, the examples are somewhat taken from
production code, but with all identifiers rendered unrecognizable and all
strings replaced by random words. Unhelpful comments to all of this would be
“why don’t you use shorter identifiers?” or “use shorter error messages”. But
let’s face it:
-
Naming is hard. And if your
foofunction and yourbarfunction both return a “result”, but neither can use thestd::result::Result(slightly different semantics, non-binary logic, …) and you can’t introduce a common result type (too different needs for a one-size-fits-all, not even with type parameters), then you have to end up with a type name that says “result” with some more context. And a reader 10 levels up the call tree must not confuse theFooResultwith theBarResult. Taking a thesaurus to come up with different names would be counter productive. Between aMyResultandMyOutcome, a human reader won’t remember which one is fromfooand which one is frombar, what the API differences are, etc. The best name migh just befoo_module::Resultandbar_module::Resultanduse foo_module::Resultwon’t help the reader. -
I suck at naming. That must not be a justification for using poor names, but … should I really spend another week on trying to come up with a better name and then eventually not come up with one? A reviewer might anyways have a different perspective and I could highlight a name with a few suggestions. But at that point in time, I need formatting. (And let’s keep in mind that reviewer time and energy are a finite resource)
-
Same with the strings. By now, I hate “no such file or directory” with a passion. Was this a
cpcall? Does the source file not exist? Does the target file not exist? … Oh,/usr/bin/cpmight not exist! You know what, that’s not even the actual error that you’re investigating. Thecpcall was opportunistic “copy the file if it exists, if not that’s fine.” Thelibcauthor can’t know whycpis trying to access a file. Thecpauthor doesn’t know why you’re trying to copy a file. The tool that’s invokingcpmight have some context, but it now tries to read its config file to look up error handling and the config file doesn’t exist. The config file reader doesn’t know that falling back to defaults is desired. What I’m saying: With enough layers of indirection, you don’t have a complete picture, but you need to do all you can to provide the debugging person enough information. They have no idea that deep down there’s acpinvocation. It’s not enough for them to be told thatcpis failing, they need information about why it’s called what is even happening, maybe a comment that “chances are high that this error message is a red herring”. I’m not an expert on logging, but with enough experience to know that cutting strings down to 25 characters isn’t a solution, either. -
Line length limits are still around and somewhat justified. Once you view a
git diffon the web (gitlab, github, codeberg, …), any language support of your editor won’t help. You may or may not be able to enable or disable line wrapping. At some pointfunction(argument1, argument2)must be wrapped one argument per line.
Setup
For this post, I’m using rustfmt. That’s not the only tool for formatting
rust files. In a productive setting you probably use some sort of editor/ide
integration (which calls rustfmt itself) or an LSP (which probably also just
makes a syscall to rustfmt) or use the higher level cargo tool cargo fmt
(which also calls rustfmt, but for the entire crate or workspace, so way more
convenient as git hook or pipeline stage).
Here’s the initial code
|
|
Doesn’t look too bad. Creating an anyhow error, only to instantiate a
thiserror error with it, only to instantiate another thiserror error with
it is probably a code smell. But you see that we’re in the transition from an
old API to a new API. We need to return a Result<(), CustomThiserrorEnumType>, refactoring the error type and all usage sites would
be out of scope.
What’s odd is the indentation of the first )),. But rustfmt --edition 2021 leaves the code as-is.
Curiously, I was at first blaming the type names. Because once I added some use, the formatting improves:
|
|
Now the lines around the enums got rewrapped and parentheses merged.
Curiously, one can also wrap the string:
|
|
and rustfmt will improve the indentations
|
|
Except, curiously, rustfmt only indents the first line of a multi-line string. And I consider this the danger-zone. In the printout, I wouldn’t want to see many white spaces between the period and Lore. But that’s fine, rust ignores the leading spaces, I can indent the second line all I want and rustfmt won’t move Lore back to the 9th column.
Going back to the initial example, when calling
/home/pseyfert/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rustfmt --error-on-unformatted --unstable-features --edition 2021 example/nightly.rs --config error_on_line_overflow=true,format_strings=true
we get finally something acceptable
|
|
Note, here I let the formatter wrap the multi line string.
Aren’t configurable formatters bad?
Yes. It’s important to note though that the output of my configured nightly rustfmt is invariant under the standard stable rustfmt.
The output of the configured nightly rustfmt is in the fixpoint set of standard stable rustfmt.
Does it matter?
Yes. rustfmt by default leaves expressions with too long strings unformatted.
Sadly, expressions can be pretty big. In the example below, I added some wrong indentations, and out-of-the-box, rustfmt just leaves things as they are.
Observe where I suggest several lines to be added. It could be that the "Lorem Ipsum" string isn’t even on the same screen page as let y.
Let me repeat: only the length of the Lorem Ipsum string is what blocks formatting of let y = and all the other formatting issues.
|
|
Failure modes
Sadly, with the configuration that I suggest above, the formatter is not guaranteed to succeed. Normally, one can just format code without handling failures.
long links in comments
A simple link can be too long to result in unformattable code
|
|
I haven’t come up with a way to format this in a way that my rustfmt setting can handle. The link just is too long.
let (many, assignments) = call
TODO, I have a line of code in non-public code, which I haven’t managed to turn into a standalone example yet.
far indented long identifier
I’m for other reasons wondering if indentation should better not be counted for column limits (only count columns from first non-space to last non-space). Ignoring the reasons, let’s look at a far indent
|
|
rustfmt will give up with
error[internal]: line formatted, but exceeded maximum width (maximum: 100 (see `max_width` option), found: 101)
--> /home/pseyfert/coding/blog/research/rustfmt/example/ind.rs:22:22:101
|
22 | ... let _critical_object_with_long_name =
| ^
|
warning: rustfmt has failed to format. See previous 1 errors.
Again nothing that can be done automatically. You probably should break up the block into a call stack. From the formatting point of view, the code is unformattable.
Conclusion
What do I want:
- I don’t want silent failures of the formatter. A formatter should tell me if the result of formatting is not good.
- Multiline strings should be indented (Actually, I’m unhappy that the rustfmt default doesn’t do that well)
Not within this post (read: if I ever write those posts, I should link them here):
- Manual formatting in exceptions needs to be possible. (future post? maybe?)
- I’m unhappy with counting indentations for column limits (merge conflicts? maybe future post?)
- Thoughts on formatting in CI