Rubocop... but SemVer
Published: 2022-04-11 06:12:09

> [Cover Photo](https://unsplash.com/photos/yaSLNLtKRIU) by [Yusuf Evli](https://unsplash.com/@yusufevli)

---

I maintain dozens of gems, with varying requirements for supporting Ruby releases from "ye olden tyme".  Every time I dust off a gem to update it, removing support for old Rubies as I go, I have to maintain a high level of awareness on the [`rubocop`](https://github.com/rubocop/rubocop) version.

Many times I have forgotten the need to keep `rubocop` pegged within a seemingly arbitrary range of ancient versions and have spent valuable time upgrading the code to newer Rubocop releases, and newer Ruby syntax. Too often I remember the old Ruby constraint only once the updates hit CI.

> "Oh *hit I have to support Ruby version `Aught-Naught`"

And I proceed to undo all the work I just did.

Then I have to go check if there is a newer-but-still-supporting-old-Rubies, version of `rubocop` I can upgrade to instead.

# Compound Interest

$1: `rubocop` doesn't use [SemVer](https://semver.org/) (Semantic Versioning) with their releases.  NOTE: There is [ongoing discussion](https://github.com/semver/semver/issues/716#issuecomment-1115768846) about what SemVer means, which is a topic for another time.  Rubocop claims to be ["arguably SemVer"](https://metaredux.com/posts/2022/04/21/rubocop-turns-10.html?ht-comment-id=6148679), and [I disagree](https://github.com/semver/semver/issues/716#issuecomment-1115128186), hence this new suite of RubyGems.

$2: As of April 21, 2022 there are [201 releases](https://metaredux.com/posts/2022/04/21/rubocop-turns-10.html) of `rubocop`.  Two hundred and one.  It's a lot to dig through, and I have done it too many times. A significant chunk of the server load I have personally placed on rubygems.org was me doing this.

$3: The real dependency, the one that prevents the `rubocop` upgrade, is the minimum version of Ruby the library needs to support, conflicting with either the minimum `TargetRubyVersion` supported by RuboCop, or the `required_ruby_version`. Unfortunately the `TargetRubyVersion` dependency can't be resolved by [`bundler`](https://bundler.io/), which makes it impossible to automatically coordinate `TargetRubyVersion` with the `.gemspec`'s `required_ruby_version`.

$4: Dependency greening tools, like GitHub's `dependabot`, or the excellent alternatives [`depfu`](https://depfu.com/), and [`renovate`](https://www.whitesourcesoftware.com/free-developer-tools/renovate/) will all send a PR whenever a new version of `rubocop` comes out, asking to upgrade from _ancient_ to _hot-right-now_. While this is often a non-starter for a library, the repeated *invalid* PRs can be a time sink, and a distraction.

# ☁️ Imagine ☁️

☁️ A set of gems making clear the dependency between rubocop and a minimum Ruby version
☁️ Never researching the "right" `rubocop` version for a library again?
☁️ Automatically constraining the dependency on a minimum Ruby version to a maximum Rubocop version in a way that `bundler` supports!
☁️ Never seeing another *invalid* dependency greening PR about `rubocop` needing to be upgraded.
☁️ Adding an optional `inherit_gem` statement to your `.rubocop.yml` to bring in a set of Rubocop rules that will maximize developer happiness for whatever version of Ruby your project uses!

# 🚀 Make it so! 🚀

💎 Ruby 3.1 + `ruboocop ~> 1.29.1` + SemVer 2.0 = [`rubocop-ruby3_1`](https://github.com/rubocop-lts/rubocop-ruby3_1)
💎 Ruby 3.0 + `ruboocop ~> 1.29.1` + SemVer 2.0  = [`rubocop-ruby3_0`](https://github.com/rubocop-lts/rubocop-ruby3_0)
💎 Ruby 2.7 + `ruboocop ~> 1.29.1` + SemVer 2.0  = [`rubocop-ruby2_7`](https://github.com/rubocop-lts/rubocop-ruby2_7)
💎 Ruby 2.6 + `ruboocop ~> 1.29.1` + SemVer 2.0  = [`rubocop-ruby2_6`](https://github.com/rubocop-lts/rubocop-ruby2_6)
💎 Ruby 2.5 + `ruboocop = 1.28.2` + SemVer 2.0 = [`rubocop-ruby2_5`](https://github.com/rubocop-lts/rubocop-ruby2_5)
💎 Ruby 2.4 + `ruboocop = 1.12.1` + SemVer 2.0 = [`rubocop-ruby2_4`](https://github.com/rubocop-lts/rubocop-ruby2_4)
💎 Ruby 2.3 + `rubocop = 0.81.0` + SemVer 2.0 = [`rubocop-ruby2_3`](https://github.com/rubocop-lts/rubocop-ruby2_3)
💎 Ruby 2.2 + `rubocop = 0.68.1` + SemVer 2.0 = [`rubocop-ruby2_2`](https://github.com/rubocop-lts/rubocop-ruby2_2)
💎 Ruby 2.1 + `rubocop = 0.57.2` + SemVer 2.0 = [`rubocop-ruby2_1`](https://github.com/rubocop-lts/rubocop-ruby2_1)
💎 Ruby 2.0 + `rubocop = 0.50.0` + SemVer 2.0 = [`rubocop-ruby2_0`](https://github.com/rubocop-lts/rubocop-ruby2_0)
💎 Ruby 1.9 + `rubocop = 0.41.2` + SemVer 2.0 = [`rubocop-ruby1_9`](https://github.com/rubocop-lts/rubocop-ruby1_9)
💎 Ruby 1.8 - Just kidding, lulz. Ain't nobody got time for that.

Did you notice that many of the above are locked to a specific version with `=`, and the rest are locked to patch-level updates (`~>`)? That's [important for SemVer](https://github.com/rubocop/rubocop/pull/10577/files)!

When a gem follows SemVer, it is generally safe to add version constraints at the minor version level like so `~> 1.28`. In the `rubocop` scenario, this isn't safe, becuase `1.29` could be pulled in with that constraint, and it may drop support for your `TargetRubyVersion` while also potentially _not_ changing the `required_ruby_version`. While in the PR linked above they are changed in tandem, the rubocop documentation is [careful to *note*](https://github.com/rubocop/rubocop/blob/master/docs/modules/ROOT/pages/compatibility.adoc#support-matrix) that they are not equivalent and may not change together.

RuboCop released 1.29.0 with both install and `TargetRubyVersion` support for Ruby 2.5 dropped, exactly as in the PR above, and when this was [pointed out](https://github.com/rubocop/rubocop/issues/10625) the change was [reverted](github.com/rubocop/rubocop/pull/10626), and 1.29.1 supports 2.5 as `TargetRubyVersion`, but still not install.

This is a great example of where placing a buffer between your project and RuboCop might prove salvific of your time and sanity.  That my [initial complaint](https://github.com/semver/semver/issues/716#issuecomment-1115758167) was misdirected to SemVer proper, and ineffective, is a great example of why using the proper forum and framing for an issue is important.  I hadn't fully understood the framing I needed initially.

Ultimately the message was received and it looks like RuboCop will begin partially following SemVer with version 1.29.1. It appears that `TargetRubyVersion` drops (not runtime drops) will trigger a major version update going forward.  In addition, support for EOL'd rubies has been backhauled into HEAD: [ruby 2.5](https://github.com/rubocop/rubocop/pull/10626), [ruby 2.4](https://github.com/rubocop/rubocop/pull/10632), [ruby 2.3](https://github.com/rubocop/rubocop/pull/10640), [ruby 2.2](https://github.com/rubocop/rubocop/pull/10644).

# ≐ Take it to the limit ≐

Assuming those gems exist, what else could they do for us? Perhaps provide a `rubocop.yml` with a few defaults so you don't need to manage them anymore?

```yaml
inherit_gem:
  rubocop-ruby3_1: rubocop.yml
```

Would replace these lines:
```yaml
AllCops:
  TargetRubyVersion: 3.1
  NewCops: enable
```

> NOTE: Some of the gems have more, or different, directives than those in the example above. `rubocop-ruby1_9` even brings back Ruby 1.8.7 [support](https://github.com/rubocop-lts/rubocop-ruby1_9#what-about-ruby-18) straight out of the [grave 🪦](https://github.com/rubocop/rubocop/issues/2640#issuecomment-171787177).

Why these directives, specifically?

## TargetRubyVersion

Allowing this gem to manage the target ruby version means you can switch to a different gem within the family when you upgrade to the next version of Ruby, and have nothing else to change.  A single line in the `Gemfile`, and you are done.

## NewCops: enable

You may not use this setting in your project yet.  Upgrades to the latest `rubocop` can include all kinds of changes, including removing support for the version of Ruby your project uses, or adding a cop that may not work with some of your syntax (e.g. [some use cases of 'module_function`](https://github.com/rubocop/rubocop/issues/5953#issuecomment-805921993)).  Accepting new cops arriving in a new version of Rubocop can feel risky, especially when it doesn't follow SemVer.

But this set of gems shoehorns `rubocop` into SemVer... so `NewCops` is now safe(r)!

# Not Good Enough?

Check out the [next post](https://dev.to/pboling/rubocop-lts-1e31) for a gem, `rubocop-lts`, that wraps everything in this post up into a lovely single gem you can add to any project.