id | title |
---|---|
rbi |
RBI Files |
RBI files are "Ruby Interface" files. Sorbet uses RBI files to learn about constants, ancestors, and methods defined in ways it doesn't understand natively. By default, Sorbet does not know about:
- anything defined within a gem
- ancestors modified with dynamic includes (
other.extend(...)
) - constants accessed or defined with
const_get
orconst_set
- methods defined with
define_method
ormethod_missing
RBI files lie at the intersection of the static and dynamic components of Sorbet. They can be autogenerated using Ruby's reflection APIs at runtime or written by users of Sorbet. However, RBI files have no runtime impact--they are not used for any sort of runtime checking.
This doc answers three main questions:
- How can we create and update RBI files?
- What is the syntax of an RBI file?
- What kinds of RBI files exist in a project?
We'll cover these questions in order.
These are the many commands to create or update RBI files within a Sorbet project:
I want to: | so I'll run: |
---|---|
Initialize a new Sorbet project, including all RBI files | tapioca init |
Fetch pre-written RBIs from rbi-central |
tapioca annotations |
(Re)generate RBIs for all gems using runtime reflection | tapioca gems |
(Re)generate RBIs for all DSLs such as Rails using runtime reflection | tapioca dsl |
(Re)generate an RBI for all "hidden definitions" in a project (deprecated) | srb rbi hidden-definitions |
(Re)generate a the TODO RBI file (for missing constants) | tapioca todo |
For more information about tapioca init
, see Adopting Sorbet.
The contents of the sorbet/
folder after initialization look like this:
sorbet/
│ # Default options to be passed to sorbet on every run
├── config
└── rbi/
│ # Community-written type definition files for your gems
├── annotations/
│ # Autogenerated type definitions for your gems
├── gems/
│ # Autogenerated type definitions for your DSLs such as Rails
├── dsl/
│ # Things defined when run, but hidden statically (only if running the deprecated `srb rbi hidden-definitions` command)
├── hidden-definitions/
│ # Constants which were still missing
└── todo.rbi
Note how the tapioca rbi
subcommand names match the folders within the
sorbet/rbi/
folder.
The tapioca
subcommands and different kinds of RBI files are discussed below.
See also https://github.com/Shopify/tapioca for more details.
The syntax of RBI files is the same as normal Ruby files, except that method definitions do not need implementations. The only new syntax is for method signatures (which are themselves valid Ruby syntax). See Writing sigs for the syntax of method signatures.
# -- example.rbi --
# typed: strict
# Declares a module
module MyModule
end
# Declares a class
class Parent
# Declares what modules this class mixes in
include MyModule
extend MyModule
# Declares an untyped method
def foo
end
# Declares a method with input and output types
sig {params(x: Integer).returns(String)}
def bar(x)
end
end
# Declares a class that subclasses another class
class Child < Parent
# Declares an Integer constant, with an arbitrary value
X = T.let(T.unsafe(nil), Integer)
end
RBIs are predominantly used to define classes, constants, and methods to Sorbet. Sorbet does not look at the source code of gems that a project depends on (in general, this would make Sorbet slower to typecheck a project and also toilsome if any gems don't typecheck on their own).
Instead, there are three ways to get RBI files to teach Sorbet about a gem:
- By using
tapioca
to create RBIs using runtime reflection. - By handwriting RBIs for a gem, or using handwritten RBIs the Sorbet community has shared.
- By including RBI files directly within the source of a gem.
We'll discuss each one of these in turn.
The most common way to get RBI files for gems is to generate (or regenerate) them. To regenerate RBI files in an already-initialized project:
# Update only the autogenerated gem RBIs for new gems:
❯ bin/tapioca gems
# Update autogenerated gem RBI for a specific gem:
❯ bin/tapioca gem <gem-name>
# Update autogenerated gem RBIs for all gems:
❯ bin/tapioca gems --all
The way this works is that tapioca
will load a project's Gemfile and require
each gem declared within. While loading each gem, it uses runtime reflection to
learn what things that gem defines, and then serializes this information into a
single RBI file for that gem.
This process is somewhat imperfect, but it's a good start. Importantly, because it only loads a gem's code, it can only know the arity of methods defined, but not what types of values the methods accept or return.
For gems that have a normal default require and that load all of their constants
through that, everything should work seamlessly. However, for gems that are
marked as require: false
in the Gemfile, or for gems that export constants
optionally via different requires, Tapioca will not be able to load everything
automatically. In these cases it is possible to
configure how Tapioca should require your gems.
An alternative to autogenerated RBI files are hand-written RBI files. For example, Sorbet itself ships with hand-written RBIs for the Ruby standard library. Hand-written RBI files have the advantage that in addition to capturing a method's arity, they can also declare a method's input and output types.
Hand-written RBIs for gems can come from either:
rbi-central
, a central repository for sharing hand-written RBI files with the Sorbet community, or- you!
When initializing a new Sorbet project, tapioca init
reads the project's
Gemfile
, checks to see if rbi-central
already has any suitable RBI files,
and fetches them into the current project if so. After initialization, to update
or add new RBI files from rbi-central
, run:
❯ bin/tapioca annotations
Note: With
srb rbi
moving to maintenance mode,rbi-central
has shifted to being the main repository of community-sourced RBI files for open source gems, replacingsorbet-typed
.
When rbi-central
does not have RBI files for a gem, tapioca init
will still
have created some autogenerated RBI files for that gem. These autogenerated
files are a great way to start off writing hand-written RBIs for a gem!
Simply copy the autogenerated file from the sorbet/rbi/gems/
folder to
sorbet/rbi/shims/gems
, and start modifying it by hand.
We expect that as adoption grows, gems will include their external interfaces
RBI files in an rbi/
directory. When they do this, Tapioca will automatically
merge those definitions in the autogenerated RBIs. In the future, we anticipate
this to be the preferred way to include RBI files into a project.
There is no versioning for gems whose RBIs live in Sorbet itself. Instead, Sorbet tries to provide a best effort one-size-fits-all set of RBIs.
Consider some examples:
-
A new version of Ruby adds a method to a class defined in the standard library.
We will accept PRs to add an RBI definition for the new method. This means that Sorbet will not report "method does not exist errors" for codebases that are still on the old version of Ruby.
-
A new version of Ruby deletes a method that was deprecated.
We will not accept PRs to remove the corresponding method definition from Sorbet, as that would prevent users on old Sorbet versions from calling it.
While this is a general rule, we do make exceptions on a case-by-case basis. Please open an issue if you're unsure.
It does. The primary way it matters is that Sorbet only checks "Method does not
take a block" errors for methods defined in # typed: strict
files. If an RBI
file is # typed: true
, Sorbet will never report "Method does not take a block"
errors for methods defined inside it, even if a given method logically takes no
block.
See Methods that take no blocks for more.
Note: At the moment,
this issue in Sorbet prevents
using # typed: strict
to require that all definitions in an RBI file have
explicit type annotations. This is not intentional and will be fixed in a future
version of Sorbet.
Sorbet by itself does not understand DSLs involving meta-programming, such as
Rails. This means that Sorbet won't know about constants and methods generated
by ActiveRecord
or ActiveSupport
. To solve this, Tapioca can load your
application and introspect it to find the constants and methods that would exist
at runtime and compile them into RBI files.
To generate the RBI files for the DSLs used in your application:
# Update the autogenerated DSL RBIs:
❯ bin/tapioca dsl
# Update the autogenerated DSL RBI for a specific constant:
❯ bin/tapioca dsl <ConstantName>
You can read about all the DSL RBI compilers supplied by Tapioca in the Tapioca's manual.
The Hidden Definitions RBI
The
srb rbi hidden-definitions
command is in maintenance mode, please usetapioca
instead.This command will be removed in future versions and all RBI generation will be done through Tapioca.
Add Tapioca to your Gemfile then run
bundle install
to install it:gem "tapioca", require: false, :group => [:development, :test]Once Tapioca is installed, simply run
tapioca init
to initialize your project with Sorbet and generate the necessary RBI files:❯ bundle exec tapioca init
When typechecking an existing Ruby project with Sorbet, usually type annotations
for all the gems are not enough to statically understand everything that's going
on. Ruby as a language is well-known for encouraging metaprogramming, or
defining things at runtime. In Sorbet, we call anything that can't be seen
statically but which is defined at runtime a hidden definition. This
includes constants defined with const_set
, methods defined with
define_method
, ancestor lists modified within methods (instead of at the
top-level of a class), and more.
In order to find all the hidden definitions in a project, Sorbet does three things:
- loads an entire project, and walks every object in
ObjectSpace
to see what's defined - runs Sorbet on the entire project in a mode where Sorbet will emit something for every definition
- subtract these two lists of definitions, and serialize an RBI file for the difference
Like with autogenerated gem RBI files, the methods defined in the hidden definitions RBI file will all be untyped.
Unlike gem RBIs which only need to be updated when a gem is added, removed, or updated, the hidden definitions might need to be updated frequently, depending on how much metaprogramming a codebase is using. To regenerate the hidden definitions RBI file:
❯ srb rbi hidden-definitions
On a philosophical level, we believe that while heavily metaprogrammed APIs can make it easy for a code author, they're frequently harder for consumers of the API to understand where things are being defined. By making metaprogramming explicit in one file, it's easy to track whether the amount of metaprogramming in a codebase is going up or down over time.
Sorbet offers a number of powerful type-level features to enforce ergonomic abstractions. When writing new code or refactoring old code, be sure to read up on Abstract methods and interfaces and Union types, both of which allow code authors to write code that Sorbet can understand and analyze more effectively.
Sometimes even autogenerated gem RBI files and the hidden definitions RBI file aren't enough to adopt Sorbet in a new codebase. Sorbet requires that all constants resolve, and our runtime reflection to find constants is still imperfect.
As one last attempt to ease the adoption process of Sorbet, when running
tapioca init
if after creating all these RBI files there are still missing
constants, we'll write out an RBI file that defines them, regardless of whether
they exist at runtime or not. This can be dangerous! In particular, at this
point we're not checking whether something was actually defined but Sorbet
couldn't see it, or whether it was never defined and is actually buggy code that
hasn't been caught by a project's test suite!
That's why this file is called the TODO RBI. After initializing a project to use
Sorbet, take a glance over the file at sorbet/rbi/todo.rbi
and attempt to find
out why Sorbet thinks these constants are missing. Ask: is the code actually
covered by tests? Does the constant exist in an irb
or pry
REPL session?
If after inspecting a constant in the TODO RBI file you're sure it should always exist at runtime, feel free to move it out of the TODO RBI file and into a hand-written RBI file.
You might have noticed that we vendor all gems' RBI files into the current directory, and commit them to version control. Why? When developing RBI files for Sorbet, we referenced the prior art that Flow developed. Our reasoning is the same as theirs:
When an RBI is improved or updated, there's some chance that the change could introduce new Sorbet errors into the project. As good as it is to find new issues, we also want to make sure that Sorbet errors in a project are consistent and predictable over time.
So if/when we wish to upgrade an RBI that we've already checked in to our project's version control, we can do so explicitly with the related Tapioca command.