Proposal: DSL Plugin system for dynamic method-defining DSLs
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 28/100
Hướng nghiên cứu
Bắt đầu với lib/typeprof/core/ast/meta.rb và luồng Service#new sau Builtin.deploy. Theo dõi cách các handler của MethodEntity và các nút meta hiện có được biểu diễn trước khi đánh giá Registry và Scope API được đề xuất. Để được xem là hoàn tất, cần thống nhất phạm vi triển khai cho việc đăng ký, cài đặt và phát hiện plugin, nhưng đề xuất không nêu các bài kiểm thử cũng như một thay đổi đầu tiên có phạm vi giới hạn.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Motivation
Ruby has many DSLs that define methods or mix in modules dynamically:
define_method(:hello) { "hi" }addshelloto the receiver.Forwardable#def_delegator :@target, :foogenerates a delegating methodfoo.- Rails'
belongs_to :usergeneratesuser,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:
- Each
on "X#foo"declaration registers(cpath, mid, singleton) -> PluginClassinTypeProf::Dsl::Registry. Service#newcallsRegistry.apply(genv)right afterBuiltin.deploy. For each entry,applyresolves the targetMethodEntityand installs a handler intome.builtin.- When that method is later called during analysis, the handler builds a
Scopeand callsplugin.install(scope). It returnsfalseso 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 intypeprof.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
--dslflag andtypeprof.conf.jsonc"dsl"key follow once it stabilizes.
- Ngôn ngữ chính
- Ruby
- Star
- 833
- Fork
- 99
- Merge trung bình
- 9 giờ 30 phút
- Pull request đã merge (30 ngày)
- 12
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của ruby/typeprof
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 45/100
-
invalid option: --init Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 25/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 38/100
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 30/100
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 35/100
Tất cả issue của ruby/typeprof
Issue tương tự
-
SyncEm always forwards a dummy block, so wrapped methods lose their no-block/Enumerator behavior Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
endoflife-date/endoflife.date#11086 ·
-
internal
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
Kong/developer.konghq.com#7322 ·
-
bug P2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100