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 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.
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.
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
(*) We use the Dual booting technique to test the current and next Rails versions, so we need to disable the
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.
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.
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.
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
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
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/**/*
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
TODO or not TODO
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:
.rubocop.ymlthat 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
.rubocop_with_todo.ymlthat 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.ymlfile and no
todo, less complexity, less things to remember, and no hidden offenses!
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"]
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.
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.
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.
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.