Proposal: DSL Plugin system for dynamic method-defining DSLs
Nessuno ha ancora preso questa issue.
- Lingua principale
- Ruby
- Stelle
- 833
- Fork
- 99
- Merge medio
- 9h 30m
- PR unite (30g)
- 12
Descrizione
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.
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Inizia con lib/typeprof/core/ast/meta.rb e con il flusso di Service#new dopo Builtin.deploy. Traccia come sono rappresentati i gestori di MethodEntity e i nodi meta esistenti prima di valutare le Registry e Scope API proposte. Per considerarlo completato sarebbe necessario concordare l’ambito di implementazione per la registrazione, l’installazione e la discovery dei plugin, ma la proposta non indica né test né una prima modifica circoscritta.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- ruby
- Ambito
- devtools
- Tipo di issue
- Funzionalità
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Stato di attività
- Tranquilla
- Chiarezza
- Da chiarire
- Idoneità per principianti
- 28/100