RuboCoping with legacy: Bring your Ruby code up to Standard
Share this post on
You’ll hardly find a Ruby developer who hasn’t heard about RuboCop, the Ruby linter and formatter. Yet still, it’s also not that hard to find a project where code style has not been enforced. Usually, these are large, mature codebases, and often successful ones. But fixing linting and formatting can be a challenge if it wasn’t set up correctly from the get-go. So, if your RuboCop is seeing red, here’s how to fix it!
Disclaimer: This article is being regularly updated with the best, most up-to-date recommendations; take a look at the Changelog section for more info.
In this post, I’ll show you how we at Evil Martians touch up customer codebases in
<%= Date.today.year>: from quick and dirty hacks to proper Standard-enforced style guides, and our own
patented way to use Standard and RuboCop configs together. After reading, if you like the proposed configuration, you can find the instructions on how to automatically apply it to your project using the RuboCoping application template.
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 detect and squash bugs in a timely fashion.
- No more “single vs. double quotes” holy wars (double W)!
Enough theory for today, time for practice!
TODO or not TODO
So, you’ve joined a project with no style guide, or a project with a
.rubocop.yml file that was added years ago. You run RuboCop, and you see something like this:
$ bundle exec rubocop 3306 files inspected, 12418 offenses detected
Flocks of noble
knights developers have tried to slay the beast fix the offenses, but they’ve all given 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 and counts all the offenses
- Then, it generates a
.rubocop_todo.ymlwhere all the current offenses are ignored
- And finally, it makes
This is the way to define the status quo and enforce style checks for new code only. 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 an “anything goes” situation, and that defeats the purpose of keeping code style consistent in the first place.
What does that mean for the typical legacy codebase? Most new code will be ignored by RuboCop, too. We’ve made the tool happy, but are we happy with it?
Luckily, 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 cops from being completely disabled.
.rubocop_todo.yml file won’t affect your new files or entirely new offenses in the old ones.
RuboCop not only helps with style—it also saves you from common mistakes that can break your code in production, as posed by these questions:
- What if you had some bugs and had ignored the corresponding cops 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
.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 cops like this into a
.rubocop_strict.yml configuration file like this:
Lint/Debugger: # don't leave binding.pry or debugger 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 place the Strict config right after the TODO config file 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 convincing everyone on the team to always use double-quotes for strings, or to add trailing commas to multiline arrays, or to \<choose-your-own-controversal-style-rule>? We’re 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
And that’s okay. RuboCop’s default configuration is not a golden standard, and it was never meant to be the “one style to fit them all”.
Should the Ruby community have that “one 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 a 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 contains a mapping of source files to the cop names which produce offenses for these files. When you run Standard in the future it will ignore these errors.
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: # Standard's config uses custom cops, # so it must be loaded - standard # rubocop-performance is required when using Performance cops - rubocop-performance inherit_gem: standard: config/base.yml # You can also choose a Ruby-version-specific config # standard: config/ruby-3.0.yml # Standard plugins must be loaded separately (since v1.28.0) standard-performance: config/base.yml standard-custom: config/base.yml inherit_from: - .rubocop_todo.yml - .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.2
This 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 RuboCop’s default. A tiny trade-off if you think about the benefits of no more style arguments.
Don’t forget to add
standard to your Gemfile and freeze its minor version to avoid unexpected failures during upgrades:
gem "standard", "~> 1.28", require: false
Although the approach above allows you to tinker with the Standard configuration, I wouldn’t recommend doing so. Use this flexibility to extend the default behavior, not to change it!
Beyond the Standard
RuboCop has a lot of plugins distributed as separate gems:
rubocop-md, to name a few.
Standard only includes the
rubocop-performance plugin. We usually add
rubocop-rspec to our configuration.
For each plugin, we keep a separate YAML file in the
.rubocop/ folder (to avoid polluting the project root):
.rubocop/rspec.yml, etc. Only the
.rubocop_todo.yml stays in the project’s root—it’s better to keep it in the default location familiar to Ruby developers.
Inside the base config we add these files to
inherit_from: - .rubocop/rails.yml - .rubocop/rspec.yml - .rubocop_todo.yml - .rubocop/strict.yml
.rubocop/rails.yml is based on the configuration that existed in Standard before they dropped Rails support.
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: - standard - rubocop-performance inherit_gem: standard: config/base.yml standard-performance: config/base.yml standard-custom: config/base.yml inherit_from: - .rubocop/rails.yml - .rubocop/rspec.yml - .rubocop/custom.yml - .rubocop_todo.yml - .rubocop/strict.yml AllCops: SuggestExtensions: false TargetRubyVersion: 3.2
Feel free to use this as inspiration for your projects that are in need of some tough RuboCop love.
Sticking with RuboCop or switching to Standard CLI?
Standard is designed to be a standalone tool, an opinionated (“no decisions to make”) Ruby code linter and formatter. If you take a look at its documentation, you can see that the recommended way of standardizing the codebase is to use the
standardrb CLI along with a
.standard.yml configuration file. So, why do we recommend using RuboCop in the first place?
For a long time (the first version of this post was published in 2020), the main argument was the lack of extension support in Standard. However, recent versions have introduced the
extend_config configuration option, and this has made it possible to load additional RuboCop configurations from separate YAML files. It’s a game-changer! Here’s how we can migrate the configuration we introduced above to Standard:
# .standard.yml parallel: true format: progress extend_config: - .rubocop/rails.yml - .rubocop/rspec.yml - .rubocop/custom.yml
Unfortunately, it’s not fully compatible with the
.rubocop.yml file we had before:
.rubocop/strict.yml are missing. This is because Standard’s
extend_config option does not allow overriding configuration; it only supports adding new cops.
We can replace
.standard_todo.yml for sure. But there is no replacement for
Similarly, if you decide to go slightly off the standard path, and, for example, try to disable a cop globally in the custom config file, this wouldn’t work.
Thus, we still prefer to use
standardrb in larger projects with a lot of history. For greenfield projects,
standardrb is a viable choice, especially if you manage to automate its execution, so developers don’t need to think which command to run,
RuboCop vs. Bundler vs. Gemfile
The standard way of adding RuboCop to a project is adding it as a dependency to the Gemfile:
# Gemfile group :development do # Technically, we don't need to specify "rubocop" here, # since "standard" declares it as a dependency gem "rubocop", "~> 1.0" gem "standard", "~> 1.28" end
With every added RuboCop plugin, the Gemfile grows. What’s wrong with a large number of dependencies in a Gemfile? Two things:
1) Time. More precisely, the time it takes Bundler to verify the dependencies. And since we run
bundle exec, the overhead also affects the time to lint the code.
2) Readability. The less you scroll through the Gemfile to learn about dependencies the better. A dozen lines of code is probably not a big deal but still can be considered as a point for improvement.
How can we refactor a Gemfile to solve these problems? We can extract RuboCop dependencies into a separate Gemfile! Since linter is a static analysis tool and not required in runtime, we can freely keep a separate manifest (a Gemfile) and a lock-file for it.
Let’s create a Gemfile for our RuboCop configuration and put it in the
# gemfiles/rubocop.gemfile source "https://rubygems.org" do gem "rubocop-rails" gem "rubocop-rspec" gem "standard", "~> 1.28" end
Now we have two options: we can include this Gemfile in the main one via the
eval_gemfile "gemfiles/rubocop.gemfile" expression, or we can use it independently. In the second case, we can create a simple
bin/rubocop executable to automatically install the dependencies and use the correct Gemfile:
#!/bin/bash cd $(dirname $0)/.. export BUNDLE_GEMFILE=./gemfiles/rubocop.gemfile bundle check > /dev/null || bundle install bundle exec rubocop $@
As a positive side-effect, we can now run RuboCop locally even if using Docker for development. No need to install all the project dependencies; having (any version of) Ruby installed is sufficient to lint the code. That’s the third reason to extract RuboCop dependencies into a separate Gemfile.
Rubocoping application template
To help you adopt the standardized RuboCop configuration, we’ve created an interactive generator that makes introducing linting and style to your Rails application as simple as running a single command:
rails app:template LOCATION="https://railsbytes.com/script/V4YsLQ"
For non-Rails projects, you can use Ruby Bytes:
rbytes install https://railsbytes.com/script/V4YsLQ
Here is an example run of the generator:
The source code of the generator can be found in the evilmartians/rubocoping-generator repo.
RuboCop plays a vital role in the Ruby world and will remain #1 for linting and formatting code for quite a long time. Don’t ignore RuboCop; write code in style 😎
- Upgrade to Standard v1.28.0 with
- Added rubocoping-generator.
Standard now supports configuration extensions.
Changed to use the
.rubocop/*.ymlapproach by default.
TargetRubyVersionto the example configuration.
- Added a note about Standard’s version-specific configs.
- Added 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.
- Mentioned 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.