Post

Reviving Sorbet in a Rails project

When I started with my current company a year ago, we had sorbet sigs and sigils everywhere in the code, but we didn’t enforce any type checking. At some point, Sorbet broke, was turned off in CICD, and never turned back on. Here is how I revived it.

A few things I wanted to achieve in the process

Minimal code changes

I knew would be touching a lot of files, and I wanted to minimize any code changes. As much as possible, I avoided rewriting any sigs and, especially, any actual code. Instead, I chose to change the strictness of files to false or ignore when faced with Sorbet errors.

A Reviewable PR

I knew a lot of changes would be necessary, and I wanted my colleagues not to be overwhelmed by a PR with 3500 file changes. After each step, I made a commit and recommended making each commit message obvious as to whether it was human or machine-changed code. I never mixed autogenerated code and manual code changes.

1
2
git commit -m "[me] Updated the gemfile"
git commit -m "[machine] Found and replaced true sigils to false"

No new runtime errors

Since many of our sigs could be out of date, I didn’t want our prod environment to throw any new errors. See the step below for how to accomplish this.

The Process

Update the gem file with the latest version of the Sorbet and Tapioca gems

1
2
3
  gem "sorbet-runtime", "~> 0.5.11221"
  gem "sorbet", "~> 0.5.11221"
  gem "tapioca", "~> 0.11.14", require: false

Dealing with sorbet-rails

In our project, we are using sorbet-rails, which is now deprecated. In addition to generating RBI files, sorbet-rails provides some methods like find_n, which wraps some ActiveRecord methods. In an effort to change the least amount of code, I opted to leave this in place for now.

Nuke the current Sorbet folder

1
rm -rf sorbet

Make a note of the config files as they may be helpful later.

Add some Rubocop rules

Add a couple of Rubocop rules to make Sorbet happy. Ideally, add these in a separate PR before you start this process

1
2
3
4
5
6
7
8
9
10
11
12
require: rubocop-sorbet

...your other rules

Style/KeywordParametersOrder:
  Enabled: true

Sorbet/ValidSigil:
  RequireSigilOnAllFiles: true
  Exclude:
    - test/**/*

Run rubocop with auto-correct

1
bundle exec rubocop -a

Tapioca init

1
bundle exec tapioca init

Edit sorbet config

Update the sorbet/config file to ignore the directories that sorbet shouldn’t be looking at. Here is what mine looked like:

1
2
3
4
5
6
7
8
--dir
.
--ignore=tmp/
--ignore=vendor/
--ignore=db/
--ignore=test/
--ignore=cypress/
--ignore=script/

Update true sigils to false

Use find and replace. I suggest using a clean install of VScode that is not burdened by your plugins and auto formatters. I used the Visual Studio Insider version because it can be installed along with your current copy of Visual Studio.

1
# typed: true -> #typed: false

Update the other sigils

  1. Leave ignore, as they are probably like that for a reason
  2. Your strict and strong files are where things get a bit tricky. In our code base, some strict files were using abstract! and when changing these to anything less than strict, it caused more errors than it prevented. So leave strict in place for any file using abstract!

Create DSL RBIs

1
bundle exec tapioca dsl

Fix Sorbet Errors

At this point, you should be in a pretty good place. Run Sorbet and manually fix any issues. I also had to ignore some gems in Tapioca.

1
bundle exec srb tc

Install Spoom

Spoom has some great tools, especially bump, which we will use in the next step

1
gem "spoom"

Spoom Bump

1
2
3
bundle exec spoom bump --from false --to true
bundle exec spoom bump --from true --to strict
bundle exec spoom bump --from strict --to strong

Sorbet run-time

The Sorbet team highly recommends throwing any TypeErrors in production. Although I agree with this, I wouldn’t recommend it for a revived sorbet project—at least not at first.

Here is a quote from the Sorbet documentation explaining why throwing errors is important:

“Runtime checks have been invaluable when developing Sorbet and rolling it out in large Ruby codebases like Stripe’s. Type annotations in a codebase are near useless if developers don’t trust them (consider how often YARD annotations fall out of sync with the code… 😰”

To log errors instead of throwing them, create a file called config/initializers/sorbet.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# typed: strict

require 'sorbet-runtime'

# (1) Register call_validation_error_handler callback.
# This runs every time a method with a sig fails to type check at runtime.
T::Configuration.call_validation_error_handler = lambda do |signature, opts|
  if ENV["SORBET_RUNTIME_ERRORS_LOG_ONLY"] == "true"
    MyLogger.present? && MyLogger.notify(opts[:pretty_message], tags: "sorbet", error_class: "TypeError")
  else
    raise TypeError.new(opts[:pretty_message])
  end
end

Update readme.md, CICD, and git hooks

  1. Update the readme with helpful commands for your team
  2. Update git hooks to run srb tc before pushes
  3. Update CICD to enforce type checking

Summary

This is by no means a complete instruction set for updating any out-of-date Sorbet install, but hopefully it gives you a start to getting Sorbet back up and running in your project.

This post is licensed under CC BY 4.0 by the author.

Trending Tags