Why I'm Not a Fan of Strict Code Auto-formatting Tools

March 12, 2023 —

Since I deleted my Twitter account last summer, I have been lacking a place to post mini-rants about silly things that no one will read. So here's one.

<rant>

I'm generally not a huge fan of strict code auto-formatting tools. For example, stuff like gofmt and rustfmt as well as others. I haven't tried them all so maybe there are some out there that I'd like.

My issue is basically with the fairly subjective nature of what constitutes code readability, at least as far as formatting is concerned. If you listen to or read discussions about this you'll undoubtedly find a lot of people blindly saying that, well, these tools are the only way to keep codebases formatted cleanly! Have you seen what kinds of messes other developers commit?

I guess as someone who has personally heard people comment on his own code several times over his career with stuff like "Wow, this code is actually very neat, I'm surprised!" I get bothered by this because ... *gasp!* some of us out there are capable of manually keeping code neat and tidy (formatting-wise, anyway!).

My complaining about this is probably ironic I suppose, as a big reason these tools exist in the first place and are in common use is to help remove these exact types of debates and subjective takes on code formatting. To just pick a "one true style" for the team and be done with it. And I understand the logic. But I don't have to like it, heh.

To be very clear here, despite my complaints I would always use such a tool when I am working on a team which has decided to use one. Always follow the standard that your team has set forth!

Basically my biggest annoyance with some of these tools is the disallowance of some forms of arbitrary formatting that would, at first glance, seem to be well within the bounds of where things like wrapping to the next line, etc. kick in based on the tool's settings.

An example of what I mean with Rust and rustfmt:

let m = Matrix3x3::new(
	1.0, 0.0, 0.0,
	0.0, 1.0, 0.0,
	0.0, 0.0, 1.0
);

Once passed through rustfmt (default settings), you get:

let m = Matrix3x3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);

To prevent this from happening, you should sprinkle a little annotation before the block of code you want to not be auto-formatted:

#[rustfmt::skip]
let m = Matrix3x3::new(
	1.0, 0.0, 0.0,
	0.0, 1.0, 0.0,
	0.0, 0.0, 1.0
);

I initially thought that this was an acceptable compromise and went on with my coding.

But as time went on, I found that this kind of thing does come up often enough with function calls that have more than just one or two arguments, especially where one or more of the arguments are expressions of some sort. Sometimes I do want the formatting to kick in and for it to wrap, and sometimes I don't. It IS subjective, yes.

// I am quite fine with this all in one line in this particular case. It's not too long to me,
// and the arguments are all simple enough to read (no semi-complex expressions).
system.res.video.print_string("Hello, world!", 20, 20, FontRenderOpts::Color(15), &system.res.font);

// Rustfmt's default settings disagree.
system.res.video.print_string(
    "Hello, world!",
    20,
    20,
    FontRenderOpts::Color(15),
    &system.res.font,
);

Aside from once again sprinkling in #[rustfmt::skip], the only real option that rustfmt gives you to control this is fn_call_width which is applied very strictly. For example, if I have set fn_call_width = 120 then the above original call to system.res.video.print_string() is left alone. So I guess problem solved, right?

Not so fast! Now, with this larger fn_call_width setting, if I later decide in another function call elsewhere that I want to put all the arguments on their own lines because maybe one of the arguments in this case is some sort of expression and I think it just reads nicer this way:

do_something(
    foo + 10,
    (bar + 2) / 100,
    &blah
);

Well, too bad, because that's all going onto one line once rustfmt is done with it! Because rustfmt sees that the entire function call easily fits within your 120 character limit.

do_something(foo + 10, (bar + 2) / 100, &blah);

The same type of thing happens with method chaining. If I had rustfmt configured for long lines, for example setting max_width = 100 and even explicitly setting chain_width = 100, if I were then to write some code like this to help with my own readability in this particular case:

let mut system = SystemBuilder::new()
    .window_title("Testing 123")
    .vsync(true)
    .build(config)?;

Well, rustfmt helpfully puts all this on one line, because the chain width of this entire thing is less than 100, so it fits! You're welcome!

let mut system = SystemBuilder::new().window_title("Testing 123").vsync(true).build(config)?;

You get the idea hopefully.

It's just not possible to fine tune the configuration because the tool always wants to make everything match its maximum length (or whatever other equivalent), or else enact a different style of formatting. Are you within that threshold but not over it? Too bad!

Yes, you can sprinkle #[rustfmt::skip] around wherever you like. But that's damn annoying to have to do in all of these kinds of situations! I'm sorry, but it is.

It is my understanding that the way that some of these formatting tools work (including rustfmt) is by just parsing the input code file into an AST and then "pretty-printing" it all back out according to the configuration. So it's not even aware of whatever manual formatting the original code may or may not have had. Certainly I can appreciate the logic of this implementation despite its limitations.

I don't believe that a method of formatting that uses arbitrary line lengths as a "do this or do that" boolean check is good enough when it comes to code. There are more than enough edge cases that come up almost daily that I think most developers see.

</rant>

2023-03-13 Update

Shortly after posting this, I was informed that there is another way to force rustfmt to keep your manually added linebreaks aside from the usage of #[rustfmt::skip]. Just add trailing comments:

let m = Matrix3x3::new(
	1.0, 0.0, 0.0, //
	0.0, 1.0, 0.0, //
	0.0, 0.0, 1.0
);

let mut system = SystemBuilder::new()
    .window_title("Testing 123") //
    .vsync(true) //
    .build(config)?;

I don't know if this is documented anywhere, but if it is, I definitely missed it. Thanks to Stephan Sokolow for pointing this out to me!