DEV Community

Cover image for Rubocop... but SemVer
Galtzo
Galtzo

Posted on

Rubocop... but SemVer

Cover Photo by Yusuf Evli


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 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 (Semantic Versioning) with their releases. NOTE: There is ongoing discussion about what SemVer means, which is a topic for another time. Rubocop claims to be "arguably SemVer", and I disagree, hence this new suite of RubyGems.

$2: As of April 21, 2022 there are 201 releases 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, 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, and 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
💎 Ruby 3.0 + ruboocop ~> 1.29.1 + SemVer 2.0 = rubocop-ruby3_0
💎 Ruby 2.7 + ruboocop ~> 1.29.1 + SemVer 2.0 = rubocop-ruby2_7
💎 Ruby 2.6 + ruboocop ~> 1.29.1 + SemVer 2.0 = rubocop-ruby2_6
💎 Ruby 2.5 + ruboocop = 1.28.2 + SemVer 2.0 = rubocop-ruby2_5
💎 Ruby 2.4 + ruboocop = 1.12.1 + SemVer 2.0 = rubocop-ruby2_4
💎 Ruby 2.3 + rubocop = 0.81.0 + SemVer 2.0 = rubocop-ruby2_3
💎 Ruby 2.2 + rubocop = 0.68.1 + SemVer 2.0 = rubocop-ruby2_2
💎 Ruby 2.1 + rubocop = 0.57.2 + SemVer 2.0 = rubocop-ruby2_1
💎 Ruby 2.0 + rubocop = 0.50.0 + SemVer 2.0 = rubocop-ruby2_0
💎 Ruby 1.9 + rubocop = 0.41.2 + SemVer 2.0 = 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!

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 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 the change was reverted, 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 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, ruby 2.4, ruby 2.3, ruby 2.2.

≐ 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?

inherit_gem:
  rubocop-ruby3_1: rubocop.yml
Enter fullscreen mode Exit fullscreen mode

Would replace these lines:

AllCops:
  TargetRubyVersion: 3.1
  NewCops: enable
Enter fullscreen mode Exit fullscreen mode

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 straight out of the grave 🪦.

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`). 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 for a gem, rubocop-lts, that wraps everything in this post up into a lovely single gem you can add to any project.

Top comments (0)