tapioca 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +84 -0
- data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
- data/lib/tapioca/cli.rb +9 -0
- data/lib/tapioca/commands/abstract_dsl.rb +76 -20
- data/lib/tapioca/commands/abstract_gem.rb +1 -0
- data/lib/tapioca/commands/annotations.rb +1 -1
- data/lib/tapioca/commands/gem_generate.rb +20 -4
- data/lib/tapioca/dsl/compiler.rb +24 -10
- data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
- data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_delegated_types.rb +64 -16
- data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +17 -11
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +2 -2
- data/lib/tapioca/dsl/compilers/config.rb +1 -1
- data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
- data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
- data/lib/tapioca/dsl/compilers/url_helpers.rb +312 -8
- data/lib/tapioca/dsl/compilers.rb +1 -1
- data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +10 -3
- data/lib/tapioca/dsl/pipeline.rb +6 -27
- data/lib/tapioca/gem/events.rb +1 -1
- data/lib/tapioca/gem/listeners/documentation.rb +13 -8
- data/lib/tapioca/gem/listeners/methods.rb +79 -13
- data/lib/tapioca/gem/listeners/sorbet_enums.rb +1 -1
- data/lib/tapioca/gem/pipeline.rb +3 -3
- data/lib/tapioca/gemfile.rb +13 -0
- data/lib/tapioca/helpers/env_helper.rb +1 -1
- data/lib/tapioca/helpers/file_helper.rb +42 -0
- data/lib/tapioca/helpers/rbi_files_helper.rb +96 -5
- data/lib/tapioca/helpers/rbi_helper.rb +17 -3
- data/lib/tapioca/helpers/sorbet_helper.rb +1 -1
- data/lib/tapioca/internal.rb +2 -0
- data/lib/tapioca/loaders/gem.rb +33 -0
- data/lib/tapioca/loaders/loader.rb +0 -13
- data/lib/tapioca/rbi_ext/model.rb +16 -2
- data/lib/tapioca/rbs/bootsnap_cache.rb +52 -0
- data/lib/tapioca/rbs/rewriter.rb +91 -25
- data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
- data/lib/tapioca/runtime/generic_type_registry.rb +38 -14
- data/lib/tapioca/runtime/reflection.rb +18 -1
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +0 -20
- data/lib/tapioca/sorbet_ext/generic_type_patch.rb +48 -0
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +11 -10
- metadata +9 -6
|
@@ -33,7 +33,7 @@ module Tapioca
|
|
|
33
33
|
# include GeneratedDelegatedTypeMethods
|
|
34
34
|
#
|
|
35
35
|
# module GeneratedDelegatedTypeMethods
|
|
36
|
-
# sig { params(args: T.untyped).returns(T.any(Message, Comment)) }
|
|
36
|
+
# sig { params(args: T.untyped).returns(T.any(::Message, ::Comment)) }
|
|
37
37
|
# def build_entryable(*args); end
|
|
38
38
|
#
|
|
39
39
|
# sig { returns(Class) }
|
|
@@ -45,7 +45,7 @@ module Tapioca
|
|
|
45
45
|
# sig { returns(T::Boolean) }
|
|
46
46
|
# def message?; end
|
|
47
47
|
#
|
|
48
|
-
# sig { returns(T.nilable(Message)) }
|
|
48
|
+
# sig { returns(T.nilable(::Message)) }
|
|
49
49
|
# def message; end
|
|
50
50
|
#
|
|
51
51
|
# sig { returns(T.nilable(Integer)) }
|
|
@@ -54,7 +54,7 @@ module Tapioca
|
|
|
54
54
|
# sig { returns(T::Boolean) }
|
|
55
55
|
# def comment?; end
|
|
56
56
|
#
|
|
57
|
-
# sig { returns(T.nilable(Comment)) }
|
|
57
|
+
# sig { returns(T.nilable(::Comment)) }
|
|
58
58
|
# def comment; end
|
|
59
59
|
#
|
|
60
60
|
# sig { returns(T.nilable(Integer)) }
|
|
@@ -67,6 +67,9 @@ module Tapioca
|
|
|
67
67
|
class ActiveRecordDelegatedTypes < Compiler
|
|
68
68
|
include Helpers::ActiveRecordConstantsHelper
|
|
69
69
|
|
|
70
|
+
# A delegated type entry paired with the fully-qualified constant name it resolves to.
|
|
71
|
+
ResolvedType = Struct.new(:raw_name, :qualified_name, keyword_init: true)
|
|
72
|
+
|
|
70
73
|
# @override
|
|
71
74
|
#: -> void
|
|
72
75
|
def decorate
|
|
@@ -77,8 +80,11 @@ module Tapioca
|
|
|
77
80
|
constant.__tapioca_delegated_types.each do |role, data|
|
|
78
81
|
types = data.fetch(:types)
|
|
79
82
|
options = data.fetch(:options, {})
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
resolved_types = types.map do |type|
|
|
84
|
+
ResolvedType.new(raw_name: type, qualified_name: qualified_type_name(type, role))
|
|
85
|
+
end
|
|
86
|
+
populate_role_accessors(mod, role, resolved_types)
|
|
87
|
+
populate_type_helpers(mod, role, resolved_types, options)
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
|
|
@@ -96,8 +102,8 @@ module Tapioca
|
|
|
96
102
|
|
|
97
103
|
private
|
|
98
104
|
|
|
99
|
-
#: (RBI::Scope mod, Symbol role, Array[
|
|
100
|
-
def populate_role_accessors(mod, role,
|
|
105
|
+
#: (RBI::Scope mod, Symbol role, Array[ResolvedType] resolved_types) -> void
|
|
106
|
+
def populate_role_accessors(mod, role, resolved_types)
|
|
101
107
|
mod.create_method(
|
|
102
108
|
"#{role}_name",
|
|
103
109
|
parameters: [],
|
|
@@ -113,20 +119,20 @@ module Tapioca
|
|
|
113
119
|
mod.create_method(
|
|
114
120
|
"build_#{role}",
|
|
115
121
|
parameters: [create_rest_param("args", type: "T.untyped")],
|
|
116
|
-
return_type:
|
|
122
|
+
return_type: build_return_type(resolved_types),
|
|
117
123
|
)
|
|
118
124
|
end
|
|
119
125
|
|
|
120
|
-
#: (RBI::Scope mod, Symbol role, Array[
|
|
121
|
-
def populate_type_helpers(mod, role,
|
|
122
|
-
|
|
123
|
-
populate_type_helper(mod, role,
|
|
126
|
+
#: (RBI::Scope mod, Symbol role, Array[ResolvedType] resolved_types, Hash[Symbol, untyped] options) -> void
|
|
127
|
+
def populate_type_helpers(mod, role, resolved_types, options)
|
|
128
|
+
resolved_types.each do |resolved_type|
|
|
129
|
+
populate_type_helper(mod, role, resolved_type, options)
|
|
124
130
|
end
|
|
125
131
|
end
|
|
126
132
|
|
|
127
|
-
#: (RBI::Scope mod, Symbol role,
|
|
128
|
-
def populate_type_helper(mod, role,
|
|
129
|
-
singular =
|
|
133
|
+
#: (RBI::Scope mod, Symbol role, ResolvedType resolved_type, Hash[Symbol, untyped] options) -> void
|
|
134
|
+
def populate_type_helper(mod, role, resolved_type, options)
|
|
135
|
+
singular = resolved_type.raw_name.tableize.tr("/", "_").singularize
|
|
130
136
|
query = "#{singular}?"
|
|
131
137
|
primary_key = options[:primary_key] || "id"
|
|
132
138
|
role_id = options[:foreign_key] || "#{role}_id"
|
|
@@ -142,7 +148,7 @@ module Tapioca
|
|
|
142
148
|
mod.create_method(
|
|
143
149
|
singular,
|
|
144
150
|
parameters: [],
|
|
145
|
-
return_type: "T.nilable(#{
|
|
151
|
+
return_type: "T.nilable(#{resolved_type.qualified_name})",
|
|
146
152
|
)
|
|
147
153
|
|
|
148
154
|
mod.create_method(
|
|
@@ -151,6 +157,48 @@ module Tapioca
|
|
|
151
157
|
return_type: as_nilable_type(getter_type),
|
|
152
158
|
)
|
|
153
159
|
end
|
|
160
|
+
|
|
161
|
+
# Collapses to `T.untyped` if any member is `T.untyped`, since `T.any(::Foo, T.untyped)`
|
|
162
|
+
# is equivalent to `T.untyped` in Sorbet and the per-type error has already been recorded.
|
|
163
|
+
#: (Array[ResolvedType] resolved_types) -> String
|
|
164
|
+
def build_return_type(resolved_types)
|
|
165
|
+
qualified_types = resolved_types.map(&:qualified_name)
|
|
166
|
+
if qualified_types.include?("T.untyped")
|
|
167
|
+
"T.untyped"
|
|
168
|
+
elsif qualified_types.size == 1
|
|
169
|
+
qualified_types.fetch(0)
|
|
170
|
+
else
|
|
171
|
+
"T.any(#{qualified_types.join(", ")})"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Resolves a delegated type entry to a fully-qualified constant name. The strings passed
|
|
176
|
+
# to `delegated_type(..., types: %w[...])` are written verbatim into the generated RBI,
|
|
177
|
+
# but the surrounding `class A::B::C` scope omits `A` and `A::B` from Sorbet's lexical
|
|
178
|
+
# nesting, so a bare `D` reference fails to resolve to `A::B::D` even when that constant
|
|
179
|
+
# exists. `compute_type` is `ActiveRecord::Base`'s own (private) namespace-walking lookup
|
|
180
|
+
# — the same one Rails uses for STI and polymorphic associations — so it resolves both
|
|
181
|
+
# bare and fully-qualified names. When the constant can't be resolved (NameError) or its
|
|
182
|
+
# qualified name can't be derived (anonymous class) we record a compiler error and emit
|
|
183
|
+
# `T.untyped`, which both surfaces the problem and keeps the generated RBI type-checkable.
|
|
184
|
+
#: (String type, Symbol role) -> String
|
|
185
|
+
def qualified_type_name(type, role)
|
|
186
|
+
klass = constant.send(:compute_type, type)
|
|
187
|
+
qualified_name = qualified_name_of(klass)
|
|
188
|
+
return qualified_name if qualified_name
|
|
189
|
+
|
|
190
|
+
add_unresolvable_type_error(type, role)
|
|
191
|
+
rescue NameError, LoadError
|
|
192
|
+
add_unresolvable_type_error(type, role)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
#: (String type, Symbol role) -> String
|
|
196
|
+
def add_unresolvable_type_error(type, role)
|
|
197
|
+
add_error(<<~MSG.strip)
|
|
198
|
+
Cannot generate delegated_type `#{role}` on `#{constant}` since the type `#{type}` could not be resolved.
|
|
199
|
+
MSG
|
|
200
|
+
"T.untyped"
|
|
201
|
+
end
|
|
154
202
|
end
|
|
155
203
|
end
|
|
156
204
|
end
|
|
@@ -26,13 +26,21 @@ module Tapioca
|
|
|
26
26
|
# # test_case.rbi
|
|
27
27
|
# # typed: true
|
|
28
28
|
# class ActiveSupport::TestCase
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# sig {
|
|
32
|
-
#
|
|
29
|
+
# include ActiveRecord::TestFixtures
|
|
30
|
+
#
|
|
31
|
+
# sig { returns(T::Array[Post]) } # No names: returns an Array of all fixtures
|
|
32
|
+
# sig { params(fixture_name: T.any(String, Symbol)).returns(Post) } # One name: returns the requested fixture
|
|
33
|
+
# sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)) # Many names: returns an Array of the requested fixtures
|
|
34
|
+
# .returns(T::Array[Post]) }
|
|
33
35
|
# def posts(fixture_name = nil, *other_fixtures); end
|
|
34
36
|
# end
|
|
35
37
|
# ~~~
|
|
38
|
+
#
|
|
39
|
+
# The `include` is generated because Rails mixes `ActiveRecord::TestFixtures` into
|
|
40
|
+
# `ActiveSupport::TestCase` through the `:active_support_test_case` load hook in
|
|
41
|
+
# `rails/test_help.rb`. Since RBI generation does not load an app's test helper, this runtime
|
|
42
|
+
# include is not captured. Without it, Sorbet does not see the class methods the module
|
|
43
|
+
# contributes via `mixes_in_class_methods`, such as `fixtures`.
|
|
36
44
|
#: [ConstantType = singleton(ActiveSupport::TestCase)]
|
|
37
45
|
class ActiveRecordFixtures < Compiler
|
|
38
46
|
MISSING = Object.new
|
|
@@ -47,9 +55,10 @@ module Tapioca
|
|
|
47
55
|
end
|
|
48
56
|
|
|
49
57
|
method_names.select! { |name| fixture_class_mapping_from_fixture_files[name] != MISSING }
|
|
50
|
-
return if method_names.empty?
|
|
51
58
|
|
|
52
59
|
root.create_path(constant) do |mod|
|
|
60
|
+
mod.create_include("ActiveRecord::TestFixtures")
|
|
61
|
+
|
|
53
62
|
method_names.each do |name|
|
|
54
63
|
create_fixture_method(mod, name.to_s)
|
|
55
64
|
end
|
|
@@ -110,19 +119,16 @@ module Tapioca
|
|
|
110
119
|
node.add_opt_param("fixture_name", "nil")
|
|
111
120
|
node.add_rest_param("other_fixtures")
|
|
112
121
|
|
|
113
|
-
node.add_sig do |sig|
|
|
114
|
-
sig.add_param("fixture_name", "NilClass")
|
|
115
|
-
sig.add_param("other_fixtures", "NilClass")
|
|
122
|
+
node.add_sig do |sig| # No-parameter overload: returns an Array of all fixtures
|
|
116
123
|
sig.return_type = "T::Array[#{return_type}]"
|
|
117
124
|
end
|
|
118
125
|
|
|
119
|
-
node.add_sig do |sig|
|
|
126
|
+
node.add_sig do |sig| # One parameter overload: returns the requested fixture
|
|
120
127
|
sig.add_param("fixture_name", "T.any(String, Symbol)")
|
|
121
|
-
sig.add_param("other_fixtures", "NilClass")
|
|
122
128
|
sig.return_type = return_type
|
|
123
129
|
end
|
|
124
130
|
|
|
125
|
-
node.add_sig do |sig|
|
|
131
|
+
node.add_sig do |sig| # Multi-parameter overload: returns an Array of the requested fixtures
|
|
126
132
|
sig.add_param("fixture_name", "T.any(String, Symbol)")
|
|
127
133
|
sig.add_param("other_fixtures", "T.any(String, Symbol)")
|
|
128
134
|
sig.return_type = "T::Array[#{return_type}]"
|
|
@@ -224,10 +224,10 @@ module Tapioca
|
|
|
224
224
|
order: ["T.any(Symbol, T::Array[Symbol])", ":asc"],
|
|
225
225
|
cursor: ["T.untyped", "primary_key"],
|
|
226
226
|
use_ranges: ["T.untyped", "nil"],
|
|
227
|
-
} #: Hash[Symbol, [String, String]]
|
|
227
|
+
}.freeze #: Hash[Symbol, [String, String]]
|
|
228
228
|
CALCULATION_METHODS = ActiveRecord::Calculations.instance_methods(false) #: Array[Symbol]
|
|
229
229
|
RELATION_METHODS = ActiveRecord::Relation.instance_methods(false) #: Array[Symbol]
|
|
230
|
-
TO_ARRAY_METHODS = [:to_ary, :to_a] #: Array[Symbol]
|
|
230
|
+
TO_ARRAY_METHODS = [:to_ary, :to_a].freeze #: Array[Symbol]
|
|
231
231
|
|
|
232
232
|
private
|
|
233
233
|
|
|
@@ -120,7 +120,7 @@ module Tapioca
|
|
|
120
120
|
schema = constant.schema
|
|
121
121
|
|
|
122
122
|
# empty? does not exist on JsonApiClient::Schema
|
|
123
|
-
schema if schema.size > 0
|
|
123
|
+
schema if schema.size > 0
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
#: (RBI::Scope mod, ::JsonApiClient::Schema::Property property) -> void
|
|
@@ -46,7 +46,7 @@ module Tapioca
|
|
|
46
46
|
# @override
|
|
47
47
|
#: -> void
|
|
48
48
|
def decorate
|
|
49
|
-
return unless constant.
|
|
49
|
+
return unless constant.method_defined?(:perform)
|
|
50
50
|
|
|
51
51
|
root.create_path(constant) do |worker|
|
|
52
52
|
method_def = constant.instance_method(:perform)
|
|
@@ -7,7 +7,22 @@ module Tapioca
|
|
|
7
7
|
module Dsl
|
|
8
8
|
module Compilers
|
|
9
9
|
# `Tapioca::Dsl::Compilers::UrlHelpers` generates RBI files for classes that include or extend
|
|
10
|
-
# [`Rails.application.routes.url_helpers`](https://api.rubyonrails.org/
|
|
10
|
+
# [`Rails.application.routes.url_helpers`](https://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#module-ActionDispatch::Routing::UrlFor-label-URL+generation+for+named+routes).
|
|
11
|
+
#
|
|
12
|
+
# The compiler registers generated constants to represent the Rails route helper modules:
|
|
13
|
+
#
|
|
14
|
+
# 1. `GeneratedPathHelpersModule` holds the main application's path helpers, such as `post_path`.
|
|
15
|
+
#
|
|
16
|
+
# 2. `GeneratedUrlHelpersModule` holds the main application's URL helpers, such as `post_url`.
|
|
17
|
+
#
|
|
18
|
+
# 3. `GeneratedMountedHelpers` is a synthetic module for mounted application and engine helpers, such as
|
|
19
|
+
# `main_app` and `articles`. Rails exposes these helpers through an anonymous dynamic module, so the compiler creates
|
|
20
|
+
# a named RBI module that can be included or extended by classes that receive mounted helpers at runtime. It is
|
|
21
|
+
# only generated for applications that mount an engine that defines its own routes.
|
|
22
|
+
#
|
|
23
|
+
# For mounted engines, the compiler also registers engine-scoped `GeneratedPathHelpersModule` and
|
|
24
|
+
# `GeneratedUrlHelpersModule` constants. Mounted engine helper methods return a synthetic
|
|
25
|
+
# `GeneratedRoutesProxy` subclass that includes those engine-scoped helper modules.
|
|
11
26
|
#
|
|
12
27
|
# For example, with the following setup:
|
|
13
28
|
#
|
|
@@ -16,6 +31,8 @@ module Tapioca
|
|
|
16
31
|
# class Application < Rails::Application
|
|
17
32
|
# routes.draw do
|
|
18
33
|
# resource :index
|
|
34
|
+
#
|
|
35
|
+
# mount Blog::Engine, at: "/blog", as: "articles"
|
|
19
36
|
# end
|
|
20
37
|
# end
|
|
21
38
|
# ~~~
|
|
@@ -42,6 +59,9 @@ module Tapioca
|
|
|
42
59
|
# include ActionDispatch::Routing::UrlFor
|
|
43
60
|
#
|
|
44
61
|
# sig { params(args: T.untyped).returns(String) }
|
|
62
|
+
# def articles_path(*args); end
|
|
63
|
+
#
|
|
64
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
45
65
|
# def edit_index_path(*args); end
|
|
46
66
|
#
|
|
47
67
|
# sig { params(args: T.untyped).returns(String) }
|
|
@@ -60,6 +80,9 @@ module Tapioca
|
|
|
60
80
|
# include ActionDispatch::Routing::UrlFor
|
|
61
81
|
#
|
|
62
82
|
# sig { params(args: T.untyped).returns(String) }
|
|
83
|
+
# def articles_url(*args); end
|
|
84
|
+
#
|
|
85
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
63
86
|
# def edit_index_url(*args); end
|
|
64
87
|
#
|
|
65
88
|
# sig { params(args: T.untyped).returns(String) }
|
|
@@ -78,6 +101,86 @@ module Tapioca
|
|
|
78
101
|
# include GeneratedUrlHelpersModule
|
|
79
102
|
# end
|
|
80
103
|
# ~~~
|
|
104
|
+
#
|
|
105
|
+
# ~~~rb
|
|
106
|
+
# # blog/config/routes.rb
|
|
107
|
+
# Blog::Engine.routes.draw do
|
|
108
|
+
# resources :posts
|
|
109
|
+
# end
|
|
110
|
+
# ~~~
|
|
111
|
+
#
|
|
112
|
+
# ~~~rbi
|
|
113
|
+
# # blog/engine/generated_path_helpers_module.rbi
|
|
114
|
+
# # typed: true
|
|
115
|
+
# module Blog::Engine::GeneratedPathHelpersModule
|
|
116
|
+
# include ActionDispatch::Routing::PolymorphicRoutes
|
|
117
|
+
# include ActionDispatch::Routing::UrlFor
|
|
118
|
+
#
|
|
119
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
120
|
+
# def edit_post_path(*args); end
|
|
121
|
+
#
|
|
122
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
123
|
+
# def new_post_path(*args); end
|
|
124
|
+
#
|
|
125
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
126
|
+
# def post_path(*args); end
|
|
127
|
+
#
|
|
128
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
129
|
+
# def posts_path(*args); end
|
|
130
|
+
# end
|
|
131
|
+
# ~~~
|
|
132
|
+
#
|
|
133
|
+
# ~~~rbi
|
|
134
|
+
# # blog/engine/generated_url_helpers_module.rbi
|
|
135
|
+
# # typed: true
|
|
136
|
+
# module Blog::Engine::GeneratedUrlHelpersModule
|
|
137
|
+
# include ActionDispatch::Routing::PolymorphicRoutes
|
|
138
|
+
# include ActionDispatch::Routing::UrlFor
|
|
139
|
+
#
|
|
140
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
141
|
+
# def edit_post_url(*args); end
|
|
142
|
+
#
|
|
143
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
144
|
+
# def new_post_url(*args); end
|
|
145
|
+
#
|
|
146
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
147
|
+
# def post_url(*args); end
|
|
148
|
+
#
|
|
149
|
+
# sig { params(args: T.untyped).returns(String) }
|
|
150
|
+
# def posts_url(*args); end
|
|
151
|
+
# end
|
|
152
|
+
# ~~~
|
|
153
|
+
#
|
|
154
|
+
# ~~~rbi
|
|
155
|
+
# # generated_mounted_helpers.rbi
|
|
156
|
+
# # typed: true
|
|
157
|
+
# module GeneratedMountedHelpers
|
|
158
|
+
# sig { returns(Blog::Engine::GeneratedRoutesProxy) }
|
|
159
|
+
# def articles; end
|
|
160
|
+
#
|
|
161
|
+
# sig { returns(GeneratedRoutesProxy) }
|
|
162
|
+
# def main_app; end
|
|
163
|
+
# end
|
|
164
|
+
# ~~~
|
|
165
|
+
#
|
|
166
|
+
# ~~~rbi
|
|
167
|
+
# # generated_routes_proxy.rbi
|
|
168
|
+
# # typed: true
|
|
169
|
+
# class GeneratedRoutesProxy < ::ActionDispatch::Routing::RoutesProxy
|
|
170
|
+
# include GeneratedPathHelpersModule
|
|
171
|
+
# include GeneratedUrlHelpersModule
|
|
172
|
+
# end
|
|
173
|
+
# ~~~
|
|
174
|
+
#
|
|
175
|
+
# ~~~rbi
|
|
176
|
+
# # blog/engine/generated_routes_proxy.rbi
|
|
177
|
+
# # typed: true
|
|
178
|
+
# class Blog::Engine::GeneratedRoutesProxy < ::ActionDispatch::Routing::RoutesProxy
|
|
179
|
+
# include Blog::Engine::GeneratedPathHelpersModule
|
|
180
|
+
# include Blog::Engine::GeneratedUrlHelpersModule
|
|
181
|
+
# end
|
|
182
|
+
# ~~~
|
|
183
|
+
#
|
|
81
184
|
#: [ConstantType = Module[top]]
|
|
82
185
|
class UrlHelpers < Compiler
|
|
83
186
|
# @override
|
|
@@ -87,14 +190,25 @@ module Tapioca
|
|
|
87
190
|
when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
|
|
88
191
|
generate_module_for(root, constant)
|
|
89
192
|
else
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
193
|
+
# `GeneratedMountedHelpers` is only defined when an engine is mounted (see `gather_constants`).
|
|
194
|
+
if defined?(::GeneratedMountedHelpers) && GeneratedMountedHelpers.singleton_class === constant
|
|
195
|
+
generate_mounted_helpers_module(root)
|
|
196
|
+
elsif engine_helper_module?(constant)
|
|
197
|
+
generate_module_for(root, constant)
|
|
198
|
+
else
|
|
199
|
+
generate_url_helper_includer
|
|
93
200
|
end
|
|
94
201
|
end
|
|
95
202
|
end
|
|
96
203
|
|
|
204
|
+
# Maps each engine's mount name to its class, e.g. `{ blog: Blog::Engine }`.
|
|
205
|
+
# Populated by `gather_constants` and read when generating the mounted helpers module.
|
|
206
|
+
@engine_mount_names = {} #: Hash[Symbol, singleton(::Rails::Engine)]
|
|
207
|
+
|
|
97
208
|
class << self
|
|
209
|
+
#: Hash[Symbol, singleton(::Rails::Engine)]
|
|
210
|
+
attr_reader :engine_mount_names
|
|
211
|
+
|
|
98
212
|
# @override
|
|
99
213
|
#: -> Enumerable[Module[top]]
|
|
100
214
|
def gather_constants
|
|
@@ -110,24 +224,55 @@ module Tapioca
|
|
|
110
224
|
Object.const_set(:GeneratedUrlHelpersModule, url_helpers_module)
|
|
111
225
|
Object.const_set(:GeneratedPathHelpersModule, path_helpers_module)
|
|
112
226
|
|
|
227
|
+
@engine_mount_names = mounted_engine_names
|
|
228
|
+
engine_helper_modules = register_engine_route_helpers
|
|
229
|
+
|
|
230
|
+
# Only synthesize the mounted helpers module when at least one mounted engine
|
|
231
|
+
# contributes its own route helpers. A mount of a routeless engine (or an app with
|
|
232
|
+
# no mounts) would leave nothing but `main_app`, which we don't generate on its own.
|
|
233
|
+
# This predicate mirrors `proxied_engines` in `generate_mounted_helpers_module`.
|
|
234
|
+
mounts_engine_with_helpers = @engine_mount_names.values.any? do |engine_class|
|
|
235
|
+
name_of(engine_class) && engine_class.const_defined?(:GeneratedPathHelpersModule, false)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if mounts_engine_with_helpers
|
|
239
|
+
Object.const_set(:GeneratedMountedHelpers, Module.new)
|
|
240
|
+
end
|
|
241
|
+
|
|
113
242
|
constants = all_modules.select do |mod|
|
|
114
243
|
next unless name_of(mod)
|
|
115
244
|
|
|
116
245
|
# Fast-path to quickly disqualify most cases
|
|
117
|
-
|
|
246
|
+
has_helpers = url_helpers_module > mod ||
|
|
118
247
|
path_helpers_module > mod ||
|
|
119
248
|
url_helpers_module > mod.singleton_class ||
|
|
120
249
|
path_helpers_module > mod.singleton_class
|
|
121
250
|
|
|
251
|
+
has_helpers ||= engine_helper_modules.any? do |engine_mod|
|
|
252
|
+
engine_mod > mod || engine_mod > mod.singleton_class
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
next false unless has_helpers
|
|
256
|
+
|
|
122
257
|
includes_helper?(mod, url_helpers_module) ||
|
|
123
258
|
includes_helper?(mod, path_helpers_module) ||
|
|
124
259
|
includes_helper?(mod.singleton_class, url_helpers_module) ||
|
|
125
|
-
includes_helper?(mod.singleton_class, path_helpers_module)
|
|
260
|
+
includes_helper?(mod.singleton_class, path_helpers_module) ||
|
|
261
|
+
engine_helper_modules.any? { |engine_mod| includes_helper?(mod, engine_mod) || includes_helper?(mod.singleton_class, engine_mod) }
|
|
126
262
|
end
|
|
127
263
|
|
|
128
|
-
constants
|
|
264
|
+
constants
|
|
265
|
+
.concat(NON_DISCOVERABLE_INCLUDERS)
|
|
266
|
+
.push(GeneratedUrlHelpersModule, GeneratedPathHelpersModule)
|
|
267
|
+
.concat(engine_helper_modules)
|
|
268
|
+
|
|
269
|
+
constants.push(GeneratedMountedHelpers) if defined?(GeneratedMountedHelpers)
|
|
270
|
+
|
|
271
|
+
constants
|
|
129
272
|
end
|
|
130
273
|
|
|
274
|
+
private
|
|
275
|
+
|
|
131
276
|
#: -> Array[Module[top]]
|
|
132
277
|
def gather_non_discoverable_includers
|
|
133
278
|
[].tap do |includers|
|
|
@@ -141,10 +286,59 @@ module Tapioca
|
|
|
141
286
|
end.freeze
|
|
142
287
|
end
|
|
143
288
|
|
|
289
|
+
# Maps each mounted engine's mount name to its class (e.g. `{ articles: Blog::Engine }`).
|
|
290
|
+
# Reads the same route table that `bin/rails routes` inspects: a mounted engine appears
|
|
291
|
+
# as a route whose endpoint is the engine (`app.engine?`), with the mount name (or `as:`
|
|
292
|
+
# alias) as the route name.
|
|
293
|
+
#: -> Hash[Symbol, singleton(::Rails::Engine)]
|
|
294
|
+
def mounted_engine_names
|
|
295
|
+
Rails.application.routes.routes.each_with_object({}) do |route, mapping|
|
|
296
|
+
app = route.app
|
|
297
|
+
next unless app.respond_to?(:engine?) && app.engine?
|
|
298
|
+
|
|
299
|
+
name = route.name
|
|
300
|
+
next unless name
|
|
301
|
+
|
|
302
|
+
mapping[name.to_sym] = app.rack_app
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Registers engine-scoped `GeneratedPathHelpersModule`/`GeneratedUrlHelpersModule`
|
|
307
|
+
# constants on each engine with routes, and returns those helper modules.
|
|
308
|
+
#: -> Array[Module[top]]
|
|
309
|
+
def register_engine_route_helpers
|
|
310
|
+
engine_helper_modules = [] #: Array[Module[top]]
|
|
311
|
+
|
|
312
|
+
Rails.application.railties.grep(::Rails::Engine).each do |engine_instance|
|
|
313
|
+
engine_class = engine_instance.class
|
|
314
|
+
next if engine_class == Rails.application.class
|
|
315
|
+
|
|
316
|
+
engine_path_helpers = engine_instance.routes.named_routes.path_helpers_module
|
|
317
|
+
engine_url_helpers = engine_instance.routes.named_routes.url_helpers_module
|
|
318
|
+
|
|
319
|
+
# Skip engines with no routes
|
|
320
|
+
next if engine_path_helpers.instance_methods(false).empty? &&
|
|
321
|
+
engine_url_helpers.instance_methods(false).empty?
|
|
322
|
+
|
|
323
|
+
unless engine_class.const_defined?(:GeneratedPathHelpersModule, false)
|
|
324
|
+
engine_class.const_set(:GeneratedPathHelpersModule, engine_path_helpers)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
unless engine_class.const_defined?(:GeneratedUrlHelpersModule, false)
|
|
328
|
+
engine_class.const_set(:GeneratedUrlHelpersModule, engine_url_helpers)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
engine_helper_modules << engine_class.const_get(:GeneratedPathHelpersModule)
|
|
332
|
+
engine_helper_modules << engine_class.const_get(:GeneratedUrlHelpersModule)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
engine_helper_modules
|
|
336
|
+
end
|
|
337
|
+
|
|
144
338
|
# Returns `true` if `mod` "directly" includes `helper`.
|
|
145
339
|
# For classes, this method will return false if the `helper` is included only by a superclass
|
|
146
340
|
#: (Module[top] mod, Module[top] helper) -> bool
|
|
147
|
-
|
|
341
|
+
def includes_helper?(mod, helper)
|
|
148
342
|
ancestors = ancestors_of(mod)
|
|
149
343
|
|
|
150
344
|
own_ancestors = if Class === mod && (superclass = superclass_of(mod))
|
|
@@ -178,6 +372,116 @@ module Tapioca
|
|
|
178
372
|
end
|
|
179
373
|
end
|
|
180
374
|
|
|
375
|
+
#: (RBI::Tree root) -> void
|
|
376
|
+
def generate_mounted_helpers_module(root)
|
|
377
|
+
# Mount name => engine name, for mounted engines that actually expose helper modules.
|
|
378
|
+
# Routeless engines (no `GeneratedPathHelpersModule`) and anonymous engines get no proxy.
|
|
379
|
+
# (If *every* mounted engine is routeless, `gather_constants` doesn't generate this module.)
|
|
380
|
+
proxied_engines = self.class.engine_mount_names.filter_map do |mount_name, engine_class|
|
|
381
|
+
engine_name = name_of(engine_class)
|
|
382
|
+
next unless engine_name
|
|
383
|
+
next unless engine_class.const_defined?(:GeneratedPathHelpersModule, false)
|
|
384
|
+
|
|
385
|
+
[mount_name, engine_name]
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
root.create_module("GeneratedMountedHelpers") do |mod|
|
|
389
|
+
mod.create_method(
|
|
390
|
+
"main_app",
|
|
391
|
+
return_type: "GeneratedRoutesProxy",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
proxied_engines.each do |mount_name, engine_name|
|
|
395
|
+
mod.create_method(
|
|
396
|
+
mount_name.to_s,
|
|
397
|
+
return_type: "#{engine_name}::GeneratedRoutesProxy",
|
|
398
|
+
)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# The application's own RoutesProxy subclass, returned by `main_app`. Mirrors the
|
|
403
|
+
# engine proxies below so that `main_app.post_path` & co. type-check.
|
|
404
|
+
root.create_class("GeneratedRoutesProxy", superclass_name: "::ActionDispatch::Routing::RoutesProxy") do |klass|
|
|
405
|
+
klass.create_include("GeneratedPathHelpersModule")
|
|
406
|
+
klass.create_include("GeneratedUrlHelpersModule")
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# One RoutesProxy subclass per engine, `uniq` since an engine can be mounted under
|
|
410
|
+
# several names but needs only a single proxy class.
|
|
411
|
+
proxied_engines.map { |_mount_name, engine_name| engine_name }.uniq.each do |engine_name|
|
|
412
|
+
proxy_class_name = "#{engine_name}::GeneratedRoutesProxy"
|
|
413
|
+
path_helpers_name = "#{engine_name}::GeneratedPathHelpersModule"
|
|
414
|
+
url_helpers_name = "#{engine_name}::GeneratedUrlHelpersModule"
|
|
415
|
+
|
|
416
|
+
root.create_class(proxy_class_name, superclass_name: "::ActionDispatch::Routing::RoutesProxy") do |klass|
|
|
417
|
+
klass.create_include(path_helpers_name)
|
|
418
|
+
klass.create_include(url_helpers_name)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
#: (Module[top] mod) -> bool
|
|
424
|
+
def engine_helper_module?(mod)
|
|
425
|
+
Rails.application.railties.grep(::Rails::Engine).any? do |engine_instance|
|
|
426
|
+
engine_class = engine_instance.class
|
|
427
|
+
next false if engine_class == Rails.application.class
|
|
428
|
+
next false unless engine_class.const_defined?(:GeneratedPathHelpersModule, false)
|
|
429
|
+
|
|
430
|
+
mod == engine_class.const_get(:GeneratedPathHelpersModule) ||
|
|
431
|
+
mod == engine_class.const_get(:GeneratedUrlHelpersModule)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
#: -> void
|
|
436
|
+
def generate_url_helper_includer
|
|
437
|
+
root.create_path(constant) do |mod|
|
|
438
|
+
create_mixins_for(mod, GeneratedUrlHelpersModule)
|
|
439
|
+
create_mixins_for(mod, GeneratedPathHelpersModule)
|
|
440
|
+
|
|
441
|
+
# GeneratedMountedHelpers is only synthesized when an engine is mounted (see
|
|
442
|
+
# `gather_constants`). It is a fresh `Module.new` used purely for naming, so we
|
|
443
|
+
# check against the real `mounted_helpers` module for ancestor detection. Only
|
|
444
|
+
# controllers/framework classes actually have `mounted_helpers` in their ancestor
|
|
445
|
+
# chain; plain url_helpers includers do not.
|
|
446
|
+
if defined?(::GeneratedMountedHelpers)
|
|
447
|
+
mounted_helpers = Rails.application.routes.mounted_helpers
|
|
448
|
+
include_mounted = constant.ancestors.include?(mounted_helpers) ||
|
|
449
|
+
NON_DISCOVERABLE_INCLUDERS.include?(constant)
|
|
450
|
+
extend_mounted = constant.singleton_class.ancestors.include?(mounted_helpers)
|
|
451
|
+
|
|
452
|
+
mod.create_include("GeneratedMountedHelpers") if include_mounted
|
|
453
|
+
mod.create_extend("GeneratedMountedHelpers") if extend_mounted
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
create_engine_helper_mixins(mod)
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
#: (RBI::Scope mod) -> void
|
|
461
|
+
def create_engine_helper_mixins(mod)
|
|
462
|
+
Rails.application.railties.grep(::Rails::Engine).each do |engine_instance|
|
|
463
|
+
engine_class = engine_instance.class
|
|
464
|
+
next if engine_class == Rails.application.class
|
|
465
|
+
next unless engine_class.const_defined?(:GeneratedPathHelpersModule, false)
|
|
466
|
+
|
|
467
|
+
create_engine_helper_mixin(mod, engine_class.const_get(:GeneratedUrlHelpersModule))
|
|
468
|
+
create_engine_helper_mixin(mod, engine_class.const_get(:GeneratedPathHelpersModule))
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
#: (RBI::Scope mod, Module[top] helper_module) -> void
|
|
473
|
+
def create_engine_helper_mixin(mod, helper_module)
|
|
474
|
+
# Engine helpers must be added only when actually present; the
|
|
475
|
+
# NON_DISCOVERABLE_INCLUDERS fallback is only valid for main app helpers.
|
|
476
|
+
if constant.ancestors.include?(helper_module)
|
|
477
|
+
mod.create_include(T.must(helper_module.name))
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
if constant.singleton_class.ancestors.include?(helper_module)
|
|
481
|
+
mod.create_extend(T.must(helper_module.name))
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
181
485
|
#: (RBI::Scope mod, Module[top] helper_module) -> void
|
|
182
486
|
def create_mixins_for(mod, helper_module)
|
|
183
487
|
include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
|