Type Checking With Sorbet

The majority of the code in Homebrew is written in Ruby which is a dynamic language. To avail the benefits of static type checking, we have set up Sorbet in our codebase which provides the benefits of static type checking to dynamic languages like Ruby.

The Sorbet Documentation is a good place to get started if you want to dive deeper into Sorbet and its abilities.

Sorbet in the Homebrew Codebase

Inline type annotations

The sig method is used to annotate method signatures. Here’s a simple example:

class MyClass
  sig { params(name: String).returns(String) }
  def my_method(name)
    "Hello, #{name}!"
  end
end

With params we specify that there is a parameter name which must be a String, and with returns we specify that this method always returns a String.

For more information on how to express more complex types, refer to the official documentation:

Ruby interface files (.rbi)

RBI files help Sorbet learn about constants, ancestors and methods defined in ways it doesn’t understand natively. We can also create an RBI file to help Sorbet understand dynamic definitions. Some of these files are automatically generated (see the next section) and some are manually written, e.g. extend/on_system.rbi.

There are also a very small number of files that Homebrew loads before sorbet-runtime, such as utils/gems.rb. Those files cannot have type signatures alongside the code itself, so RBI files are used there instead to retain static type checking.

The Library/Homebrew/sorbet directory

The rbi directory contains all Ruby Interface (.rbi) files auto-generated by running brew typecheck --update:

The tapioca directory contains configuration files and compilers for Tapioca, allowing Sorbet to type check the dynamically generated components of the codebase.

The config file is a newline-separated list of arguments to pass to srb tc, the same as if they’d been passed on the command line. Arguments in the config file are always passed first, followed by arguments provided on the command line. We use it to ignore e.g. gem directories which we do not wish to type check.

Using brew typecheck

Every Ruby file in the codebase has a magic # typed: <level> comment at the top, where <level> is one of Sorbet’s strictness levels, usually false, true or strict. The false files only report errors related to the syntax, constant resolution and correctness of the method signatures, but no type errors. Our long-term goal is to move all false files to true and start reporting type errors on those files as well. Therefore, when adding new files, you should ideally mark it with # typed: true and work out any resulting type errors.

When run without any arguments, brew typecheck will run considering the strictness levels set in each of the individual Ruby files in the core Homebrew codebase. However, when run on a specific file or directory, more errors may show up since Sorbet cannot resolve constants defined outside the scope of the specified file. These problems can be solved with RBI files. Currently brew typecheck provides --quiet, --file, --dir and --ignore options, but you can explore more options with srb tc --help and pass them with srb tc.

Resolving Type Errors

Sorbet reports type errors along with an error reference code, which can be used to look up more information on how to debug the error, or what causes the error in the Sorbet Documentation. Here’s how to debug some common type errors:

Fork me on GitHub