Approximating “Prettier for Ruby” with RuboCop

By Max Heinritz

We recently migrated Flexport’s Ruby codebase to a new RuboCop configuration that yields Prettier-like formatting results. Out of the box, RuboCop didn’t support autocorrection of line lengths, multi-line expression layout consistency, etc. So we wrote a few new cops internally that we are now open sourcing to share with the broader community. Here are a few before-and-after examples:

Original on the left; formatted with Flexport RuboCop on the right.

Flexport has a Ruby on Rails backend and a JavaScript React frontend. On the frontend, we use Prettier to format our code. We love it. It’s fast and reliable enough to run on file save and in our precommit hook, so all JavaScript files are automatically formatted before they reach code review.

Naturally, we wanted a similar setup for Ruby. We’d been using RuboCop, but our configuration wasn’t strict about things like line length and whitespace. So we began a project to improve backend code style. The main goals were:

  • Universal consistency for minor stylistic things. Ideally, the formatter obviates the need for a written style guide beyond explanatory comments in the configuration file.
  • Minimal disruption to day-to-day engineering workflow. To be enabled by default in the precommit hook, the formatter must autocorrect extremely reliably and quickly.
  • Low complexity. We want something straightforward to implement and maintain, and originally hoped to use something totally off-the-shelf.
  • Configurability. As a fast-growing team of engineers working across languages, we tend to prefer more explicit Ruby code that matches our JavaScript styles. Method calls with parentheses, return statements to disambiguate author intent, etc.

When we started this project in June 2018, we explored three options: RuboCop, rufo, and the Ruby plugin for Prettier. Each have merits.

Rubocop and Ruby plugin for Prettier. (I couldn’t find a rufo logo!)

Internals

Rufo’s and Prettier’s approaches to formatting are more streamlined than RuboCop’s. They parse an abstract syntax tree for a file and then render it in one pass according to a lightly parametrizable algorithm. In contrast, RuboCop mutates code in place using a series of atomic autocorrections defined by individual cops. RuboCop autocorrect may need to run several times, as one cop’s autocorrection may introduce a violation of a different cop.

Configurability

RuboCop is highly configurable and supports static analysis beyond formatting, while Prettier and rufo focus exclusively on code style. For example, the Rails/UnknownEnv cop ensures that Rails.env.foo? is only allowed if foo is a valid environment. Flexport uses cops like this for checking RSpec, Rails, Performance. We’ve also written custom cops for a variety of internal use cases such as ensuring all files have team name comments, standardizing error logging, isolating Rails engines, etc.

Maturity

RuboCop is the most mature project for Ruby. RuboCop was created in 2012 specifically for Ruby and is itself written in Ruby. Rufo is also a Ruby-native project, started in 2017. Prettier was originally created for JavaScript, and there is a beta program for community plugins to support other languages. Development of the Ruby plugin for Prettier began in May 2018, which was around the time we started exploring our options.

Github Stars over time for Ruby formatting options

Trying Ruby plugin for Prettier

Given our success on the frontend codebase, we were eager to experiment with the Prettier Ruby plugin. However, when we tried it in July 2018, we found that it was not reliably correct enough to use on production code. For example, it did not preserve comments. The project was very new and seems to have matured significantly since then, though it still has a few open issues. We’ll keep an eye on it and would encourage others starting out to give it serious consideration.

Trying Rufo

Next we tried Rufo. While reliably correct and fast, it didn’t support the programmatic transformations we needed. In particular, it did not shorten long lines or add parentheses to method calls. Further, the rufo project is planning to remove settings entirely, which poses a risk if the defaults don’t align with our preferences.

Deciding on Rubocop

After surveying the options, we decided to double down on RuboCop. With this approach, we have been able to simplify our developer toolchain, customize settings our taste, and reduce Flexport-specific complexity by using existing cops for most formatting considerations. Though slightly slower than the other options, we need to run it anyway for non-formatting linting. It’s proven fast enough that some engineers run it upon file save.

Simply tweaking our RuboCop configuration for existing cops got us most of what we wanted, but there were a few key features missing. The new cops we wrote internally fill the major gaps.

They work in together nicely because the autocorrector runs repeatedly. For example, on its own AutocorrectableLineLength might yield unsightly results, but MultilineHashKeyLineBreaks will come in and clean them up in later transformations. Check out this code comment for details.

MultilineExpressionLineBreaks / This family of cops checks that each item in a multi-line expression starts on a separate line. It works on hash literals, array literals, and method calls. The upstream PR was merged and released in RuboCop 0.67, available now.

AutocorrectableLineLength / RuboCop has an existing Metrics/LineLength cop, but it doesn’t autocorrect. This new cop programmatically inserts new line breaks into certain long expressions to break them up. It is conservative and only breaks lines when it knows there will be no functionality change, such as method calls. We have an open upstream PR under review.

IndentMultilineClosingBrace / This extends the behavior of the existing Layout/ClosingParenthesisIndentation cop to support hash literals with braces like } and array literals like ], as well as parenthesis like ). We have an open upstream PR under review.

We are collaborating with the RuboCop core team to upstream these, and their names may change. We’ll update this blog post as we go.

Our Ruby code formatting configuration is stable now. During the migration to the new setup we ran into a few cases where interactions between different RuboCop autocorrections led to unusual layouts for particularly egregiously formatted legacy code, but we were able to manually fix these cases.

We’ve now had the Ruby formatter enabled on precommit by default for the whole codebase for several months with no major issues. To give you a sense of magnitude, we have about 1.5m lines of Ruby code and a team of around 100 full-stack engineers.

That said, the configuration is not perfect. Here’s the canonical example for Ruby Prettier:

Original on the left; formatted with Flexport RuboCop on the right.

As you can see, the output is not as clean as the Ruby plugin for prettier on the project README. However, we’ve found that for real code humans write on a day-to-day basis, our configuration is sufficient to achieve consistency without risk of code correctness issues.

Per the links above, we’d like to finish upstreaming the new cops into the RuboCop repository. If you’d find them useful, please add an emoji reaction or comment on the upstream PRs. Code suggestions welcome as well — there are several opportunities for improvement, such as expanding the cases handled by AutocorrectableLineLength.

We are also curious to hear how other teams have approached Ruby formatting. There are many reasonable choices, as RuboCop creator Bozhidar Batsov concluded in his recent blog post The Missing Ruby Code Formatter.

Let us know what you think!