LoginSignup
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-13

はじめに

先日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の更新

インストールしていない方はインストールをしてください。

shell
$ brew upgrade rbenv ruby-build

3.0.0-preview2のインストール

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

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

作業後のテストプロジェクトの構成

root/
  - lib/
    - user.rb
  - sig/
    - user.rbs
  - .ruby-version
  - main.rb
  - Gemfile
  - Gemfile.lock
  - Steepfile

RBS, TypeProf, Steepの追加

ついでにTypeProfと型チェックに使用するSteepをインストールします。

shell
# Gemfileの雛形作成
bundle init
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"
shell
# rbsとtypeprofとsteepのインストール
bundle install

Userクラスの追加

lib/user.rb
class User
  def initialize(name:, age:)
    @name, @age = name, age
  end
  attr_reader :name, :age
end

mainの追加

main.rb
require "./lib/user"

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

if __FILE__ == $0
  main
end

動作確認

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)を書いていく

sig/user.rbs
class User
  attr_reader name : String
  attr_reader age : Integer
  def initialize : (name: String, age: Integer) -> nil
end

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

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

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

Steepの設定

shell
$ bundle exec steep init
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でチェック

shell
$ bundle exec steep check

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

試しに型情報と違う実装に変更する

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
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を使ってみる

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フォーマットで自動で生成してくれます。ただnameageにどの型が渡されるかがUserクラスの定義だけではわからないためuntypedとなっています。

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

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"age20を指定しているためUserクラスの型情報が正確になりました。ただ、initializeの返り値が[String, Integer]になっています。実際、期待する返り値はnilですので、実装を修正します。

TypeProfの出力から実装の改善

lib/user.rb
class User
  def initialize(name:, age:)
    @name, @age = name, age
    return # returnを記載することで返り値が無いことを明示する
  end
  attr_reader :name, :age
end

typeprofを再実行してみます。

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が返っている」ような挙動は問題ないが意図とはずれている実装や、定義ジャンプの際に候補が複数個も提案されるといったことも静的解析によって改善されると嬉しいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0