On How We Use RuboCop and StandardRB

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 opens a new window (and RuboCop opens a new window ) 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 opens a new window . 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 or rubocop-minitest)

(*) We use the Dual booting technique opens a new window 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 opens a new window , 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 no todo, less complexity, less things to remember, and no hidden offenses!

Pre-commit Hook

For our internal projects, we use Overcommit opens a new window 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 opens a new window

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.

Get the book