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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -45
  3. data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
  4. data/lib/tapioca/cli.rb +10 -11
  5. data/lib/tapioca/commands/abstract_dsl.rb +71 -20
  6. data/lib/tapioca/commands/abstract_gem.rb +1 -0
  7. data/lib/tapioca/commands/annotations.rb +1 -1
  8. data/lib/tapioca/commands/dsl_generate.rb +0 -15
  9. data/lib/tapioca/commands/gem_generate.rb +20 -4
  10. data/lib/tapioca/dsl/compiler.rb +24 -10
  11. data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
  12. data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  14. data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +17 -11
  15. data/lib/tapioca/dsl/compilers/active_record_relations.rb +2 -2
  16. data/lib/tapioca/dsl/compilers/config.rb +1 -1
  17. data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
  18. data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
  19. data/lib/tapioca/dsl/compilers/url_helpers.rb +312 -8
  20. data/lib/tapioca/dsl/compilers.rb +1 -1
  21. data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +7 -2
  22. data/lib/tapioca/dsl/pipeline.rb +3 -0
  23. data/lib/tapioca/gem/events.rb +1 -1
  24. data/lib/tapioca/gem/listeners/documentation.rb +13 -8
  25. data/lib/tapioca/gem/listeners/methods.rb +79 -13
  26. data/lib/tapioca/gem/listeners/sorbet_enums.rb +1 -1
  27. data/lib/tapioca/gem/pipeline.rb +3 -3
  28. data/lib/tapioca/gemfile.rb +13 -0
  29. data/lib/tapioca/helpers/env_helper.rb +1 -1
  30. data/lib/tapioca/helpers/file_helper.rb +42 -0
  31. data/lib/tapioca/helpers/rbi_files_helper.rb +96 -5
  32. data/lib/tapioca/helpers/rbi_helper.rb +17 -3
  33. data/lib/tapioca/helpers/sorbet_helper.rb +1 -1
  34. data/lib/tapioca/internal.rb +1 -0
  35. data/lib/tapioca/loaders/gem.rb +33 -0
  36. data/lib/tapioca/loaders/loader.rb +0 -13
  37. data/lib/tapioca/rbi_ext/model.rb +16 -2
  38. data/lib/tapioca/rbs/bootsnap_cache.rb +52 -0
  39. data/lib/tapioca/rbs/rewriter.rb +32 -21
  40. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
  41. data/lib/tapioca/runtime/generic_type_registry.rb +27 -2
  42. data/lib/tapioca/runtime/reflection.rb +18 -1
  43. data/lib/tapioca/version.rb +1 -1
  44. data/lib/tapioca.rb +11 -10
  45. metadata +6 -4
@@ -26,13 +26,21 @@ module Tapioca
26
26
  # # test_case.rbi
27
27
  # # typed: true
28
28
  # class ActiveSupport::TestCase
29
- # sig { params(fixture_name: NilClass, other_fixtures: NilClass).returns(T::Array[Post]) }
30
- # sig { params(fixture_name: T.any(String, Symbol), other_fixtures: NilClass).returns(Post) }
31
- # sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol))
32
- # .returns(T::Array[Post]) }
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
 
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- return unless defined?(Config) && defined?(Config::VERSION) && defined?(Config.const_name)
4
+ return unless defined?(Config::VERSION) && defined?(Config.const_name)
5
5
 
6
6
  module Tapioca
7
7
  module Dsl
@@ -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 # rubocop:disable Style/ZeroLengthPredicate
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.instance_methods.include?(:perform)
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/v5.1.7/classes/ActionDispatch/Routing/UrlFor.html#module-ActionDispatch::Routing::UrlFor-label-URL+generation+for+named+routes).
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
- root.create_path(constant) do |mod|
91
- create_mixins_for(mod, GeneratedUrlHelpersModule)
92
- create_mixins_for(mod, GeneratedPathHelpersModule)
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
- next false unless url_helpers_module > mod || # rubocop:disable Style/InvertibleUnlessCondition
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.concat(NON_DISCOVERABLE_INCLUDERS).push(GeneratedUrlHelpersModule, GeneratedPathHelpersModule)
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
- private def includes_helper?(mod, helper)
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)
@@ -13,7 +13,7 @@ module Tapioca
13
13
  NAMESPACES = [
14
14
  "#{name}::", # compilers in this namespace
15
15
  "::", # compilers that need to be fully namespaced
16
- ] #: Array[String]
16
+ ].freeze #: Array[String]
17
17
  end
18
18
  end
19
19
  end
@@ -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
- if type.list?
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.instance_methods.include?(:prepare)
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)
@@ -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.
@@ -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) # rubocop:disable Metrics/ParameterLists
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
- comments = declaration.definitions.flat_map(&:comments)
73
- comments.uniq!
74
- return [] if comments.empty?
75
-
76
- lines = comments
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)