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:
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.
returnstatements to disambiguate author intent, etc.
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.
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.
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.
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:
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
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!