Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Proposal: DSL Plugin system for dynamic method-defining DSLs

オープン
#460 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
28/100
issue の種類
機能追加
明瞭さ
説明が足りない
活発さ
静か
技術スタック
ruby
領域
devtools

調査の方向性

lib/typeprof/core/ast/meta.rb を起点にし、Builtin.deploy 後の Service#new フローを追跡してください。提案されている Registry および Scope API を評価する前に、MethodEntity ハンドラーと既存の meta ノードがどのように表現されているかを確認してください。完了とするには、プラグインの登録、インストール、ディスカバリーに関する合意済みの実装範囲が必要ですが、提案ではテストも、範囲を限定した最初の変更も示されていません。

索引モデルが issue の本文から書いたものです。

説明

Motivation

Ruby has many DSLs that define methods or mix in modules dynamically:

  • define_method(:hello) { "hi" } adds hello to the receiver.
  • Forwardable#def_delegator :@target, :foo generates a delegating method foo.
  • Rails' belongs_to :user generates user, user=, build_user, create_user, etc.

TypeProf currently special-cases a few of these in hand-written meta nodes (lib/typeprof/core/ast/meta.rb), but most are not — Rails in particular.

This issue proposes a DSL Plugin system that extends TypeProf to handle these DSLs without touching the core. Third parties can ship support as external gems.

Design

A DSL Plugin fires when a method call resolves to a specific MethodEntity (for example, ActiveRecord::Associations::ClassMethods#belongs_to, not the bare name belongs_to). It then synthesises methods, modules, or mix-in relations as a side effect.

The Registry flow:

  1. Each on "X#foo" declaration registers (cpath, mid, singleton) -> PluginClass in TypeProf::Dsl::Registry.
  2. Service#new calls Registry.apply(genv) right after Builtin.deploy. For each entry, apply resolves the target MethodEntity and installs a handler into me.builtin.
  3. When that method is later called during analysis, the handler builds a Scope and calls plugin.install(scope). It returns false so TypeProf still resolves the call's return type through RBS — the plugin only adds side effects.

Plugins read the AST and type-inference Vertices only — TypeProf never boots the user's app and never evaluates strings. Existing hand-written meta nodes (attr_reader, include, Struct.new, etc.) keep working as is; this plugin layer is a pure addition.

Plugin discovery

Plugins bundled with TypeProf core (lib/typeprof/dsl/ruby/*.rb) are auto-loaded on boot. External plugin gems are enabled via either of:

  • CLI: typeprof --dsl typeprof-rails app.rb

  • Config: a "dsl" key in typeprof.conf.jsonc:

    {
      "dsl": ["typeprof-rails"]
    }
    

The argument is a gem name. TypeProf requires the gem, which registers its plugins on load.

Plugin authoring

A plugin for Rails' belongs_to. It turns belongs_to :user in Post into def user: -> User? on Post::GeneratedAssociationMethods (a module included by Post).

module TypeProf
  module Dsl
    module ActiveRecord
      class Associations < TypeProf::Dsl::Base
        on "ActiveRecord::Associations::ClassMethods#belongs_to"

        def install(scope)
          # 1. Read the association name.
          name = scope.arg_symbol(0) or return

          # 2. Resolve the associated class by Rails' naming convention.
          klass = scope.resolve_const(default_name_for(name)) or return

          # 3. Locate or create the module to attach methods to.
          mod = scope.find_module("GeneratedAssociationMethods")
          unless mod
            mod = scope.create_module("GeneratedAssociationMethods")
            scope.owner.include_module(mod)
          end

          # 4. Define the method.
          mod.define_method(name, returns: scope.nilable(klass))
        end
      end
    end
  end
end
Triggers
Trigger Fires when
on "X#foo" Call resolves to instance method X#foo
on "X.foo" Call resolves to singleton method X.foo
on_inherit "X" A class is defined with X in its ancestor chain
on_include "X" A module body executes include X
on_extend "X" A module body executes extend X
on_prepend "X" A module body executes prepend X

Inside an on "X#foo" pattern, # always denotes an instance method and . a singleton method on the definition side.

Scope API
Context
scope.mid          #: () -> Symbol           method name that triggered the plugin
scope.owner        #: () -> ScopeOwner       wrapper for the calling module
scope.has_block?   #: () -> bool             whether a block was passed
Reading arguments

Each helper extracts a single concrete literal from a positional or keyword argument. Returns nil if the argument is missing, isn't a literal, resolves to multiple values, or is untyped.

Only Symbol and true/false literal values are preserved on Vertices today. String / Integer literals flow as types only, so arg_string / kwarg_string are intentionally omitted.

scope.arg_symbol(idx)         #: (Integer) -> Symbol?
scope.arg_symbols_from(idx)   #: (Integer) -> Array[Symbol]?    collect a Symbol rest list

scope.kwarg_symbol(:key)      #: (Symbol) -> Symbol?
scope.kwarg_bool(:key)        #: (Symbol) -> bool?
Constructing types
scope.resolve_const("User")            #: (String) -> Type?         cref-walking lookup
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   return type of @x.foo
scope.untyped                          #: () -> Type                 fallback
Resolving and creating modules
mod = scope.find_module("ClassMethods")     #: (String) -> ModuleEntity?    nil if missing
mod = scope.create_module("Generated")       #: (String) -> ModuleEntity     raises if exists

mod.include_module(other)                    #: (ModuleEntity) -> void       mix in (idempotent)
mod.extend_module(other)                     #: (ModuleEntity) -> void       extend (idempotent)
Defining methods on a module
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                                       reuse the passed block as body
mod.define_ivar(:"@count", type: t)                           #: (Symbol, type: Type) -> void

Notes

  • This issue describes the target shape. Details may change during implementation.
  • The Scope API will land for core-bundled plugins first. The --dsl flag and typeprof.conf.jsonc "dsl" key follow once it stabilizes.
主要言語
Ruby
スター
833
フォーク
99
平均マージ
9時間 30分
マージ済み PR(30日)
12

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

ruby/typeprof のほかの issue

ruby/typeprof の issue をすべて見る

似ている issue

Ruby の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。