Proposal: DSL Plugin system for dynamic method-defining DSLs
还没有人认领这个 Issue。
评估
调研方向
从 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" }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.
- 主要语言
- Ruby
- 星标
- 833
- 派生
- 99
- 平均合并
- 9 小时 30 分钟
- 30 天内合并 PR
- 12
贡献指南
这个仓库没有索引到贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
ruby/typeprof 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 45/100
-
难度 3/5 1-2 天 新手友好度 25/100
-
难度 2/5 1-3 小时 新手友好度 38/100
-
难度 3/5 1-2 天 新手友好度 30/100
-
难度 3/5 1-2 天 新手友好度 35/100
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 75/100
palladius/rails8-app-on-gcp#145 ·
-
难度 2/5 1-3 小时 新手友好度 75/100
rubocop/rubocop-rspec#2236 ·
-
bug
难度 2/5 1-3 小时 新手友好度 70/100
riscv/riscv-unified-db#2624 · 1 个 reaction ·
-
难度 1/5 1 小时以内 新手友好度 88/100
-
难度 2/5 1-3 小时 新手友好度 78/100