Typechecking Ruby templates with sorbet_erb
sorbet_erb is a Ruby gem that adds Sorbet typechecking support to code that’s embedded in ERB templates.
The problem
Rails projects that have adopted Sorbet and Tapioca are already able to statically typecheck most of their application code, but there aren’t any built-in affordances for Ruby that’s embedded in ERB view templates. The only way to find these bugs is to run them (either via automated tests or in some live environment).
Imagine a view where you’ve accidentally typoed constant name User
as Usr
:
# app/views/users/_list.html.erb
<ol>
<% Usr.all.each do |u| %>
<li><%= u.name %></li>
<% end %>
</ol>
sorbet_erb generates Ruby that mimics how the view is evaluated, so that instead you can get a typechecking error instead of needing to run a controller test or loading the page that includes this view.
sorbet/erb/views/users/_list.html.erb.generated.rb:23: Unable to resolve constant Usr https://srb.help/5002
23 | Usr.all.each do |u|
^^^
Did you mean User? Use -a to autocorrect
sorbet/erb/views/users/_list.html.erb.generated.rb:23: Replace with Usr
23 | Usr.all.each do |u|
^^^
app/models/user.rb:7: User defined here
7 |class User < ApplicationRecord
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Errors: 1
How it works
sorbet_erb uses better_html’s parser to walk the ERB’s AST and extracts any Ruby in code nodes. These lines are then templated out into a Ruby class that we can run Sorbet on.
# sorbet/erb/views/users/_list.html.generated.rb
class SorbetErb686bb6bf6c5b < ApplicationController
extend T::Sig
include ActionView::Helpers
include ApplicationController::HelperMethods
def body()
Usr.all.each do |u|
u.name
end
end
end
Strict locals and locals_sig
Rails 7.1 added support for strict locals. These require including a partial signature on a view specifying which local variables are expected.
# app/views/users/_form.html.erb
<%# locals: (user:) %>
<h1><%= user.name %></h2>
Without strict locals, it’d be impossible for Sorbet to tell whether user
is a valid symbol. sorbet_erb requires strict locals for partial.
sorbet_erb also (experimentally) supports adding type information via locals_sig
. This is a Sorbet-style signature that should match the parameters given in the strict locals definition, and is templated into the generated Ruby file.
# app/views/users/_form.html.erb
<%# locals_sig: sig { params(users: User).void } %>
<%# locals: (user:) %>
<h1><%= user.name %></h2>
Looking forward
While sorbet_erb can catch some bugs, it’s limited since often there’s no type information. This affects instance variables in non-partial views (if show.html.erb
uses @users
), which is often where most of the logic is.
Even if we generated code inside the controller’s class namespace, the instance variables would need to be declared in a way that Sorbet understands.
Maybe this is possible! I don’t have deep knowledge of Rails internals, so let me know if you have ideas!