RuboCoping with legacy: Bring your Ruby code up to Standard
You will hardly find a Ruby developer who hasn’t heard about RuboCop, the Ruby linter and formatter. And still, it is not that hard to find a project where code style is not enforced. Usually, these are large, mature codebases, often successful ones. Fixing linting and formatting can be a challenge if it wasn’t set up correctly from the get-go. So, your RuboCop sees red! Here’s how to fix it.
Disclaimer: This article is being regularly updated with the best recommendations up to date; take a look at a Changelog section.
In this post, I will show you how we at Evil Martians touch up codebases of our customers in 2020: from quick and dirty hacks to proper Standard-enforced style guides, and our own
patented way to use Standard and RuboCop configs together.
Let’s pretend I have to convince you to follow code style guidelines (I know, I know I don’t have to!)
Here are the arguments I would use:
- Developers understand each other much better when they
speakwrite the same language.
- Onboarding new engineers becomes much easier when the code style is standardized.
- Linters help to detect and squash bugs in time.
- No more “single vs. double quotes” holy wars (double FTW)!
That was all the theory for today. Time for practice!
TODO or not TODO
So, you have joined a project with no style guide or with a
.rubocop.yml that was added years ago. You run RuboCop, and you see something like:
$ bundle exec rubocop 3306 files inspected, 12418 offenses detected
Flocks of noble
knights developers tried to slay the beast fix the offenses but gave up. But that doesn’t stop you—you know the magic spell:
$ bundle exec rubocop --auto-gen-config Added inheritance from `.rubocop_todo.yml` in `.rubocop.yml`. Created .rubocop_todo.yml. $ bundle exec rubocop 3306 files inspected, no offenses detected
That was simple! Toss the coin to your…
Let’s take a closer look at what
--auto-gen-config flag does:
- First, it collects all the offenses and their counts;
- then, it generates a
.rubocop_todo.ymlwhere all the current offenses are ignored;
- and finally, it makes
That is the way to set the status quo and only enforce style checks for new code. Sounds smart, right? Not exactly.
.rubocop_todo.yml handles “ignores” depends on the cop types and the total number of current offenses:
- For metrics cops (such as
Layout/LineLength), the limit (
Max) is set to the maximum value for the current codebase.
- All cops could be disabled if the total number of offenses hits the threshold (only 15 by default).
So, you end up with anything goes situation, and that defeats the purpose.
What does it mean for a typical legacy codebase? Most of the new code would be ignored by RuboCop, too. We made the tool happy, but are we happy with it?
Hopefully, there is a way to generate a better TODO config by adding more options to the command:
bundle exec rubocop \ --auto-gen-config \ --auto-gen-only-exclude \ --no-exclude-limit
--auto-gen-only-exclude force-excludes metrics cops instead of changing their
Max value, and
--no-exclude-limit prevents from cops being disabled completely.
.rubocop_todo.yml won’t affect your new files or entirely new offenses in the old ones.
RuboCop doesn’t only help with style—it also saves you from common mistakes that can break your code in production. What if you had some bugs and ignored them in your TODO config? What are the cops that should never be ignored? Let me introduce the RuboCop strict configuration pattern.
You shall not pass: introducing
There are a handful of cops that must be enabled for all the files independently of the
.rubocop_todo.yml. For example:
Lint/Debugger—don’t leave debugging calls (e.g.,
rubocop-rspec)—don’t forget to clear focused tests (to make sure CI runs the whole test suite).
We put such cops into a
.rubocop_strict.yml configuration file like this:
inherit_from: - .rubocop_todo.yml Lint/Debugger: # don't leave binding.pry Enabled: true Exclude:  RSpec/Focus: # run ALL tests on CI Enabled: true Exclude:  Rails/Output: # Don't leave puts-debugging Enabled: true Exclude:  Rails/FindEach: # each could severely affect the performance, use find_each Enabled: true Exclude:  Rails/UniqBeforePluck: # uniq.pluck and not pluck.uniq Enabled: true Exclude: 
Then, we replace the TODO config with the Strict config in our base
.rubocop.yml configuration file:
inherit_from: - - .rubocop_todo.yml + - .rubocop_strict.yml
Exclude:  is crucial here: even if our
.rubocop_todo.yml contained exclusions for strict cops, we nullify them here, thus, re-activating these cops for all the files.
One Standard to rule them all
One of the biggest problems in adopting a code style is to convince everyone on the team to always use double-quotes for strings, or to add trailing commas to multiline arrays, or ro \<choose-your-own-controversal-style-rule>? We are all well familiar with bikeshedding.
RuboCop provides a default configuration based on the Ruby Style Guide. And you know what? It’s hard to find a project which follows all of the default rules, there are always reconfigured or disabled cops in the
That’s okay. RuboCop’s default configuration is not a golden standard; it was never meant to be the one style to fit them all.
Should Ruby community have the only style at all? It seems that yes, we need it.
Thankfully, Ruby’s ecosystem has got you covered: there is a project called Standard, which claims to be the one and only Ruby style guide.
From the technical point of view, Standard is a wrapper over RuboCop with its custom configuration and CLI (
Standard also supports TODOs with
bundle exec standardrb --generate-todo. This will create a
.standard_todo.yml that lists all the files that contain errors. When you run Standard in the future it will ignore these files as if they lived under the ignore section in the .standard.yml file.
However, if you prefer to keep all your eggs in one basket and look for a way to integrate your existing
.rubocop_todo.yml with your Standard workflow, look no further.
We can still use Standard as a style guide while continuing to use RuboCop as a linter and formatter!
For that, we can use RuboCop’s
# .rubocop.yml # We want Exclude directives from different # config files to get merged, not overwritten inherit_mode: merge: - Exclude require: # Performance cops are bundled with Standard - rubocop-performance # Standard's config uses custom cops, # so it must be loaded - standard inherit_gem: standard: config/base.yml # You can also choose a Ruby-version-specific config # standard: config/ruby-3.0.yml inherit_from: - .rubocop_strict.yml # Sometimes we enable metrics cops # (which are disabled in Standard by default) # # Metrics: # Enabled: true # Global options, like Ruby version AllCops: SuggestExtensions: false TargetRubyVersion: 3.1
That is the configuration I use in most of my OSS and commercial projects. I can’t say I agree with all the rules, but I definitely like it more than the RuboCop’s default. That is a tiny trade-off if you think about the benefit of not arguing over the style anymore.
Don’t forget to add
standard to your Gemfile and freeze its minor version to avoid unexpected failures during upgrades:
gem "standard", "~> 1.0", require: false
Another advantage of this approach is that you get support for RuboCop plugins and custom cops that Standard doesn’t provide out of the box as of this writing.
Although the approach above allows you to tinker with the Standard configuration, I would not recommend doing that. Use this flexibility to extend the default behavior, not change it!
Beyond the Standard
Standard only includes the
rubocop-performance plugin. We usually add
rubocop-rspec to our configuration.
For each plugin, we keep a separate YAML file:
Inside the base config we add these files to
inherit_from: - .rubocop_rails.yml - .rubocop_rspec.yml - .rubocop_strict.yml
There is no standard RSpec configuration, so we had to figure out our own:
We also usually enable a select few custom cops, for example,
Lint/Env. We use a
.rubocop_custom.yml configuration file for this:
# .rubocop_custom.yml require: # Cops source code lives in the lib/ folder - ./lib/rubocop/cops Lint/Env: Enabled: true Include: - "**/*.rb" Exclude: - "**/config/environments/**/*" - "**/config/application.rb" - "**/config/environment.rb" - "**/config/puma.rb" - "**/config/boot.rb" - "**/spec/*_helper.rb" - "**/spec/**/support/**/*" - "lib/generators/**/*"
In the end, our typical RuboCop configuration for Rails projects looks like this👇
# .rubocop.yml inherit_mode: merge: - Exclude require: - rubocop-performance - standard inherit_gem: standard: config/base.yml inherit_from: - .rubocop_rails.yml - .rubocop_rspec.yml - .rubocop_custom.yml - .rubocop_strict.yml AllCops: SuggestExtensions: false TargetRubyVersion: 3.1
If you prefer (as I do) to avoid bloat In the project’s root folder, you can put your configs in the
<project>/ .rubocop/ custom.yml rails.yml rspec.yml strict.yml app/ ... .rubocop.yml .rubocop_todo.yml ...
Feel free to use it this as inspiration with your projects that are in need of some RuboCop tough love.
RuboCop plays a vital role in the Ruby world and will stay TOP-1 for linting and formatting code for quite a long time (though competing formatters are evolving, for example, rubyfmt and prettier-ruby). Don’t ignore RuboCop; write code in style 😎
TargetRubyVersionto the example configuration.
- Add a note about Standard’s version-specific configs.
- Add a link to
-no-exclude-limit(added in RuboCop v1.3.7).
- Extracted custom cops into their own configuration.
- Added a note about the
- Added rubocop-graphql to the list of popular plugins.
- Upgraded to Standard 1.0.
- Mention Standard support for
--generate-todoflag. Thanks to Jennifer Konikowski, one of the StandardRb maintainers, for pointing that out to us.
- Fixed TODO config generation command.
- Added note about rubocop_lineup and pronto-rubocop.
- Added note about RuboCop defaults survey.