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
- Leave
ignore
, as they are probably like that for a reason - Your
strict
andstrong
files are where things get a bit tricky. In our code base, some strict files were usingabstract!
and when changing these to anything less thanstrict
, it caused more errors than it prevented. So leavestrict
in place for any file usingabstract!
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
- Update the readme with helpful commands for your team
- Update git hooks to run
srb tc
before pushes - 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.