rbs collection was released!
Tags: ruby
Published: 2021-09-17 17:49:03

Recently we released RBS v1.6 with `rbs collection` feature which is "Bundler" for RBS! The feature is experimental now, and it will be GA in RBS v2 for Ruby 3.1. Now you can use it with `gem install rbs` if you use Ruby 2.6+!

I'm an RBS maintainer and I designed and implemented the feature, so I'll describe `rbs collection` feature in this article.

By the way, I made a presentation on Sep. 10 at RubyKaigi Takeout 2021 and explained `rbs collection` in the talk. This article is based on the talk. See https://github.com/pocke/rubykaigi-2021  for more details (unfortunately I spoke in Japanese but it has English slides and subtitles).

## What is RBS

First of all, I introduce RBS.

RBS is a language to describe Ruby types. It is similar to `.d.ts` of TypeScript or header files of C-language.
The syntax is similar to Ruby.

The following code is a simple RBS example:

```rbs
class Blog
  attr_reader name: String

  def initialize: (String name) -> void

  def each_article: () { (Article) -> void } -> self # For blog.each_article { |article| ... }
                  | () -> Enumerator[Article, self]  # For blog.each_article.map { |article| ... }
end

class Article
  attr_reader title: String
  attr_reader content: String

  def initialize: (String title, String, content) -> void

  def published_at: () -> Time
end
```

## ruby/gem_rbs_collection

