RuboCoping with legacy
Tags: ruby, linters, formatters, rubocop
Published: 2020-03-24 21:38:35

_Originally posted in [Martian Chronicles](https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard)._

You will hardly find a Ruby developer who hasn't heard about [RuboCop][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.

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](https://github.com/testdouble/standard)-enforced style guides, and our own ~~patented~~ way to use Standard and RuboCop configs together.

## Style matters

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 ~~speak~~ write 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:

```sh
$ 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:

```sh
$ 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.yml` where all the current offenses are ignored;
- and finally, it makes `.rubocop.yml` inherit from `.rubocop_todo.yml`.

That is the way to set the status quo and only enforce style checks for new code. Sounds smart, right? Not exactly.

The way `.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.

> [This article](https://medium.com/@scottm/rubocop-in-legacy-projects-part-1-todos-and-todonts-877ace9f23b7) covers this problem in more detail.

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:

```sh
bundle exec rubocop –-auto-gen-config \
  --auto-gen-only-exclude \
  --exclude-limit=10000
```

Where `--auto-gen-only-exclude` force-excludes metrics cops instead of changing their `Max` value, and `--exclude-limit` sets the threshold for the exclusion (set to some large enough number to avoid disabling cops completely).

Now your `.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 `.rubocop_strict.yml`

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., `binding.pry`).
- `RSpec/Focus` (from [`rubocop-rspec`][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`](https://gist.github.com/palkan/ee1d0247be2076e38020a9a6fbae68d5) configuration file like this:

```yml
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:

```diff
 inherit_from:
-  - .rubocop_todo.yml
+  - .rubocop_strict.yml
```

The `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](https://github.com/rubocop-hq/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 `.rubocop.yml`.

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.

I think the main reason for that is the popularity of auto-formatters in other programming languages: JavaScript, Go, Rust, Elixir. Auto-formatters are usually very strict and allow almost none or zero configuration. And developers got used to that! People like writing code without worrying about indentation, brackets, and spaces; _robots_ would sort it all out!

> Checkout out the [lightning talk](https://www.youtube.com/watch?v=uLyV5hOqGQ8) and let Justin Searls convince you to switch to [Standard](https://github.com/testdouble/standard).

Thankfully, Ruby's ecosystem has got you covered: there is a project called [Standard](https://github.com/testdouble/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`).

Unfortunately, Standard lacks some RuboCop features that are essential at the early stages of a style guide's adoption: it is not possible to use `.rubocop_todo.yml` or any other local configuration. It also [doesn't support](https://github.com/testdouble/standard/issues/135) RuboCop plugins or custom cops.

But 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 `inherit_gem` directive:

```yml
# .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 this custom cop,
  # so it must be loaded
  - standard/cop/semantic_blocks

inherit_gem:
  standard: config/base.yml

inherit_from:
  - .rubocop_strict.yml

# Sometimes we enable metrics cops
# (which are disabled in Standard by default)
#
# Metrics:
#   Enabled: true
```

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:

```ruby
gem "standard", "~> 0.2.0", require: false
```

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

RuboCop has a lot of plugins distributed as separate gems: [`rubocop-rspec`](https://github.com/rubocop-hq/rubocop-rspec), [`rubocop-performance`](https://github.com/rubocop-hq/rubocop-performance), [`rubocop-rails`](https://github.com/rubocop-hq/rubocop-rails), [`rubocop-md`](https://github.com/rubocop-hq/rubocop-md), to name a few.

Standard only includes the `rubocop-performance` plugin. We usually add `rubocop-rails` and `rubocop-rspec` to our configuration.

For each plugin, we keep a separate YAML file: `.rubocop_rails.yml`, `.rubocop_rspec.yml`, etc.

Inside the base config we add these files to `inherit_from`:

```yml
inherit_from:
  - .rubocop_rails.yml
  - .rubocop_rspec.yml
  - .rubocop_strict.yml
```

Our [`.rubocop_rails.yml`](https://gist.github.com/palkan/24869b835c45e89116b9727b534e579e) is based on the [configuration that existed in Standard](https://github.com/testdouble/standard/commit/94d133f477a5694084ac974d5ee01e8a66ce777e#diff-65478e10d5b2ef41c7293a110c0e6b7c) before they dropped Rails support.

There is no standard RSpec configuration, so we had to figure out our own: [`.rubocop_rspec.yml`](https://gist.github.com/palkan/623c0816b05ed246bfe0cb406050990a).

We also usually enable a select few _custom_ cops, for example, [`Lint/Env`](https://github.com/evilmartians/terraforming-rails/tree/master/tools/lint_env).

In the end, our typical RuboCop configuration for Rails projects looks like this👇

```yml
# .rubocop.yml
inherit_mode:
  merge:
    - Exclude

require:
  - rubocop-performance
  - standard/cop/semantic_blocks
  - ./lib/cops/lint/env.rb

inherit_gem:
  standard: config/base.yml

inherit_from:
  - .rubocop_rails.yml
  - .rubocop_rspec.yml
  - .rubocop_strict.yml

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/**/*'
```

Feel free to use it as an inspiration for your projects that could use some RuboCop's 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](https://github.com/penelopezone/rubyfmt/) and [prettier-ruby](https://github.com/prettier/plugin-ruby)). Don't ignore RuboCop; write code in style 😎

---

Read more dev articles on [https://evilmartians.com/chronicles](https://evilmartians.com/chronicles)!



[rubocop]: https://docs.rubocop.org
[rubocop-rspec]: https://github.com/rubocop-hq/rubocop-rspec