Properly formatted `cargo fix` code

Being spoiled by my editor running rustfmt, I frequently find that when I run cargo fix or cargo clippy --fix, I forget that I need to manually run cargo fmt afterwards. Sometimes I don't notice until CI complains.

@kpreid brought up the idea of cargo fix concluding with a call to cargo fmt in Automatic `cargo fmt` after `cargo fix` · Issue #12608 · rust-lang/cargo · GitHub.

As a Cargo team, there are different opinions on whether we should do something about this but we at wanted to get further input on the idea and the potential ways of moving this forward.

The most basic solution is for cargo fix to print a note reminding people that the applied changes don't conform to a style guide and a call to cargo fmt may be warranted.

The next step up from that is a way for users to opt-in to cargo fmt integration. Initially, this would mean that whenever cargo fix or cargo clippy --fix change source, Cargo runs rustfmt with the appropriate edition set. This could even be done in-memory, avoiding a write/read/write between operations. Later as cargo fmt gains Cargo.toml formatting support, this could extend to cargo add, etc.

Do others feel this is a problem to be solved?

What would the opt-in look like? To match my editor, I would prefer user config. Its rare that that causes me problems at least. However, maybe its a repo setting (maybe config) or a project setting (manifest)?

How cautious should we be with formatting? cargo fix is already cautious in what code it touches. Even with the opt-in, there might be a question of whether we should format unformatted code, like cargo fix not fixing uncommitted changes. We could cargo fmt --check the file first and require --allow-dirty to move forward. If we could tell rustfmt the byte ranges of the code we changed and it could do the minimal format for that, that could also help.

What other commands would benefit from check integration? While we could have different enforcement points built-in to cargo, I feel like an error would be too disruptive to development. We could do these as warnings though.

What problems may we run into with rustfmt config? One I've run into with my editor is when nightly rustfmt features are used and I have to format with a different toolchain than I do development with.

CC @calebcartwright

1 Like

Isn’t the no-brainer/minimalist and presumably straightforward approach something like “if a file was fully cargo-fmt formatted before then the file should be formatted after else behave like currently”? Could there be any downsides to this approach?


This could be combined with warning that fires if the file in question was not found to be correctly formatted yet.

Further opt-in or settings aren’t really required IMO anyway. In cases where people would want the file completely cargo fmt-formatted, but it hasn’t been in a well-cargo fmt-ed, I don’t see any benefit in prompting the user to “please cargo fmt first” if they could just as well run cargo fmt afterwards and be done already, with the same end result.

Maybe the suggestion could even include the command that would format just the files in question – would have been my next sentence, but looking into this some more, is it really true that stable rustfmt does not support single-file formatting (i.e. without other files reachable via mod foo;) yet!?


A mode to “always run cargo fmt, too” seems unnecessary as well, because cargo fix isn’t supposed to replace cargo fmt anyway, right? So all proper users would also be using cargo fmt manually, anyway, (or equivalent functionality through their IDE).

Thinking of possible downsides anyway…

…the only remaining somewhat unsupported use-case is when users want to e.g. use stable cargo fix in a project where nightly rustfmt is used and configured in a place that the cargo fix-invoked rustfmt would pick up, that motivates simply also emitting the same warning that the resulting changes might not be ideally formatter; perhaps with an added help or note that describes the failure case.

The only false behavior would be in the rare case where

  • rustfmt (or alternative Rust formatting infrastructure) that the user actually uses is configured somewhere else – or a different stable version – but happens to consider the file in question well-formatted before the fix, too, as does the actual tool the user uses
    • then after the fix, formats it into a state that no longer matches the formatting tooling that user actually uses
    • so there was no warning, but the result wasn’t the “desired” formatted result the user would have wanted

Though this is not worse than the status quo, anyway.


Aaaand I’m noticing a second possible case of misbehavior:

  • rustfmt sometimes “gives up” on sections of code leaving them untouched as a result
    • this means that if the fix happens in such a section, the result could be not-properly-formatted (needing to be looked at by the user) but rustfmt behavior itself gives no indication of this happening
    • this means that if the cargo fix change happens to remove the issue that rustfmt got stuck on, then subsequent formatting of the file can affect a larger section of code, in principle
    • this means that if the cargo fix change happens to introduce a code pattern that rustfmt “gives up” on, the result could be not-properly-formatted without warning

Only the second case would be an actual possible regression; but I also think this is rarely all that problematic because rustfmt “giving up” is often well-restricted to small sections of code anyway.

Ideally, rustfmt should have a mode where it simply tells use the set of ranges of file positions that it “gave up on” and if the cargo fix diffs overlap with those, a warning could be generated.

Probably, the behavior should still be the same “for each file separately: if a per-file cargo fmt --check operation blessed the file’s previous state, then run a per-file formatting after the fix”. The main case the warning addresses is where the fix applies to an non-formattable section, and thus manual review by the user is might be required for a good-looking result.

I do think all of this can be left as a future possibility though.

Just like a range-based mode can be a future possibility. (Looks like rustfmt already has unstable functionality for formatting ranges within a file; so in the future with this, it might be possible to transition the per-file based logic to a per-diff based logic, but maybe that should wait for the relevant rustfmt feature to be stabilized.)[1]

