Franklin Hu

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!