rbs-inlineに触れてみた

# はじめに

この記事は、rbs-inlineを触ってみた際のメモ的な記事です。rbs-inlineのREADMEを参照しながら動かしてみました

https://github.com/soutaro/rbs-inline

## 検証しているバージョン

- Ruby 3.3.4
- rbs-inline 0.6.0
- steep 1.7.1

# 導入

RubyやBundlerは導入ずみであるとします。

## 1. bundle install

`bundle init`でGemfileを作成し、その中にrbs-inlineを追加します。

```Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

gem 'rbs-inline', require: false
```

追記したら、`bundle install`。

```text
$ bundle install 
Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
Fetching rbs 3.5.3
Installing rbs 3.5.3 with native extensions
Fetching rbs-inline 0.6.0
Installing rbs-inline 0.6.0
Bundle complete! 1 Gemfile dependency, 5 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
```

## 2. Rubyを書く

README.mdにサンプルが書かれているので、それを参考にコードを書きます。

```
$ mkdir lib
$ touch lib/person.rb
```

```lib/person.rb
# rbs_inline: enabled

class Person
  attr_reader :name #: String

  attr_reader :addresses #: Array[String]

  # @rbs name: String
  # @rbs addresses: Array[String]
  # @rbs return: void
  def initialize(name:, addresses:)
    @name = name
    @addresses = addresses
  end

  def to_s #: String
    "Person(name = #{name}, addresses = #{addresses.join(", ")})"
  end

  # @rbs &block: (String) -> void
  def each_address(&block) #:: void
    addresses.each(&block)
  end
end
```

## 3. RBS fileの生成をCLI上で確認する

`bundle exec rbs-inline [ディレクトリ/ファイル名]`とすると、CLI上にRBSの生成結果が出力されます。

```sh
$ bundle exec rbs-inline lib/person.rb 
# Generated from lib/person.rb with RBS::Inline

class Person
  attr_reader name: String

  attr_reader addresses: Array[String]

  # @rbs name: String
  # @rbs addresses: Array[String]
  # @rbs return: void
  def initialize: (name: String, addresses: Array[String]) -> void

  def to_s: () -> String

  # @rbs &block: (String) -> void
  def each_address: () { (String) -> void } -> untyped
end

🎉 Generated 1 RBS files under
```

## 4. 自動生成したRBSをファイルに保存する

`bundle exec rbs-inline --output [ディレクトリ/ファイル名]`を実行すると、`sig/generated`配下にrbsファイルが保存されます。

```sh
$ bundle exec rbs-inline --output lib/person.rb 
🎉 Generated 1 RBS files under sig/generated
```

## 5. steepで型チェック

`$ bundle add steep`でsteepを導入します。

```sh
% bundle add steep
Fetching gem metadata from https://rubygems.org/.............
（省略）
Fetching steep 1.7.1
Installing steep 1.7.1
```

Steepfileを`$ bundle exec steep init`で生成します。

```sh
$ bundle exec steep init
Writing Steepfile...

$ tree .
.
├── Gemfile
├── Gemfile.lock
├── Steepfile
├── lib
│   └── person.rb
└── sig
    └── generated
        └── person.rbs

4 directories, 5 files
```

Steepの[getting started](https://github.com/soutaro/steep/blob/master/guides/src/getting-started/getting-started.md#type-checking-your-first-ruby-script)を参考に、Steepfileに以下を追記します

```Steepfile
target :app do
  check "lib"
  signature "sig"
end
```

`bundle exec steep check`で型チェックをします。

```
$ bundle exec steep check
# Type checking files:

.....................................................................................

No type error detected. 🧋
```

## 6. わざとエラーが発生することを体験する

5までの手順で、型チェックができるようになりました。しかし、エラーが出る場合を体験していないので、わざとエラーが出るようにしてみます。

### 6-1. `sig/generated/person.rbs`を編集する

今回は、rbs-inlineを書かずに手動でRBSファイルを変更します。
以下のように、`def age: () -> String`を追記します。

```sig/generated/person.rbs
  （省略）
  def age: () -> String
end
```

### 6-2. `Person#age`を追加

Personクラスに、Integerを返すageインスタンスメソッドを追記します。

```lib/person.rb
# rbs_inline: enabled

class Person
  # 省略
  def age
    20
  end
  # 省略
```

### 6-3. 型チェックを実行する

`bundle exec steep check`を実行すると、エラーが出力されました。RBSではStringを期待していたのに、`Person#age`ではIntegerを返していることから、`MethodBodyTypeMismatch`と判定されているようです。

```
$ bundle exec steep check
# Type checking files:

....................................................................................F

lib/person.rb:19:6: [error] Cannot allow method body have type `::Integer` because declared as type `::String`
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::MethodBodyTypeMismatch
│
└   def age
        ~~~

Detected 1 problem from 1 file
```

### 6-4. 修正する

rbs-inlineを用いて、RBSを修正してみましょう。

まずは、`Person#age`に戻り値の型定義を追加します。

```lib/person.rb
  def age #: Integer
    20
  end
```

つぎに、`bundle exec rbs-inline --output lib/person.rb｀でRBSファイルを自動生成します。

```
$ bundle exec rbs-inline --output lib/person.rb 
🎉 Generated 1 RBS files under sig/generated
```

### 6-5. 修正後の型チェックを実行

`bundle exec steep check`を再実行すると、今度は問題なく型チェックが通りました。

```
$ bundle exec steep check                      
# Type checking files:

.....................................................................................

No type error detected. 🍵
```

# さいごに

初めてrbs-inlineに触れてみました。Rubyのコードの中に型も一緒に書ける体験は非常にいいものだと感じました。まだ入門したばかりなので、もっと触れていこうと思います。