Edit: Another example I’m just thinking of is rustfmt::skip on a code section; though that would probably have all the same implications & relevant mechanisms… Realistically, the only reason why we would want to warn here is “hey, you should look at this code, it might not be formatted do your desires”, but realistically reviewing of cargo fix changes “look good” is the user’s job anyway, and the main concern that we’re instead solving is to avoid cases where the result does look like (and is) perfectly valid and well-formatted code, but happens not to be cargo fmt’s exact choice of formatting. This main concern does not apply with rustfmt::skip sections or sections rustfmt “gives up” on.


A last concern is that that occasionally rustfmt itself can break code. In theory there could be a case where cargo fix would change code correctly and only subsequent formatting breaks it. It’s usually the opposite, i.e. cargo fix seems more likely to break code itself in the first place, and there’s already counter measures in place that check vcs so that no state is lost unless the user opts in to it. This should be rare and the user still has the compiler warning anyway, so between [case 1:] a slightly incorrect suggestion from cargo check where cargo fix breaks your code, and this new case [case 2:] of the suggestion from cargo fix being 100% correct and only the conditional automatic subsequent formatting making the code slightly wrong… the UX should be quite similar. They can always try acting on the warning manually; and might not even notice the difference between these cases.


  1. I think even in this range-based approach as a future added functionality, it would necessarily be a fallback to full-file approaches. Because it isn’t trivial to find the right ranges IMO! It could always happen that ranges being too short means that rustfmt doesn’t format the whole file properly, which would be unacceptable if the initial state of the whole file was “completely formatted”. ↩︎

6 Likes

Arguing against myself a bit, here’s a recent reason why I would want a file not to be formatted by cargo fix: The 2024 edition migration has drop order “fixes” where the right choice is often to revert the fix (because the drop order doesn’t actually matter). It was easier to understand what happened, and revert them with git checkout -p, because only the beginning and ending changed, without also reindenting (and potentially rewrapping) the code in the middle of the block. For that, I would have appreciated a cargo fix --no-fmt option.

I have not thought of any specific opinions on how enabling automatic formatting should work, though. @steffahn’s “format if was formatted” is elegant but feels like the sort of DWIM thing that could easily turn out troublesome.

3 Likes
  1. Try the migration on nightly and if still is doing matches when you don't need it, open an issue! That diagnostic has been improving
  2. I think a cargo fix --interactive would also make a big difference in that case. I'd like to see it have "accept / reject all for lint X" to help with problems like the edition migration until the diagnostic is improved (or in case it can't be further improved)

Granted, that does illustrate a broader point that diffs for cargo fix are small as-is which makes it harder to look back on.

1 Like

I’d agree that offering a way of turning off the “format if was formatted” behavior (and instead doing no formatting at all) definitely seems appropriate. In my opinion, that’s the only configuration necessary though. It can also be useful as a setting, not just a command line argument; e.g. running the additional formatting checks might be unwanted extra work/computation for users that don’t use rustfmt anyway; and disabling the behavior would presumably also turn off all those new warnings regarding formatting. Those warnings could presumably also mention the existence of such a setting, anyway!

The question on whether this setting is a case of on-by-default +opt-out, or off-by-default +opt-in I’m not even 100% sure. But as I argue in my previous comment, I don’t really see much value in offering any “always format” option.


Exploring a ranges-based approach[1] I’ve already touched on in the <details> of my previous answer could perhaps be worthwhile thing as an argument in favor of making this feature on-by-default; because it can make it much more useful on projects that don’t maintain a global rustfmt-compatible style.


For cargo fix --edition specifically, usage of cargo fmt is already part of the official guide, so it might be a reasonable idea to consider making the --edition flag imply some --no-fmt flag in a world where this formatting feature is otherwise opt-out.[2]


Totally unrelated, but another thing I’m wondering now: what does #[rustfmt::skip] on a mod foo; do? On a file-by-file based approach, wouldn’t be wanting to format more files than what a full-project cargo fmt does…


  1. perhaps using existing unstable functionality rustfmt already has, or aiming for stabilization, or at least stabilization-for-internal-use [i.e. a guarantee that this kind of functionality won’t go away completely] ↩︎

  2. I can also imagine reasonable arguments against making such an exception to the opt-out rules; I’m just saying that it’s something to consider. ↩︎

3 Likes

I am of the opinion that cargo fix should've always included cargo fmt. To me, "fix" includes "fix formatting", especially since vast majority of machine - applicable fixes are purely stylistic.

But then we have non-trivial fixes like migrating editions. And I would postulate that it would make a lot of sense to separate applying those into a different subcomand altogether.

2 Likes

Well, people definitely wouldn't want cargo fix to mess up their formatting if they don't use rustfmt in the first place.

3 Likes

I use git diff to review the changes from cargo fix/cargo clippy --fix. I wouldn't want anything else formatted other than the cargo-fixed lines/items, because the diff would be diluted by unrelated formatting changes, and that would add extra work to the code review.

rustfmt can be counter-productive for reviewing code diffs, because if a 1-line change ends up crossing some threshold, it can cause the entire block of code to be reformatted, making it look big in the diff and obscure what has been actually changed.

In my experience changes from cargo fix are generally formatted well enough. Clippy's added #[must_use] stands out as one change that doesn't follow the usual formatting style, but that could be fixed in the lint itself.

4 Likes

Sorry, if I was unclear, I meant to advocate for having applying formatting be the default, with cargo fix --no-format opt-out.