YARDタグからRBSを生成する

YARD

YARDはドキュメンテーションツールです。 Rubyのコメントに

# @param [String] a
# @return [void]
def foo(a)
end

みたいな記述を見たことがありませんか? この@paramがYARDのタグ名、[String]がタグのもつ型情報です。aは引数の名前ですね。

YARDではこの型情報を元にリンクを貼ったwebページを生成したりできます。

rubydoc.infoがまさにYARDによって生成されていますね。

RBS

RBSRuby公式の型を記述する書式、およびツールの名前です。

こんなかんじです。

def foo: (String a) -> void

YARD to RBS

では

# @param [String] a
# @return [void]
def foo(a)
end

とかかれたソースコードを読み込んで

def foo: (String a) -> void

とかかれたRBSを出力できたら便利だと思いませんか?

orthoses-yard

というわけで実装したのがorthoses-yardです。

github.com

orthoses-yardはorthosesの拡張です。 orthosesが何かは、RubyKaigi2022で紹介した資料を御覧ください。動画はもうすぐ出ると思います。*1

orthoses-yardではこのorthosesの仕組みに乗っかって、任意のpathのRubyをパースしたYARD情報からRBSを生成します。 「アプリケーションの一部だけしかYARDドキュメント書けてないんだよな」という場合でも柔軟に対応できます。

基本的には@paramタグを引数として、@returnタグを返り値として解釈し、メソッドの型定義を生成しています。 また、@yieldparamタグと@yieldreturnタグにも対応し、ブロックの型表現も可能です。

これ(Rubyコード)から

class Foo
  # @param [String] a
  # @return [Integer]
  # @yieldparam [Symbol] b
  # @yieldreturn [Boolean]
  def foo(a)
  end
end

これ(RBS)を作れます。

class Foo
  # @param [String] a
  # @return [Integer]
  # @yieldparam [Symbol] b
  # @yieldreturn [Boolean]
  def foo(String a) { (Symbol b) -> bool } -> Integer
end

orthosesは柔軟なので、アプリケーションだけでなくgemのRBSもorthoses-yardを使って生成できます。手始めにyard gem自体の型定義をYARDタグを元に生成してみました。yard gemならYARD記法を使いこなして広いパターンでの挙動確認ができそうだったのが選択理由です。

Add Yard gem by ksss · Pull Request #217 · ruby/gem_rbs_collection · GitHub

既存のYARDドキュメントを元にメソッドや定数の型を決定することができています。

RBS自動生成の肝

RBS自動生成最大の問題はmethodの型定義です。

orthosesによるRBS自動生成では、classの名前や継承関係、includeしているmoduleを自動的に洗い出したりするのは得意ですが、 methodの型定義を決めることは困難なのです。

なぜならmethodの型は基本的には人間が決めるべきであり、人間が書くべきです。 正しいものが何なのかわからないと、methodの実装自体が型チェックができないですしね。

def foo
  if cond
    "foo"
  else
    :bar
  end
end

上記例だと静的解析すればfoo: () -> String | Symbolとなりますが、 実際はStringだけを期待していてSymbolが返るのは誤りになるケースも多くあるでしょう。 ましてやattr_accessorだとさらに難しくなります。

methodの型は手で書くしか無いのでしょうか……。

でも自動化したいですよね。

もし既存の資産であるYARDドキュメントを利用できれば、methodやattr_accessorであっても型ファイルを生成できます。

このYARDからの生成によって、このmethod定義問題に良いアプローチができるんじゃないかと期待しています。

YARDを書けばRBSを生成できるならYARDを書くモチベーションを上げることもできますし、YARDとRBSで二重管理になることも防げます。

補足

ちなみにRubyの型界隈に詳しい人は、似たようなツールとしてsordを思い浮かべたと思います。 確かに今回実装したものはsordによく似ていますが、orthosesの拡張なので他のorthosesの機能と組み合わせられるという点が大きなウリとなっています。 例えば「Railsプロジェクトで一部だけYARDを使っている」という場合でも、orthoses-railsrails拡張と組み合わせてプロジェクト全体の型定義をコントロール可能です。

YARDの限界

YARDは非常に歴史あるプロダクトであるため、途中から追加されたkeyword argumentsの対応が弱いように感じます。YARDタグだけではそれがキーワード引数なのかどうか判別できません。もちろんYARD的には問題ないのですが、型定義を生成する場合は情報が足りません。

# どっちも同じ書き方

# @param [String] key
def bar(key)
end

# @param [String] key
def foo(key:)
end

orthoses-yardではMethodオブジェクトからparametersをとってYARD記法とマッチングさせることによってこの問題を解決しています。*2 また、RBSでのoverloadが表現できないことも若干ネックかなと思います。

# YARDだと表現できない
def foo: (Integer) -> Integer
       | (String) -> String

これから

大まかな機能はできましたが、まだまだ細かいTODOが残っているので機能を拡充してプロダクトで使ってみて更に磨きをかけていこうかと思います。

*1:動画では「orthoses-yardが欲しいんだけど誰か作らない?」と言ってますが自分で作ってしまいました。プログラミング楽しい。

*2:引数を移譲するメソッドではこの方法では対応できないので別途対応を考え中