rust-lang / rust-lang/rustfmt

max_width makes short lines longer

Open
#6,338 9 comments 9 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-feature-request
Dominant language
Rust
Stars
7k
Forks
1.1k
Avg merge
2d 13h
Merged PRs (30d)
24

Description

If I have some code that is formatted perfectly correctly for a max_width = 50:

fn example() {
    let x = [1, 2, 3]
        .iter()
        .rev()
        .enumerate()
        .count();
}

then the same code formatted with a longer maximum length (max_width = 100) always gets reformatted to stretch as widely as possible. It gets changed despite already being formatted cleanly, and not exceeding the maximum width:

fn example() {
    let x = [1, 2, 3].iter().rev().enumerate().count();
}

This makes rustfmt's formatting especially detrimental when max_width is set to a high value (like max_width = 150 or max_width = 200), because expressions that had reasonable line breaks previously are forced to become very long lines.

I don't mind having occasional long lines in the code, and I would like to be able to set higher allowable maximum line width, without it also becoming a target width for making all code maximally wide.

Example use-cases:
  • In if conditions, wrapping has a relatively high cost. Indentation of the wrapped conditions is the same as indentation of a usual "then" block, so the reader has to pay close attention to the unusual position of {. In such cases a line that is a bit longer is IMHO a lesser evil. But I don't want rustfmt to undo already wrapped if conditions where I've already decided the line was too long.

  • I have match statements with similar but not identical content (e.g. when mapping types in an enum to invocations of a generic function, the calls may be very similar, but variant names and variable names differ in lengths). It's possible for some, but not all, of the match arms to exceed the usual limit of 80-100 chars. This makes rustfmt format the arms inconsistently in regards to each other: some will have multi-line formatting, some will have single-line formatting. This makes it harder to compare the match arms. A value of max_width that makes one match consistent will break consistency of another that has different range of widths.

  • I have expressions that implement standard-defined mathematical formulas defined in external documents. I want those expressions to have line breaks in the same places as in the source document, for them to be easy to compare to the original for review.

  • I want to avoid trivial code changes from looking more complex in code reviews. Code is more often read more than written, and code that is developed via pull requests is also often read as a diff. For me as a maintainer, readability of diffs of the code is most important. When a PR renames a function or a variable, I want that PR to have minimal single-word diffs. Unfortunately, if the new name has a different length, it often makes lines cross the (un)wrapping threshold, and changes start to look like a complete rewrite of entire blocks/statements. Same thing happens when a line is deleted from a multi-line expression (e.g. a struct field is deleted). Instead of keeping it as an obvious 1-line-delete diff, rustfmt can decide to unwrap the whole multi-line block into one long line, making the diff much bigger, and obscuring what has really changed.

Why not #[rustfmt::skip]?
  1. It's an all-or-nothing mark, rather than control over line length. I still want all the short and long lines to be correctly formatted in all other aspects, like spaces around punctuation. When a long line ends with a block, I still want the indentation of the following code in the block to be formatted well.

  2. #[rustfmt::skip] attribute by itself adds visual clutter. It becomes a point of contention whether an improvement in readability of one line is worth adding an eyesore in another. Usually it isn't, which makes me feel forced to live with an undesirable max_width behavior that always makes some code worse regardless of what value I set it to.

Notably, go fmt does not have this problem. In Golang, lines that exceed the maximum limit are wrapped to become multi-line expressions, but expressions that already fit below the maximum line length are not forced to become one-liner spaghetti. All lines are still formatted correctly according to gofmt's rules, and this behavior is entirely deterministic and idempotent.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the two formatting examples with rustfmt at max_width values of 50 and 100, then compare the behavior with the requested preservation of existing line breaks. Done means a deterministic formatting behavior that allows higher maximum widths without unnecessarily collapsing already reasonable multi-line expressions, with coverage for the listed use cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.