Rubyについに静的解析がくるので早速試してみる。【RBS】【TypeProf】【Steep】

# はじめに
先日12月8日にRuby 3.0.0-preview2がリリースされました。
https://www.ruby-lang.org/ja/news/2020/12/08/ruby-3-0-0-preview2-released/
その中に「静的解析」の文字が！この記事では早速Rubyの静的解析に関わる「RBS」と「TypeProf」と「Steep」を使ってみます。
詳しい理論や内部実装には触れず、どうやって使うのかについて記載します。

# Ruby 3.0.0-preview2を使う
## rbenvの更新
インストールしていない方はインストールをしてください。

```zsh:shell
$ brew upgrade rbenv ruby-build
```
## 3.0.0-preview2のインストール

※MacOSでの失敗を確認しています。issue解決待ち。

```zsh:shell
$ rbenv install 3.0.0-preview2
# 3.0.0-preview1で妥協
$ rbenv install 3.0.0-preview1
$ rbenv local 3.0.0-preview1
```

# 作業後のテストプロジェクトの構成
```sh
root/
  - lib/
    - user.rb
  - sig/
    - user.rbs
  - .ruby-version
  - main.rb
  - Gemfile
  - Gemfile.lock
  - Steepfile
```
## RBS, TypeProf, Steepの追加
ついでにTypeProfと型チェックに使用するSteepをインストールします。

```sh:shell
# Gemfileの雛形作成
bundle init
```
```rb:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem "rbs"
gem "typeprof"
gem "steep"
```
```sh:shell
# rbsとtypeprofとsteepのインストール
bundle install
```
## Userクラスの追加
```ruby:lib/user.rb
class User
  def initialize(name:, age:)
    @name, @age = name, age
  end
  attr_reader :name, :age
end
```
## mainの追加
```ruby:main.rb
require "./lib/user"

def main
  user = User.new(name: "John", age: 20)
  puts user.name
end

if __FILE__ == $0
  main
end
```
## 動作確認
```sh:shell
$ ruby main.rb
John
```

# RBS
> RBSはRubyプログラムの型を記述するための言語です。
> TypeProfなどの型検査ツールを初めとする静的解析を行うツールは、RBSを利用することでRubyプログラムをより精度良く解析することができます。
> RBSでは、Rubyプログラムのクラスやモジュールの型を定義します。メソッドやインスタンス変数、定数とその型、継承やmixinなどの関係などが記述できます。
> RBSはRubyプログラムに頻出するパターンをサポートするように設計されており、ユニオン型、メソッドオーバーロード、ジェネリクスなどの機能を提供します。さらに「インタフェース型」によってダックタイピングをサポートします。
> Ruby 3.0には、このRBS言語で書かれた型定義を処理するためのライブラリである rbs gemが同梱されています。

つまり、超簡単にまとめると

- RBSでRubyプログラムの型を定義する。
- プログラムの構造も定義できる。
- Ruby3.0からRBSが同梱される。


## RBSを使ってみる
## 型定義ファイル（.rbs）を書いていく
```ruby:sig/user.rbs
class User
  attr_reader name : String
  attr_reader age : Integer
  def initialize : (name: String, age: Integer) -> nil
end
```

main.rbの型定義もします。

```ruby:sig/main.rbs
class Object
  private
  def main: -> nil
end
```

型情報が書けたのでsteepでチェックをします。

## Steepの設定
```sh:shell
$ bundle exec steep init
```
```ruby:Steepfile
# target :lib do
#   signature "sig"
#
#   check "lib"                       # Directory name
#   check "Gemfile"                   # File name
#   check "app/models/**/*.rb"        # Glob
#   # ignore "lib/templates/*.rb"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "strong_json"           # Gems
# end

# target :spec do
#   signature "sig", "sig-private"
#
#   check "spec"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "rspec"
# end

target :lib do
  signature "sig"
  check "lib"
  check "main.rb"
end
```

型情報は`sig`ディレクトリに、チェック対象は`lib`ディレクトリと`main.rb`と指定しました。

## Steepでチェック
```sh:shell
$ bundle exec steep check
```

何も表示されずに終了したら問題がないということです。

## 試しに型情報と違う実装に変更する
```ruby:main.rb
require "./lib/user"

def main
  user = User.new(name: "John", age: "20") # ageをStringに変更
  puts user.name
end

if __FILE__ == $0
  main
end
```

```sh:shell
$ bundle exec steep check
main.rb:4:37: IncompatibleAssignment: lhs_type=::Integer, rhs_type=::String ("20")
  ::String <: ::Integer
   ::Object <: ::Integer
    ::BasicObject <: ::Integer
==> ::BasicObject <: ::Integer does not hold
zsh: exit 1     bundle exec steep check
```

実装箇所と内容が表示されています。
型情報とチェックについて簡単に実装しました。
次は型情報の記載を楽にしてくれるTypeProfを使用します。

# TypeProf
> TypeProf は Ruby パッケージに同梱された型解析ツールです。
> TypeProf の現在の主な用途は一種の型推論です。
> 型注釈の無い普通の Ruby コードを入力し、どんなメソッドが定義されどのように使われているかを解析し、型シグネチャのプロトタイプを RBS フォーマットで生成します。

インストールは終わっているので早速使用します。

## TypeProfを使ってみる
```sh:shell
$ bundle exec typeprof lib/user.rb
# Classes
class User
  attr_reader name: untyped
  attr_reader age: untyped
  def initialize: (name: untyped, age: untyped) -> [untyped, untyped]
end
```

このように実装からある程度、RBSフォーマットで自動で生成してくれます。ただ`name`や`age`にどの型が渡されるかがUserクラスの定義だけではわからないため`untyped`となっています。

では、次に実際にUserクラスを使用している`main.rb`に対してtypeprofを実行してみます。

```sh:shell
$ bundle exec typeprof main.rb
# Classes
class Object
  private
  def main: -> nil
end

class User
  attr_reader name: String
  attr_reader age: Integer
  def initialize: (name: String, age: Integer) -> [String, Integer]
end
```

`main.rb`では実際に`name`に`"John"`、`age`に`20`を指定しているためUserクラスの型情報が正確になりました。ただ、`initialize`の返り値が`[String, Integer]`になっています。実際、期待する返り値は`nil`ですので、実装を修正します。

## TypeProfの出力から実装の改善
```ruby:lib/user.rb
class User
  def initialize(name:, age:)
    @name, @age = name, age
    return # returnを記載することで返り値が無いことを明示する
  end
  attr_reader :name, :age
end
```

typeprofを再実行してみます。

```sh:shell
$ bundle exec typeprof main.rb
# Classes
class Object
  private
  def main: -> nil
end

class User
  attr_reader name: String
  attr_reader age: Integer
  def initialize: (name: String, age: Integer) -> nil
end
```

`initialize`の返り値が`nil`になりました！

# 流れまとめ
1. 実装
2. TypeProfでRBSを生成
3. Steepでチェック

▼ 今回の内容のリポジトリ
https://github.com/naro143/ruby-static-program-analysis-trial

# まとめ
Rubyに静的解析の機能が追加されるのは嬉しいです。RBSの記載は慣れないですがTypeProfを上手く使用すればある程度は楽ができそうです。また、TypeProfが正しく生成できるように実装がより意図を明示するようになるのも嬉しいです。
個人的には、「存在しないメソッドを叩いてnilが返っている」ような挙動は問題ないが意図とはずれている実装や、定義ジャンプの際に候補が複数個も提案されるといったことも静的解析によって改善されると嬉しいです。