Skip to content

Latest commit

 

History

History
371 lines (287 loc) · 14.8 KB

rbi.md

File metadata and controls

371 lines (287 loc) · 14.8 KB
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 or const_set
  • methods defined with define_method or method_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.

Quickref

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.

Syntax

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

Gem RBIs

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.

Autogenerated RBIs for gems

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.

Hand-written RBIs for 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, replacing sorbet-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.

RBIs within gems

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.

Versioning for standard library RBIs

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.

Does the sigil matter in an RBI file?

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.

DSL RBIs

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 use tapioca 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

For more information, please see Tapioca's README and wiki.

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.

The TODO RBI file

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.

A note about vendoring RBIs

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.