tapioca 0.19.2 → 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 +87 -45
- data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
- data/lib/tapioca/cli.rb +10 -11
- data/lib/tapioca/commands/abstract_dsl.rb +71 -20
- data/lib/tapioca/commands/abstract_gem.rb +1 -0
- data/lib/tapioca/commands/annotations.rb +1 -1
- data/lib/tapioca/commands/dsl_generate.rb +0 -15
- 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_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 +7 -2
- data/lib/tapioca/dsl/pipeline.rb +3 -0
- 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 +1 -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 +32 -21
- data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
- data/lib/tapioca/runtime/generic_type_registry.rb +27 -2
- data/lib/tapioca/runtime/reflection.rb +18 -1
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +11 -10
- metadata +6 -4
|
@@ -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)
|
|
@@ -91,15 +91,20 @@ module Tapioca
|
|
|
91
91
|
"T.untyped"
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
prepared = false
|
|
94
95
|
if prepare_method
|
|
95
96
|
prepare_signature = Runtime::Reflection.signature_of(prepare_method)
|
|
96
97
|
prepare_return_type = prepare_signature&.return_type
|
|
97
98
|
if valid_return_type?(prepare_return_type)
|
|
98
99
|
parsed_type = prepare_return_type&.to_s
|
|
100
|
+
prepared = true
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
|
|
102
|
-
|
|
104
|
+
# A `prepare` method receives (and returns) the argument's fully coerced value, list
|
|
105
|
+
# wrapper included, so its return type already accounts for `type.list?` and must not be
|
|
106
|
+
# wrapped again.
|
|
107
|
+
if type.list? && !prepared
|
|
103
108
|
parsed_type = "T::Array[#{parsed_type}]"
|
|
104
109
|
end
|
|
105
110
|
|
|
@@ -114,7 +119,7 @@ module Tapioca
|
|
|
114
119
|
|
|
115
120
|
#: (Module[top] constant) -> String
|
|
116
121
|
def type_for_constant(constant)
|
|
117
|
-
if constant.
|
|
122
|
+
if constant.method_defined?(:prepare)
|
|
118
123
|
prepare_method = constant.instance_method(:prepare)
|
|
119
124
|
|
|
120
125
|
prepare_signature = Runtime::Reflection.signature_of(prepare_method)
|
data/lib/tapioca/dsl/pipeline.rb
CHANGED
|
@@ -64,6 +64,9 @@ module Tapioca
|
|
|
64
64
|
|
|
65
65
|
# It's OK if there are no constants to process if we received a valid file/path.
|
|
66
66
|
if constants_to_process.empty? && requested_paths.none? { |p| File.exist?(p) }
|
|
67
|
+
# When running within the add-on, return early so this expected case is not logged as an error
|
|
68
|
+
return [] if @lsp_addon
|
|
69
|
+
|
|
67
70
|
report_error(<<~ERROR)
|
|
68
71
|
No classes/modules can be matched for RBI generation.
|
|
69
72
|
Please check that the requested classes/modules include processable DSL methods.
|
data/lib/tapioca/gem/events.rb
CHANGED
|
@@ -109,7 +109,7 @@ module Tapioca
|
|
|
109
109
|
#| untyped signature,
|
|
110
110
|
#| Array[[Symbol, String]] parameters
|
|
111
111
|
#| ) -> void
|
|
112
|
-
def initialize(symbol, constant, method, node, signature, parameters)
|
|
112
|
+
def initialize(symbol, constant, method, node, signature, parameters)
|
|
113
113
|
super(symbol, constant)
|
|
114
114
|
@node = node
|
|
115
115
|
@method = method
|
|
@@ -15,7 +15,7 @@ module Tapioca
|
|
|
15
15
|
"shareable_constant_value:",
|
|
16
16
|
"rubocop:",
|
|
17
17
|
"@requires_ancestor:",
|
|
18
|
-
] #: Array[String]
|
|
18
|
+
].freeze #: Array[String]
|
|
19
19
|
|
|
20
20
|
#: (Pipeline pipeline, Rubydex::Graph gem_graph) -> void
|
|
21
21
|
def initialize(pipeline, gem_graph)
|
|
@@ -69,13 +69,11 @@ module Tapioca
|
|
|
69
69
|
end
|
|
70
70
|
return [] unless declaration
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.map { |comment| comment.string.gsub(/^#+ ?/, "") }
|
|
78
|
-
.reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) }
|
|
72
|
+
# Definitions often share a comment block, such as a license header, so de-duplicate them
|
|
73
|
+
lines = declaration.definitions
|
|
74
|
+
.map { |definition| comment_lines(definition) }
|
|
75
|
+
.uniq
|
|
76
|
+
.flatten
|
|
79
77
|
|
|
80
78
|
# Strip leading and trailing blank lines, matching YARD's behavior
|
|
81
79
|
lines = lines.reverse_each.drop_while(&:empty?).reverse_each.drop_while(&:empty?)
|
|
@@ -83,6 +81,13 @@ module Tapioca
|
|
|
83
81
|
lines.map! { |line| RBI::Comment.new(line) }
|
|
84
82
|
end
|
|
85
83
|
|
|
84
|
+
#: (Rubydex::Definition definition) -> Array[String]
|
|
85
|
+
def comment_lines(definition)
|
|
86
|
+
lines = definition.comments.map { |comment| comment.string.gsub(/^#+ ?/, "") }
|
|
87
|
+
lines.reject! { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) }
|
|
88
|
+
lines
|
|
89
|
+
end
|
|
90
|
+
|
|
86
91
|
# @override
|
|
87
92
|
#: (NodeAdded event) -> bool
|
|
88
93
|
def ignore?(event)
|