swiftgen的なものをRubyで書いてみたがイマイチだったので供養

SwiftGenを使うと、翻訳ファイル等の入力からテンプレートを通して型付きのコードが得られるというツールがある。

この考えを応用して、

I18n.t('foo.bar.baz')

L10n.foo.bar.baz

と書けたら、

  • 重複定義があったらRubyでエラーになるはずなので気付ける
  • 型も同時に生成したらLSP的に便利そう

と思って書いてみた。

https://gist.github.com/ksss/b1988f09940617896a1f9ff3968975a2

以下のようなRubyと、

module L10n
  def self.foo = Foo
  module Foo
    def self.bar = Bar
    module Bar
      def self.baz = I18n.t('foo.bar.baz')
    end
  end
end

以下のようなRBSを出力する。

module L10n
  def self.foo = singleton(Foo)
  module Foo
    def self.bar = singleton(Bar)
    module Bar
      def self.baz: () -> String
    end
  end
end
L10n. # ここでfooが候補に出る
L10n.foo. # ここでbarが候補に出る
L10n.foo.bar.baa # baa methodは無いので型チェックで見つかる
L10n.foo.bar.baz # Stringが返ることが分かる

確かに目論見自体は良かったが、以下の点がイマイチだった。

  • 出力が数万行になり、無駄が多そう
  • viewは大抵テンプレートエンジン上の記述でありsteep未対応
  • I18n.t('foo.#{var}.title')的な動的記述が結構ある

SwiftGenは、言語の型による強制力が十分にあり、かつ課題がハッキリしているのでうまくいっているんだろうなあ。

RuboCop on RBS on gem_rbs_collection

gem_rbs_collectionでもrubocop-on-rbsを使っていただけることになり、導入したので紹介します。

gem_rbs_collectionへPR送る方へ

RBSを修正してPRを送るとCIから指摘を受けることがあります。

GitHub上でRBSに対してRuboCopが指摘を出している例

rubocop-on-rbsのドキュメントを見ればルールの説明が書かれています。

rubocop-on-rbs/docs/modules/ROOT/pages/cops.adoc at main · ksss/rubocop-on-rbs · GitHub

通常のRuboCopと同じく、ルールに従って修正していただければ大丈夫です。

PRを送る前に、手元でbundle exec rubocopとコマンドを実行すれば事前にチェックすることもできます。 自分で機械的なレビューができて便利ですね。

gem_rbs_collectionのgemレビュワーの方へ

gem_rbs_collectionでは各gem毎にレビュワーとして名乗り出ることが可能になっており、PRが送られるとgemのレビュワーにメンションが飛ぶ仕組みになっています。

この仕組みはpockeさんのRubyKaigi 2024の発表で詳しく紹介されています。 https://rubykaigi.org/2024/presentations/p_ck_.html

.rubocop.ymlの設定もこの仕組みに則っており、各gem毎にレビュワーが設定できるようになっています。

導入方法はgems/#{gem_name}/.rubocop.ymlを設定するだけです。すでにactivesupportなどの、PRが多く来るgemに導入しているので、大抵の場合はコピペすればいいと思います。

ルールにこだわりがあれば細かく.rubocop.ymlを設定する感じです。

ルールの大まかな説明

  • RBS/Layout
    • インデントやスペース系です。
    • おすすめ
  • RBS/Lint
    • 過剰なpublic()のつけすぎ、他のgemに影響がある書き方など、直した方が良さそうなルールです。
    • おすすめ
  • RBS/Style
    • "true | falseと書くならboolと書いた方がいいよ"など、一歩踏み込んだルールたち。
    • 基本的にはおすすめ。人によってはやりすぎと感じる人もいるかも

rubocop-on-rbsruby/rbsリポジトリにも導入されており、多くのルールでコミッター陣にもレビューしていただいているので、RBSデファクトスタンダードなフォーマットと言っても差し支えないのではないでしょうか……!

2024年なにしたっけ

1月

aws-sdkrbsサポートがリリースされた。

ksss9.hatenablog.com

あれからちょこっとPR送ったりレビューを頼まれたりしている。