I need to describe another topic before describing `rbs collection`. It is [ruby/gem_rbs_collection](https://github.com/ruby/gem_rbs_collection) GitHub repository.

The repository stores third-party gems' RBSs, like [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped).

`rbs collection` feature integrates this repository and tools use RBS, such as `rbs` command, [Steep](https://github.com/soutaro/steep), and [TypeProf](https://github.com/ruby/typeprof).

## rbs collection

It's the main topic of this article💪

`rbs collection` manages third-party RBSs, for example, installing, updating, and loading.

The first section describes a motivating example of `rbs collection`.
Before `rbs collection`, managing third-party RBSs is not efficient.

The second section describes how `rbs collection` solves the problem.

The last section describes known issues of `rbs collection`.
The feature still has some known issues for now, and I'll solve the issues until RBS v2.

### Motivating example

In this section, I describe how to set up third-party RBSs for a Rails app without `rbs collection` as the motivating example.

#### How to set up

First, download ruby/gem_rbs_collection repository as a git submodule.

```bash
$ git submodule add https://github.com/ruby/gem_rbs_collection.git gem_rbs_collection
```

Then `rbs` command is available but it needs many options. For example, the following command is necessary to validate RBSs.

```bash
# too many options ¯\_(ツ)_/¯
$ rbs -rlogger -rpathname -rmutex_m -rdate -rmonitor \
      -rsingleton -rtsort -rtime -rrack \
      -ractivesupport -ractionpack -ractivejob \
      -ractivemodel -ractionview -ractiverecord \
      -rrailties \
      --repo=gem_rbs_collection validate

# TypeProf also needs the same options.
$ typeprof -rlogger -rpathname -rmutex_m -rdate -rmonitor \
           -rsingleton -rtsort -rtime -rrack \
           -ractivesupport -ractionpack -ractivejob \
           -ractivemodel -ractionview -ractiverecord \
           -rrailties \
           --repo=gem_rbs_collection validate
```

`-r` options specify library names you want to load, and `--repo` option specifies the repository path which is the git submodule.

And Steep needs a different configuration to load the same RBSs.

```ruby
# Steepfile

target :app do
  # Specify a directory that contains target Ruby files to type check.
  check 'app'
  # Specify the git submoduled directory
  repo_path "gem_rbs_collection"
  # Specify the all library names to load.
  library 'pathname', 'logger',  'mutex_m', 'date',
          'monitor', 'singleton', 'tsort', 'time', 'rack',
          'activesupport', 'actionpack', 'activejob',
          'activemodel', 'actionview', 'activerecord',
          'railties'
end
```

Many options are required because rbs, TypeProf, and Steep don't know which libraries are required.

#### Motivations

As you can see, managing third-party RBSs has problems. I explain three problems.

First, we need to resolve dependencies by hand. In the above examples, human resolved "my app depends on Active Support, then Active Support depends on Logger, and ...". It is too hard for humans 😂

Second, we need `git submodule` or copying RBSs to download third-party RBSs. It's too primitive. If you want to use different revisions RBSs, you can't use submodule but copying RBSs looks a hell.
For example, you want to use gem X RBS with revision R, but want to use gem Y RBS with revision R', `git submodule` doesn't work.

Third, we need to use different ways between RBS, Steep, and TypeProf to load RBSs. `rbs` command and TypeProf need to specify library names as command-line options, but Steep needs to specify them in `Steepfile`. It is not a single source so it is hard to maintain.

### How `rbs collection` solves the problem

`rbs collection` automatically resolves dependencies, downloads RBSs, and loads RBSs from the single source. It means `rbs collection` sovles these problems!

To use `rbs collection`, you need to install `rbs` gem v1.6+. Then initialize the configuration file with `rbs collection init` command.

```bash
# Install rbs gem v1.6+
$ gem install rbs

# Initialize the config file
$ rbs collection init
warning: rbs collection is experimental, and the behavior may change until RBS v2.0
created: rbs_collection.yaml
```

After that, edit the generated configuration file, `rbs_collection.yaml`. The full content is the following:
Note that I'll describe why you need to edit it in the "Known Issues" section.

```yaml
# Download sources
sources:
  - name: ruby/gem_rbs_collection
    remote: https://github.com/ruby/gem_rbs_collection.git
    revision: main
    repo_dir: gems

# A directory to install the downloaded RBSs
path: .gem_rbs_collection

gems:
  # Skip loading rbs gem's RBS.
  # It's unnecessary if you don't use rbs as a library.
  - name: rbs
    ignore: true
  # 👮👮👮 Add the following lines
  - name: pathname
  - name: logger
  - name: mutex_m
  - name: date
  - name: monitor
  - name: singleton
  - name: tsort
  - name: time
  - name: set
```

Then, execute `rbs collection install` command.

```bash
# rbs collection uses Gemfile.lock to resolve dependency,
# so execute `bundle install` before that to generate Gemfile.lock.
$ bundle install

# Install RBSs from ruby/gem_rbs_collection repo
$ rbs collection install
```

Finally, the CLI tools are available!

You can use rbs and TypeProf commands without options.

```bash
$ rbs validate
$ typeprof target.rb
```

You can use Steep with a few configurations.

```ruby
# Steepfile

target :app do
  # Specify a directory that contains target Ruby files to type check.
  check 'app'
end
```

Note that the `rbs collection` integrations with Steep and TypeProf have not been released yet now (2021-09-18). You need to install them from GitHub if you want to try them for now.
Steep integration was implemented in https://github.com/soutaro/steep/pull/420  but not yet released.
TypeProf integration was implemented in https://github.com/ruby/typeprof/pull/53  but not yet merged.

#### The mechanism of `rbs collection install`

`rbs collection install` command works with the following steps.

1. Generating `rbs_collection.lock.yaml`
   1. Resolve the dependencies with Gemfile.lock.
   2. Write the gems to the lockfile, `rbs_collection.lock.yaml`.
2. Installing RBSs
   1. Copy RBSs from ruby/gem_rbs_collection with the lockfile.

Then, `rbs`, `steep` and `typeprof` commands load the installed RBSs.

### Known Issues

Unfortunately `rbs collection` has known issues.

#### Resolving Standard Libraries' Dependencies

`rbs collection` doesn't resolve dependencies of most standard libraries. For example, `logger`, `pathname`, etc.

In many cases, standard libraries do not appear in `Gemfile.lock` so `rbs collection` can't find them from `Gemfile.lock`.
We can do `require` stdlibs even if the stdlib is not included in gemspec or Gemfile. So most gems don't have explicit dependencies to stdlibs. For example, `rails` gem depends on many stdlibs such as `pathname`, but the gemspec doesn't include `pathname`.

I'll fix the problem by introducing a configuration file for RBS, such as one called `manifest.yaml`. The file will contain all stdlib dependency names.
For example, if `foo` gem depends on `pathname` and `logger`, the `manifest.yaml` will be the following.

```yaml
# ruby/gem_rbs_collection/gems/foo/1.0/manifest.yaml

depends_on:
  - name: pathname
  - name: logger
```

#### Other issues

`rbs collection` has other known issues. See https://github.com/ruby/rbs/pull/589  and https://github.com/ruby/rbs/issues/794  for more information.

## Conclusion

I introduced `rbs collection`, which is the "Bundler" for RBS.
You can easily manage third party RBSs with the command.

I hope you will give it a try. Feel free to open issues if you have any problems!
Thank you for reading <3