最近個人では Ruby で開発する際は、 rbs-inline と Steep を使って書いてます。型で色々事前にエラーを見つけてくれるのはありがたいのですが、Type Error を避けるために冗長な書き方をしなくていけなくて面倒なこともまあまああります。
例えば、↓ のようなメモ化代入は Ruby ではよく書きますが Steep にチェックさせるには、「インスタンスの型定義」も書く必要があります。面倒ですね。
def profile #: Profile
@profile ||= Profile.find(id)
end
# Steep で Type Error にならないようにするには以下の定義も必要
# @rbs @name: Profile?
こういう、Steep とかがうまく解析できるように色々 RBS を用意するのは結構求められるのですが、手で書くのは面倒です。
ということで生成するスクリプトを書きたいのですが、ゼロから書くのは大変なので Claude Code にやらせてみたところ結構うまくいったので、個人的に試したこととかを書いていこうと思います。
ちなみにサンプルとして、自分が最近作ってもらったスクリプトのサンプルも置いておきます。
実際にこれらに生成させている RBS ファイルはこんな感じです。
結論
こんな感じの指示を与えれば OK
以下のようなメモ化代入を含むコードを検出して、インスタンス変数の RBS 定義を作成してくれるような script を作成してほしい。
解析には prism や rbs gem を使ってほしい。
def profile #: Profile
@profile ||= Profile.find(id)
end
Ruby コード, RBS の解析は prism, rbs gem を使えば OK
Claude Code に与える指示としては、こんな感じのことを与えればまあやってくれます。
以下のようなメモ化代入を含むコードを検出して、インスタンス変数の RBS 定義を作成してくれるような script を作成してほしい。
def profile #: Profile
@profile ||= Profile.find(id)
end
ただ、使う gem を指定しないとコードを正規表現で解析したりする(保守性が)ヤバいコードを生み出すことがまあまああります…。
ある程度保守性が高い実装になるように、静的解析向けのライブラリを指定して指示しておいた方が良いです。
ここでは prism, rbs gem をおすすめしておきます。これらは Ruby に含まれる gem (default gem, bundled gem) なので、追加のインストールが不要なことが多いです。
Claude Code が生成するコードのイメージを掴むためにも、それぞれ軽く説明します。
prism: Ruby コードのパーサー
prism は Ruby コードのパーサーで、Ruby コードの解析を行えます。
例えば、こういう実装で、「コード中で定義されたメソッドの名前の一覧」の出力が行えたりします。
require "prism"
class MethodPrintVisitor < Prism::Visitor
def initialize
@visited_methods = []
super
end
def visit_def_node(node)
method_name = node.name
@visited_methods << method_name
super
end
attr_reader :visited_methods
end
visitor = MethodPrintVisitor.new
Prism.parse_file(ARGV[0]).value.accept(visitor)
visitor.visited_methods.each do |method_name|
puts method_name
end
$ ruby script/ruby_methods_visitor.rb lib/ruboty/adapters/slack_events/slack_events_handler.rb
initialize
handle_event
on_generic_event
on_message
on_event_callback
on_events_api
詳しい使い方は↓のリファレンスを見ると良いです。
Ruby のコードを解析して何かしてもらう、みたいなケースだと prism を使ってもらうようにすれば基本的には良いと思います。
rbs: Ruby の型やクラス構造の解析
rbs は言わずと知れた Ruby の型定義用の言語ですが、 rbs gem を使うとこれらを使った解析が行えます。
例えば、Class の継承関係などは rbs gem に解析させると早いです。
# 指定したクラス名の ancestor を列挙するスクリプト
require 'rbs'
require 'pathname'
# Load RBS environment
loader = RBS::EnvironmentLoader.new
loader.add(path: Pathname('sig'))
loader.add(path: Pathname('.gem_rbs_collection'))
environment = RBS::Environment.from_loader(loader).resolve_type_names
*type_namespaces, type_name = ARGV[0].split('::').reject(&:empty?).map(&:to_sym)
namespace = RBS::Namespace.new(path: type_namespaces, absolute: true)
typename = RBS::TypeName.new(name: type_name, namespace: namespace)
decl = environment.class_decls[typename]
if decl.nil?
puts "Type not found: #{typename}"
exit 1
end
puts "Ancestors of #{typename}:"
puts
builder = RBS::DefinitionBuilder.new(env: environment)
definition = builder.build_instance(typename)
definition.ancestors.ancestors.each do |ancestor|
puts " #{ancestor.name}"
end
$ ruby script/print_ancestors.rb "::Ruboty::AiAgent::ChatThreadMessages"
Ancestors of ::Ruboty::AiAgent::ChatThreadMessages:
::Ruboty::AiAgent::ChatThreadMessages
::Ruboty::AiAgent::ChatThreadAssociations
::Ruboty::AiAgent::RecordSet
::Object
::ActiveSupport::Tryable
::Kernel
::BasicObject
こういった継承関係等の解析は prism ではなく rbs gem にやらせた方が効率的です。
ただ、rbs gem は Ruby コードを直接解析できないので、一度 rbs-inline に ruby コードを元に rbs ファイルを生成させ、それを rbs gem に解析させるのが良いです。
注意したほうが良いこととしては、スクリプト自身が解析する RBS ファイルに、自身が出力する RBS ファイルを含めない ということです。自身が出力した RBS ファイルを、次回の解析に利用してしまうと、実行結果が変わってしまう (冪等ではなくなる) 可能性があるからです。
こういった感じで除外するようにしましょう。
loader = RBS::EnvironmentLoader.new
# スクリプトが出力した RBS ファイル (ここでは generated-by-scripts 内) を除外する
Pathname('sig').children.select(&:directory?).each do |dir|
next if dir.basename.to_s == 'generated-by-scripts'
loader.add(path: dir)
end
生成に使うスクリプトを Rake task 化しておく
スクリプトを色々作っていくと、それぞれ叩くのが大変なので Rake Task とかでまとめておくと良いです。
自分が最近書いた https://github.com/tomoasleep/ruboty-ai_agent とかではこういう感じで書いてます。
また、スクリプトに RBS ファイルを出力させる際、特定のディレクトリに全て生成させるようにしておくと良いです。自分は sig/generated-by-scripts 的な名前で用意しています。
namespace :rbs do
desc 'Clean generated RBS files'
task :clean do
sh('rm -rf sig/generate')
sh('rm -rf sig/generated-by-scripts')
end
desc 'Install rbs collection'
task :collection do
sh('bundle exec rbs collection install')
end
desc 'Run rbs-inline to generate RBS files'
task :inline do
sh('script/clean-orphaned-rbs.rb')
sh('bundle exec rbs-inline --opt-out --output lib')
end
desc 'Generate RBS definitions by script/generate-rbs.rb'
task :script do
sh('script/generate-data-rbs.rb')
sh('script/generate-concern-rbs.rb')
sh('script/generate-memorized-ivar-rbs.rb')
end
end
# rake rbs で生成をまとめて行えるようにしておく
task rbs: %i[rbs:inline rbs:script]