2月

raapを作り始めた。今もrbsリポジトリのテストに一部組み込まれている。つまりruby/rubyのbundled_gemのテストで毎回走っている。

ksss9.hatenablog.com

3月

raap作ってた。

4月

raap作ってた。

5月

rubocop-on-rbsをつくりはじめた。

ksss9.hatenablog.com

これもrbsリポジトリに組み込まれてCIのたびに走っている。今も開発は続けている。

6月

rubocop-on-rbsを作ってた。

7月

コーヒーをはじめた。ドリッパーを使ってフィルターコーヒーを淹れる。これはドハマりして、いまも続いている。

8月

コーヒー淹れてた。

9月

コーヒー淹れてた。 車の運転を練習してた。

10月

車で家族を乗せて高速道路に出るまでになった。

11月

スプラトゥーン3のチームを組んでガチキングに出てた。

12月

そろそろrubyが出るらしいので、その前にrbsに仕込めるものを仕込んだ。

来年の抱負

もうちょっとブログを書く。

RuboCop on RBS on rbs

rbsリポジトリrubocop-on-rbsが導入された。

Introduce rubocop-on-rbs by ksss · Pull Request #1899 · ruby/rbs · GitHub

RuboCopはすでにrbsリポジトリで使われていて、rubyファイルに対しての指摘はすでに行われていた。この変更によってRBSファイルに対しても様々な細かなチェックを自動的に行えるようになった。

この導入はさまざまなリポジトリへのrubocop-on-rbsの導入モデルケースにもなると思っていて、GitHub Actionsでの自動的な指摘コメントを入れるよう設定もしている。

RuboCopが指摘するものは、はっきり言ってしょうもないことかもしれない。しかし、そのしょうもないことを自動的に勝手にチェックし続けてくれて、運用され続けてくれることには、レビュー負荷を下げるなどの価値があると思っている。

特にRBSシンタックスに明るい人も少ないので「これで合ってるのか?」という不安が常についてくると想像される。その辺の負荷を減らしてあげたいという気持ちもある。

これを機にv1.0.0にした。

今後はCopを思いついたら増やしつつ、rbs-inlineが本体にmergeされたら対応しようかなというのが展望だ。

RuboCop on RBS

rubocopをRBSファイルにも効かせたい - スペクトラム で作ってたものが大体できてきて、rbs v3.5もリリースされたので公開できるようになりました。

これでRuboCopをRBSファイルにも使用できるようになりました。

github.com

例えば

class Foo
def foo: () -> void
end

というRBSをレビューする時「インデント入れてね」と指摘したくなりますよね。Rubyファイルならrubocopでできるのに、RBSファイルではrubocopで指摘できないという問題がありました。あるんです。きっと。というわけで、できるようにしました。

とりあえず初期バージョンでは20以上のルールを用意しています。

autocorrect(自動修正)にも対応しているので、いつものようにrubocop -aしてあげれば

class Foo
  def foo: () -> void
end

とrubocopが修正してくれます。

CLIで実行するもよし、CIに設定するもよし、エディタ上でLSPとして使用するもよしと、RBSの記述へのハードルが下がることを目指しています。

RuboCop on RBS

ちょけたネーミングですがワケがあります。rubocop-rbsの名前は使えなかったのです。それだけです。

rubocop-de-rbsとちょっと迷いました。

つかいかた

使い方は、rubocop-rails等ふつうのrubocop拡張と同じです。.rubocop.ymlrequireに追加するのがオススメです。

Departments

20以上の機能があり、.rubocop.ymlで設定を切り替えられます。 機能追加のリクエスト待っています。

