On How We Use RuboCop and StandardRB
We all have been there, we work on a project and, over time, we write similar code in different ways (either by two different developers or by the same developer). We have two blocks of code that follow the same logic but look different; and we have to make an extra effort to understand them only because the code is written in a different way.
Defining a code style will prevent this, but we need a way to enforce it. In this article, we’ll show what’s our setup to use StandardRB (and RuboCop ) to improve the quality of the code by keeping a consistent style to help the developers.
StandardRB
StandardRB is a gem built on top of RuboCop that defines a set of rules . It uses RuboCop under the hood, but it also provides its own standardrb
executable and configuration options.
Why
We didn’t want to waste time defining our own specific set of rules for RuboCop so we decided to use StandardRB as a starting point. Most of the rules that it defines are a good fit for us.
Limitations
Eventually, we found some limitations:
- we had to change a rule(*) and the whole idea of adhering to StandardRB is that you won’t change the config
- StandardRB does not officially support RuboCop extensions (like
rubocop-rails
,rubocop-rspec
orrubocop-minitest
)
(*) We use the Dual booting technique to test the current and next Rails versions, so we need to disable the
Bundler/DuplicatedGem
rule
RuboCop
RuboCop is a linter/formatter for Ruby code. We didn’t want to follow the default rules since it required us to do many changes (like the default single/double quotes configuration for strings), and we wanted to use extensions to add new rules.
Extensibility
RuboCop can be extended using other gems that will define new cops. You can find a list of extensions here , and you can probably find more extensions that are not listed there or even build your own.
Setup
So, our setup uses RuboCop, but it uses the StandardRB configuration as the base on which we add only a few customizations (and only if we need to).
We start by adding the standard
gem and the extensions we want. StandardRB depends on RuboCop, so we don’t need to add that explicit dependency, but you can add it if you want to define a specific version.
# Gemfile
group :development, :test do
gem "standard", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
end
Then, we will use a .rubocop.yml
file in the root of the project to configure RuboCop.
Extensions
We have to tell RuboCop which extensions we want to use, so, at the beginning of the .rubocop.yml
file we add:
require:
- standard
- rubocop-rails
- rubocop-rspec
Inherit Config
Since we want to use the StandardRB configuration as the base configuration, we have to tell that to RuboCop too. We can do so by inheriting
the configuration from a gem:
require:
- standard
- rubocop-rails
- rubocop-rspec
inherit_gem:
standard: config/base.yml
Excluding Files
Sometimes you want to exclude some files or directories so RuboCop can ignore them. For example, if your project installs gems in the vendor
directory you don’t want RuboCop to process them, it just takes time for things you won’t fix. You may also not want RuboCop to check if there’s anything to analyze inside the node_modules
directory since it will just take time to do so when there’s probably no ruby code there ever.
So we add more configuration:
require:
- standard
- rubocop-rails
- rubocop-rspec
inherit_gem:
standard: config/base.yml
AllCops:
NewCops: enable
Exclude:
- node_modules/**/*
- public/**/*
- vendor/**/*
Configuring
Now that we have the base StandardRB configuration, we can add small tweaks to it.
require:
- standard
- rubocop-rails
- rubocop-rspec
inherit_gem:
standard: config/base.yml
AllCops:
NewCops: enable
Exclude:
- node_modules/**/*
- public/**/*
- vendor/**/*
Rails:
Enabled: true # enable rubocop-rails cops
RSpec:
Enabled: true # enable rubocop-rspec cops
RSpec/DescribeClass:
Enabled: false # ignore missing comments on classes
Bundler/DuplicatedGem:
Enabled: false # ignore duplicated gem errors because we will have duplicated gems when dual booting
On a Clean Project
If you are adding this to a new project, you are good to go with that config. You can bundle exec rubocop -A
so it applies any automatic style fix for your code. Just add all the changes and ship it!
On an Existing Project
If you are adding this to a project that already has a lot of code written, you’ll find that bundle exec rubocop -A
won’t be able to fix everything automatically. If the number of offenses that can’t be fixed is small you may want to fix them right away, but sometimes that’s not possible (maybe there are hundreds of offenses, or you need to discuss with your team on how to refactor a long method, etc).
For that we can use a todo
file that will include all the offenses that we should fix eventually, but will configure RuboCop to ignore them for now until we have the bandwidth to fix them.
If we run bundle exec rubocop --auto-gen-config
, RuboCop will create a .rubocop_todo.yml
file with all the TODOs we have to fix, and will update the .rubocop.yml
file to inherit the config from the .rubocop_todo.yml
file.
TODO or not TODO
While the todo
file is really helpful, it disables specific offenses for complete files, not only the specific lines found at the moment it was generated. It makes sense since the code changes and the todo
file can’t keep track of those changes, but that also adds room to introduce new offenses on a file that has a specific cop disabled.
Instead, we use 2 different config files:
- a
.rubocop.yml
that won’t inherit from the TODO, we can use this with the code editor linter so we always see offenses in every file we open during development - a
.rubocop_with_todo.yml
that will use the TODO, we can use this in a pre-commit hook or on CI to keep the TODO configuration in those cases (we don’t want to yell at developers for files they didn’t touch!)
First, we copy the .rubocop.yml
file as .rubocop_with_todo.yml
. Then, we remove the inherit_from: .rubocop_todo.yml
line from the .rubocop.yml
file so it does not include the TODO.
So, by default, RuboCop will use the .rubocop.yml
config, but we can tell it to use the .rubocop_with_todo.yml
when we run the rubocop
command with a flag: bundle exec rubocop -c .rubocop_with_todo.yml
Eventually, when you clear the TODO file, you can revert this to have only one
.rubocop.yml
file and notodo
, less complexity, less things to remember, and no hidden offenses!
Pre-commit Hook
For our internal projects, we use Overcommit to manage different git hooks. We use RuboCop in a pre-commit hook to detect code style offenses before those are pushed to the repo.
For this hook we want to use the TODO configuration, so the Overcommit config in this case will be:
# .overcommit.yml
PreCommit:
RuboCop:
enabled: true
---
PreCommit:
RuboCop:
enabled: true
command: ["bundle", "exec", "rubocop", "-c", ".rubocop_with_todo.yml"]
CI
Since Overcommit pre-commit hooks can be skipped or disabled, we also want to run RuboCop on CI. Again, in this environment we want to use the TODO, so we will have a CI job that runs bundle exec rubocop -c ./.rubocop_with_todo.yml
. The specific setup will depend on your CI service.
Development
We encourage all the developers to add a RuboCop plugin to the code editor of their choice so the editor will show linting hints for each file that’s open.
You can find a list of all the plugins for different editors here
In this case, we DON’T WANT to use the TODO, we want the developers to see all the offenses for every file. That way it makes it easier to know that we are not introducing new offenses, and it helps us when we want to go and fix them.
The different plugins will default to using the .rubocop.yml
file, so we have nothing to change here.
Formatter
Also, code editors can be configured to use RuboCop as a formatter that will fix code style for any file on save.
We could also run bundle exec rubocop -A
every time we want to fix the style, but the code editors can do that for us.
Conclusion
By defining and enforcing rules for the code we save the time wasted in discussions about code style, specially during code reviews. We can discuss specific things every now and then and adjust the configuration if needed, but almost all the time we decide to follow the enforced rules and focus on what the code does and not on how it looks.
Also, a consistent style helps onboarding developers in the different projects, they know what to expect so there’s no extra effort on learning new rules or understanding code written with multiple different styles.
Finally, with this setup using two files with and without the TODO, we prevent introducing new offenses by not hiding them for developers, but we also hide the known offenses on CI and the pre-commit hook to not have to fix all of them at once in a big code base.