ruby/typeprof#460 の日本語版。
Ruby にはメソッドを動的に定義したり、モジュールを mix-in したりする DSL が数多くあります。
define_method(:hello) { "hi" }はレシーバにhelloを追加するForwardable#def_delegator :@target, :fooは委譲メソッドfooを生成する- Rails の
belongs_to :userはuser/user=/build_user/create_userなどを生成する
TypeProf は現状、これらの一部のみを手書きの MetaNode(lib/typeprof/core/ast/meta.rb)で特別扱いしていますが、大部分は未対応 — 特に Rails 関連が顕著です。
この Issue では、TypeProf 本体に手を入れずにこれらの DSL に対応するための DSL Plugin システムを提案します。サードパーティが外部 gem として配布することも可能になります。
DSL Plugin はメソッド呼び出しが 具体的な MethodEntity に解決された時 に発火します(例:ActiveRecord::Associations::ClassMethods#belongs_to、belongs_to という名前そのものではない)。発火後、Plugin はメソッド・モジュール・mix-in 関係などを副作用として合成します。
Registry の流れ:
- 各
on "X#foo"宣言が(cpath, mid, singleton) -> PluginClassをTypeProf::Dsl::Registryに登録する。 Service#newはBuiltin.deploy直後にRegistry.apply(genv)を呼ぶ。登録された各エントリについて、applyは対象のMethodEntityを解決してme.builtinにハンドラを仕込む。- 後にそのメソッドが解析中に呼ばれると、ハンドラは
Scopeを構築してplugin.install(scope)を呼ぶ。falseを返すことで TypeProf は呼び出しの戻り値を引き続き RBS で解決し、Plugin は副作用の追加だけを行う。
Plugin が読み取るのは AST と型推論の Vertex のみで、TypeProf はユーザーのアプリを boot することも、文字列を eval することもしません。既存の手書き MetaNode(attr_reader / include / Struct.new など)はそのまま動作し、Plugin 層は純粋な追加機能として共存します。
TypeProf 本体に同梱される Plugin(lib/typeprof/dsl/ruby/*.rb)は起動時に自動ロードされます。外部 Plugin gem は次のいずれかで有効化します:
-
CLI:
typeprof --dsl typeprof-rails app.rb -
設定ファイル:
typeprof.conf.jsoncの"dsl"キー:
引数は gem 名です。TypeProf が gem を require し、gem 側はロード時に Plugin を登録します。
Rails の belongs_to を解釈する Plugin の例です。Post クラス内の belongs_to :user を、Post::GeneratedAssociationMethods(Post が include する)上の def user: -> User? に変換します。
module TypeProf
module Dsl
module ActiveRecord
class Associations < TypeProf::Dsl::Base
on "ActiveRecord::Associations::ClassMethods#belongs_to"
def install(scope)
# 1. アソシエーション名を読み取る
name = scope.arg_symbol(0) or return
# 2. Rails の命名規約からアソシエーションのクラスを解決する
klass = scope.resolve_const(default_name_for(name)) or return
# 3. メソッドを定義する先のモジュールを取得 or 作成する
mod = scope.find_module("GeneratedAssociationMethods")
unless mod
mod = scope.create_module("GeneratedAssociationMethods")
scope.owner.include_module(mod)
end
# 4. メソッドを定義する
mod.define_method(name, returns: scope.nilable(klass))
end
end
end
end
end| トリガー | 発火条件 |
|---|---|
on "X#foo" |
呼び出しがインスタンスメソッド X#foo に解決された時 |
on "X.foo" |
呼び出しが特異メソッド X.foo に解決された時 |
on_inherit "X" |
クラスの祖先チェーンに X が含まれる場合 |
on_include "X" |
モジュール本体で include X が実行された時 |
on_extend "X" |
モジュール本体で extend X が実行された時 |
on_prepend "X" |
モジュール本体で prepend X が実行された時 |
on "X#foo" の # / . は 定義側の特異性 を表します。
scope.mid #: () -> Symbol Plugin を発火させたメソッド名
scope.owner #: () -> ScopeOwner 呼び出し元モジュールのラッパ
scope.has_block? #: () -> bool 呼び出しがブロックを持つか各ヘルパーは位置引数 / キーワード引数から単一の具体リテラルを取り出します。引数が無い、リテラルでない、複数値の union、untyped の場合は nil を返します。
現在、Vertex 上でリテラル値が保持されるのは Symbol と true/false のみです。String / Integer のリテラルは型のみ流れるため、arg_string / kwarg_string は意図的に省いています。
scope.arg_symbol(idx) #: (Integer) -> Symbol?
scope.arg_symbols_from(idx) #: (Integer) -> Array[Symbol]? Symbol の残り全部を集める
scope.kwarg_symbol(:key) #: (Symbol) -> Symbol?
scope.kwarg_bool(:key) #: (Symbol) -> bool?scope.resolve_const("User") #: (String) -> Type? cref を辿る定数解決
scope.nilable(t) #: (Type) -> Type t | nil
scope.array_of(t) #: (Type) -> Type Array[t]
scope.hash_of(k, v) #: (Type, Type) -> Type Hash[k, v]
scope.method_return_type(:"@x", :foo) #: (Symbol, Symbol) -> Type @x.foo の戻り値型
scope.untyped #: () -> Type フォールバックmod = scope.find_module("ClassMethods") #: (String) -> ModuleEntity? 無ければ nil
mod = scope.create_module("Generated") #: (String) -> ModuleEntity 既存なら例外
mod.include_module(other) #: (ModuleEntity) -> void mix in(idempotent)
mod.extend_module(other) #: (ModuleEntity) -> void extend(idempotent)mod.define_method(:foo, params: [...], returns: t) #: (Symbol, params: Array[Type], returns: Type) -> void
mod.define_singleton_method(:bar, params: [...], returns: t) #: (Symbol, params: Array[Type], returns: Type) -> void
mod.define_method_from_block(:baz) #: (Symbol) -> void 渡されたブロックをメソッド本体に流用
mod.define_ivar(:"@count", type: t) #: (Symbol, type: Type) -> void- この Issue は最終形の概要です。細かい点は実装時に調整します。
- Scope API はまず本体同梱の Plugin から始めます。安定したら
--dslフラグとtypeprof.conf.jsoncの"dsl"キーを追加します。
{ "dsl": ["typeprof-rails"] }