RBS/Layout/*

インデントやスペースの指摘、およびautocorrectによる自動修正ができます。 rbsリポジトリに記載されているスペースやインデントを正しいものとしています。

全てのトークン間で多すぎるスペースを指摘することもできます。 このためにRBSトークン別にパースする機能を入れてもらいました。 Implement token list API by ksss · Pull Request #1829 · ruby/rbs · GitHub

  • コメントのインデントがずれてると指摘
  • overload間の空改行があると指摘
  • class/module/interfaceとendのインデントが合ってないと指摘
  • スペースが無駄に多すぎると指摘
  • class/module/interfaceがあるたびにインデントをスペース2つ分深くなってないと指摘
  • overloadの:|の縦が揃ってないと指摘
  • ->の前後は1スペースじゃないと指摘
  • ブロックを表す{}の前後は1スペースじゃないと指摘
  • def foo::の前はスペースをなくすよう指摘
  • :|のあとは1スペースになるよう指摘
  • 行末の余分なスペースを指摘

RBS/Lint/*

RBSシンタックスエラーや、rbs validateでワーニングやエラーになる構文を指摘してくれます。

  • シンタックスエラー
  • 型引数を全く使っていないと警告
  • Array等、型引数を指定しなければいけない型なのに、型引数がついていなかったり多かったりしたら警告
  • メソッド引数にvoidを使用したら警告

rbs validateを実行しなくてもRuboCopで指摘してくれるのですぐに問題に気づけます。

若干SteepのLSP機能と被っている場合もありますが、RuboCop on RBSでは軽量に分かるものを、Steepでは名前解決が必要な重いものを見れればいいのかなと思っています。

RBS/Style/*

この書き方より、こっちの方がいいですよ系。 主に型の書き方で無意味な重複があったりすると教えてくれます。 自動修正にも対応しているので便利。

  • ブロックの返り値にboolを使っている時は指摘
  • TrueClass, FalseClass, NilClassを使っていると指摘
  • Integer | Integerのような重複があると指摘
  • initializeの返り値はvoidでないと指摘
  • untyped | Stringは結局untypedなので指摘
  • nil?のような重複した定義を指摘
  • true | falseを使用した場合、boolに直すよう指摘

どうやってやってるの?

rubocopは、Rubyとしてパースし、シンタックスエラーが発生したらon_other_fileを呼び出しますという挙動になっています。

このon_other_fileRBSとしてパースして、さまざまなCopに利用しています。RBS::Parser.parse_signatureでパースしたASTには位置情報も含まれているのでこれを利用しています。

また、RBS::Parser.lexというAPIも増えたので、これで全てのトークンの位置情報を細かく取得できます。これもガッツリ使っています。なのでrubocop-on-rbsrbs v3.5から使うことができます。

VSCode対応

VSCode対応もできています。

ですが https://github.com/rubocop/vscode-rubocop/pull/28 で提案中なので自分でextensionをbuildするか、~/.vscode/extensions/を直接書きかえる必要があります。(方法はrubocop-on-rbsのREADME.mdに書きました。)

エディタでリアルタイムフィードバックがあると次元の違う開発体験ができるので、ぜひ実現させたい機能です。

将来的に

ruby/rbsruby/gem_rbs_collectionに導入して、PRがきたらCIで機械的に指摘できたらいいなと思っています。

もちろんあなたのリポジトリにも使っていただけると大変嬉しいです。

raapがrbsで使われ始めた

RBSをテストコードにするツールraaprbsリポジトリで使われ始めた。

Introduce RaaP for testing of signature by ksss · Pull Request #1810 · ruby/rbs · GitHub

rbsのCIでraapを走らせている。 現状はSet classでしか使っていないが、これから範囲を広げていきたい。

経緯

raapを作っていたら、「これはRBS::UnitTestを自動化するツールだな」と思い始めてきた。

テストコードを手で書くより、自動で100パターン実行してくれた方が、管理するものが減って、人間の思い込みを排除した確認ができ、早く、変更にも強い。

もともとrbsリポジトリに置かれた記述されたRBSファイルに対するテスト("RBSの記述は正しいか"を確認するコード)は結構カバレッジが低い部分もあり、これを埋めたかった。

なのでraapをclass単位で実行して落ちたテストケースをコピペしてテストを足して型を直すという活動をやっていたが「もうこれはいっそraapをrbsリポジトリで使った方が早いのでは?」と思い始めた。

例えばSet classのRBSに対するテストはほとんど無に等しい状態で、実際型も正しくはなかったしメソッド名もtypoしてた。これを見つけたのもraapだ。

raapの完成度を上げ、十分使えそうになってきたのでrbsリポジトリに導入を提案した。

これから

まずはテストが不十分なところをraapで埋めつつ型を直していきたい。

あんまり厳密すぎても不便なだけになることもあるので、その辺のバランスを探りつつ、rbsにもraapにも還元していく流れを作っていきたい。

ゆくゆくはgem化されているスタンダードライブラリーでの使用も目論んでいる。

gem化されたスタンダードライブラリーはメンテナーが見ているわけだけど、メンテナーにRBS用のテストコードまで面倒見てもらうのは重い気がしているので、raapで自動化できたらいいんじゃない?と夢見ている。

さらにはユーザーライブラリーでの利用を広げるなど「型をテストにする」という切り口でどこまでいけるのか試してみたい。

rubocopをRBSファイルにも効かせたい

背景

RBSも利用が増えてくると、細かい"こうしたほうがいい"が出てくることがあると予想される。

例えば、

  • インデント
  • initializeの返り値はvoidを使う
  • untyped?ではなくuntypedを使う
  • メソッド引数にvoidを使わない

などなど。

こうしたことを指摘するツールとしては、Rubyではrubocopが覇権をとっており、大体の環境ですでに使われている。

このエコシステムにRBSも乗っかりたい。

問題点

VSCode上で警告を表示できない

大きな問題点として、VSCode上の波線(squigglyというらしい)が表示できないという問題がある。

エディタでインタラクティブに問題点が見える体験はかなり重要だと思っているので対応したい。

しかしながら何が原因なのかはわからない。

試しにrubocop orgが公式にホストしているrubocop-mdを使ってみたが、こちらも特にエディタ上ではなにも表示されなかった(ruby-lsp)。

原因を究明するのに時間を使うべきか、より機能追加に時間を使うべきか、悩みどころである。

名前をrubocop-rbsにできない

今回作っているものはrubocop-rbsと言うしかないものなのだが、

rubocop-rbsというgemはすでに存在している。

これはRBSを見て、Ruby側のメソッド定義の引数の形式と数のマッチングをみるもののようだ。

名前は早い者勝ちなのでしょうがないが、この作っているものの名前に悩んでいる。

今は仮にrubocop-rbs-sigとしている。

機能

とりあえず、

  • シンタックスエラー表示
  • initializeの返り値はvoidを使う
  • untyped?ではなくuntypedを使う

できた。

sig/rubocop/rbs/sig.rbs:5:31: C: [Correctable] RBS/Sig/InitializeReturnType: #initialize method should return void
        def initialize: () -> nil
                              ^^^
sig/rubocop/rbs/sig.rbs:6:19: C: [Correctable] RBS/Sig/Untyped: untyped? should be untyped
        def foo: (untyped?) -> (Integer | untyped)
                  ^^^^^^^^
sig/rubocop/rbs/sig.rbs:6:33: C: [Correctable] RBS/Sig/Untyped: Integer | untyped should be untyped
        def foo: (untyped?) -> (Integer | untyped)
                                ^^^^^^^^^^^^^^^^^
  • autocorrect
  • 機能別にCopを分ける

といったこともできている。

実装方法

rubocopはRubyに特化したツールであり、RBSRubyとは違う。

Ruby以外のファイルにrubocopを実行する参考例としては、rubocop-erbrubocop-mdがみつかった。これを参考にした。

rubocopではターゲットファイルがRubyとしてパースできなかった場合、on_other_fileを呼び出すようになっている。

rubocop/lib/rubocop/cop/commissioner.rb at 673495b70e8b9e42d82d85526292945d47189f10 · rubocop/rubocop · GitHub

このon_other_fileRBSとしてパースし、パースできたらRBSとして扱うようにしてみている。

Rubyとしてパースして、RBSとしてもパースしているので無駄はある気はするが、ここを変えようとするとrubocopにかなり無茶させないといけなくなり、コード量も増える気がするので一旦こうしている。

つまり

やればできそう。