pockestrap

Programmer's memo

rbs cli

github.com

RBSというRubyの型のためのソフトウェアがあり、RBSrbsコマンドを含んでいる。 このコマンドは、.rbsファイル(Rubyの型定義ファイル)を書いていく上で必要になる。

この記事では、そのrbsコマンドの各サブコマンドについて、簡単に解説する。

rbsコマンドの実装は lib/ast/cli.rb に書かれている。 サブコマンドの実装は、 run_サブコマンド名メソッドを読むと良い。つまり、rbs astコマンドの実装はrun_astメソッドにある。

現時点(2020-06-15)のmasterブランチの最新版( https://github.com/ruby/rbs/commit/00872ed5de364a5d2fcf8169aee87307ba67aad3 )を元にしている。

サブコマンドに共通のオプション

まず、共通で使えるオプションの説明をする。

サブコマンドに共通のオプションには以下がある。

  • --help
  • -r LIBRARY
  • -I DIR
  • --no-stdlib
  • --log-level=LEVEL
  • --log-output=OUTPUT

このうち、-r, -I, --no-stdlibについて説明する。

-r

これはrubyコマンドの-rと同じで、ライブラリをrequireする。 つまり、-r pathnameなどとするとpathnameの型がrequireされた上でrbsコマンドが実行される。

-I

これはrubyコマンドの-Iと似ていて、.rbs ファイルを置いたディレクトリを指定する。

rubyコマンドのそれとは異なり、-Iに指定したディレクトリ配下の.rbsファイルは全て読み込まれる。 つまり、-I .とするとカレントディレクトリ配下の.rbsファイルを全て読み込んだ上でrbsコマンドが実行される。

--no-stdlib

標準ライブラリなしでrbsコマンドを実行する。つまり、ArrayもStringもIntegerもない世界でrbsコマンドが実行される。


以下で各サブコマンドの解説をする。

rbs prototype

既存の資産からrbsを生成する。rbs prototype rb, rbs prototype runtime, rbs prototype rbiの3つがある。

実際に型を書いていく際には、このうちのどれかを使って雛形を生成した上で、それに手を加えていく形になると思う。

このうちrbiは使ったことがないので、それ以外の2つを解説する。

rbs prototype rb

.rbファイルを静的に解析して.rbsファイルを生成する。

# test.rb

class C
  attr_accessor :foo

  def bar(x, y, z)
    [x, y, z]
  end

  eval <<~RUBY
    def baz
      :baz
    end
  RUBY
end
$ rbs prototype rb test.rb
class C
  attr_accessor foo: untyped

  def bar: (untyped x, untyped y, untyped z) -> ::Array[untyped]
end

静的に解析するので、evalの中で定義されているbazメソッドは取れない。 一方、fooアトリビュートbarメソッドの定義は出力されている。

rbs prototype rbは戻り値がリテラルであればそれっぽい戻り値の型を出力するようになっている。 例えばbarメソッドはArrayを返すことが分かるので、そのように出力されている。 これは私が実装した。

rbs prototype runtime

prototype rbとは違い、動的に.rbファイルを読み込んで.rbsファイルを生成する。

prototype rbと同じtest.rbに対して実行すると次のようになる。

$ rbs prototype runtime --require-relative test.rb C
class C
  public

  def bar: (untyped x, untyped y, untyped z) -> untyped

  def baz: () -> untyped

  def foo: () -> untyped

  def foo=: (untyped) -> untyped
end

このように--requireもしくは--require-relativeオプションでファイルを読み込んだ上で実行する。 (ちなみに、railsアプリケーションの場合はconfig/environment.rbを読み込んだ上でeager_loadしてやるとうまくいくよう。)

prototype rbの場合とは異なり、evalで定義されているbazメソッドの情報も取れている。 一方、fooアトリビュートが単なるメソッドとして定義されていたり、barメソッドの戻り値がuntypedになってしまったりしている。

このようにrbruntimeは一長一短なので、適当に使い分けると良いと思う。

runtimeは動的に定義されたメソッドを拾える特徴があるので、メタプロをゴリゴリ使っている場合はそちらを使うと良さそう。

一方runtimerbに比べてまともに動かすのがめんどい。rbは静的な解析しかしないので詰まることはあまりないのだけど、runtimeはコードを実行しなければいけないので、ハマりどころは沢山ある。 たとえばautoloadで定義された定数は何もしなければ無なので、適当に読み込んでやる必要があったりする(これはrbsを直したほうが良いかも)。

rbs validate

.rbsファイルの定義が壊れていないかを検証する。 なので、.rbsファイルを書くときはこれを実行しながら書いていくと良さそう。

例えばsig/ディレクトリ下にrbsを書いているときは次のように実行する。

$ rbs -I sig/ validate

rbs parse

与えられた.rbsファイルをパースした上で、syntax errorがあればそれを出力してexit 1する。 rbs validateよりもやっていることが少ない分速そう。

# syntax-error.rbs

class Foo
  # barの後には : が必要
  def bar () -> untyped
end
$ rbs parse syntax-error.rbs
syntax-error.rbs:5:10: parse error on value: (kLPAREN)

$ echo $?
1

rbs ast

読み込まれているrbsファイルのASTを全て表示する。つまり、--no-stdlibオプションをつけないと標準ライブラリのASTも表示されて不便。

たとえばカレントディレクトリに以下の.rbsファイルを置いて実行すると次の出力が得られる。

# test.rbs
class Foo
  def bar: () -> untyped
end
$ rbs -I . --no-stdlib ast
[{"declaration":"class","name":"Foo","type_params":{"params":[]},"members":[{"member":"method_definition","kind":"instance","types":[{"type_params":[],"type":{"required_positionals":[],"optional_positionals":[],"rest_positionals":null,"trailing_positionals":[],"required_keywords":{},"optional_keywords":{},"rest_keywords":null,"return_type":{"class":"untyped","location":{"start":{"line":2,"column":17},"end":{"line":2,"column":24},"buffer":{"name":"test.rbs"}}}},"block":null,"location":{"start":{"line":2,"column":11},"end":{"line":2,"column":24},"buffer":{"name":"test.rbs"}}}],"annotations":[],"location":{"start":{"line":2,"column":2},"end":{"line":2,"column":24},"buffer":{"name":"test.rbs"}},"comment":null,"attributes":[]}],"super_class":null,"annotations":[],"location":{"start":{"line":1,"column":0},"end":{"line":3,"column":3},"buffer":{"name":"test.rbs"}},"comment":null}]

rbs list

定義されているクラス、モジュール、インターフェイスの一覧を出力する。 --class, --module, --interfaceオプションを指定すると、指定したものだけが出力される。

rbs ancestors

クラスを指定すると、その祖先を順に表示する。

$ rbs ancestors String 
::String
::Comparable
::Object
::Kernel
::BasicObject

rubyのコードだと、Module#ancestorsに対応する。

rbs methods

指定したクラスに存在するメソッドの一覧を返す。

$ rbs methods String
(メソッドの一覧が出てくる)

rubyのコードだと、Module#instance_methodsObject#methodsなどに対応する。

rbs method

メソッドの型情報を表示する。

$ rbs method String gsub
::String#gsub
  defined_in: ::String
  implementation: ::String
  accessibility: public
  types:
      (::Regexp | ::string pattern, ::string replacement) -> ::String
    | (::Regexp | ::string pattern, ::Hash[::String, ::String] hash) -> ::String
    | (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String
    | (::Regexp | ::string pattern) -> ::Enumerator[::String, self]

rbs constant

定数を表示する。

$ rbs constant RUBY_VERSION
Context: ::
Constant name: RUBY_VERSION
 => ::RUBY_VERSION: ::String

rbs paths

読み込まれているpathを表示するっぽい? 使わないのでよくわからない。

rbs vendor

わからん。

rbs versuion

バージョン番号が出る。

$ rbs version
rbs 0.3.1