@@ -86,10 +86,13 @@ def decorate
8686 case constant
8787 when GeneratedPathHelpersModule . singleton_class , GeneratedUrlHelpersModule . singleton_class
8888 generate_module_for ( root , constant )
89+ when GeneratedMountedHelpers . singleton_class
90+ generate_mounted_helpers_module ( root )
8991 else
90- root . create_path ( constant ) do |mod |
91- create_mixins_for ( mod , GeneratedUrlHelpersModule )
92- create_mixins_for ( mod , GeneratedPathHelpersModule )
92+ if engine_helper_module? ( constant )
93+ generate_module_for ( root , constant )
94+ else
95+ generate_url_helper_includer
9396 end
9497 end
9598 end
@@ -106,28 +109,63 @@ def gather_constants
106109
107110 url_helpers_module = Rails . application . routes . named_routes . url_helpers_module
108111 path_helpers_module = Rails . application . routes . named_routes . path_helpers_module
112+ mounted_helpers_module = Rails . application . routes . mounted_helpers
109113
110- Object . const_set ( :GeneratedUrlHelpersModule , url_helpers_module )
111- Object . const_set ( :GeneratedPathHelpersModule , path_helpers_module )
114+ unless Object . const_defined? ( :GeneratedUrlHelpersModule )
115+ Object . const_set ( :GeneratedUrlHelpersModule , url_helpers_module )
116+ end
117+
118+ unless Object . const_defined? ( :GeneratedPathHelpersModule )
119+ Object . const_set ( :GeneratedPathHelpersModule , path_helpers_module )
120+ end
121+
122+ unless Object . const_defined? ( :GeneratedMountedHelpers )
123+ Object . const_set ( :GeneratedMountedHelpers , Module . new )
124+ end
125+
126+ # Build engine registry: { mount_name => engine_class }
127+ @engine_mount_names = T . let (
128+ { } ,
129+ T . nilable ( T ::Hash [ Symbol , T . class_of ( ::Rails ::Engine ) ] ) ,
130+ )
131+ engine_helper_modules = setup_engine_helper_modules ( mounted_helpers_module )
112132
113133 constants = all_modules . select do |mod |
114134 next unless name_of ( mod )
115135
116136 # Fast-path to quickly disqualify most cases
117- next false unless url_helpers_module > mod || # rubocop:disable Style/InvertibleUnlessCondition
137+ has_main_app_helpers = url_helpers_module > mod ||
118138 path_helpers_module > mod ||
119139 url_helpers_module > mod . singleton_class ||
120140 path_helpers_module > mod . singleton_class
121141
142+ has_engine_helpers = engine_helper_modules . any? do |engine_mod |
143+ engine_mod > mod || engine_mod > mod . singleton_class
144+ end
145+
146+ next false unless has_main_app_helpers || has_engine_helpers
147+
122148 includes_helper? ( mod , url_helpers_module ) ||
123149 includes_helper? ( mod , path_helpers_module ) ||
124150 includes_helper? ( mod . singleton_class , url_helpers_module ) ||
125- includes_helper? ( mod . singleton_class , path_helpers_module )
151+ includes_helper? ( mod . singleton_class , path_helpers_module ) ||
152+ engine_helper_modules . any? { |engine_mod | includes_helper? ( mod , engine_mod ) || includes_helper? ( mod . singleton_class , engine_mod ) }
126153 end
127154
128- constants . concat ( NON_DISCOVERABLE_INCLUDERS ) . push ( GeneratedUrlHelpersModule , GeneratedPathHelpersModule )
155+ constants
156+ . concat ( NON_DISCOVERABLE_INCLUDERS )
157+ . push ( GeneratedUrlHelpersModule , GeneratedPathHelpersModule )
158+ . push ( GeneratedMountedHelpers )
159+ . concat ( engine_helper_modules )
160+ end
161+
162+ #: -> Hash[Symbol, singleton(::Rails::Engine)]
163+ def engine_mount_names
164+ @engine_mount_names || { }
129165 end
130166
167+ private
168+
131169 #: -> Array[Module[top]]
132170 def gather_non_discoverable_includers
133171 [ ] . tap do |includers |
@@ -141,10 +179,79 @@ def gather_non_discoverable_includers
141179 end . freeze
142180 end
143181
182+ #: (Module[top] mounted_helpers_module) -> Array[Module[top]]
183+ def setup_engine_helper_modules ( mounted_helpers_module )
184+ routes_to_engine = { }
185+ engine_helper_modules = [ ] #: Array[Module[top]]
186+
187+ Rails . application . railties . grep ( ::Rails ::Engine ) . each do |engine_instance |
188+ engine_class = engine_instance . class
189+ next if engine_class == Rails . application . class
190+
191+ routes_to_engine [ engine_instance . routes ] = engine_class
192+
193+ engine_path_helpers = engine_instance . routes . named_routes . path_helpers_module
194+ engine_url_helpers = engine_instance . routes . named_routes . url_helpers_module
195+
196+ # Skip engines with no routes
197+ next if engine_path_helpers . instance_methods ( false ) . empty? &&
198+ engine_url_helpers . instance_methods ( false ) . empty?
199+
200+ unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
201+ engine_class . const_set ( :GeneratedPathHelpersModule , engine_path_helpers )
202+ end
203+
204+ unless engine_class . const_defined? ( :GeneratedUrlHelpersModule , false )
205+ engine_class . const_set ( :GeneratedUrlHelpersModule , engine_url_helpers )
206+ end
207+
208+ engine_helper_modules << engine_class . const_get ( :GeneratedPathHelpersModule )
209+ engine_helper_modules << engine_class . const_get ( :GeneratedUrlHelpersModule )
210+ end
211+
212+ register_engine_mount_names ( mounted_helpers_module , routes_to_engine )
213+
214+ engine_helper_modules
215+ end
216+
217+ # Map mount names to engine classes by inspecting mounted_helpers methods.
218+ # Rails' mounted_helpers methods call `_routes_context` on `self`, so we
219+ # create a minimal context object that satisfies that interface. This lets
220+ # us instantiate the RoutesProxy and read its @routes to match back to an
221+ # engine's RouteSet.
222+ #: (Module[top] mounted_helpers_module, Hash[untyped, untyped] routes_to_engine) -> void
223+ def register_engine_mount_names ( mounted_helpers_module , routes_to_engine )
224+ context = Object . new
225+ context . define_singleton_method ( :_routes_context ) { self }
226+
227+ # Rails defines both public (blog) and private (_blog) mounted helpers.
228+ # The public method delegates to the private one, which creates the
229+ # RoutesProxy. We call the private method on our dummy context (since
230+ # the public one would fail), but record the public name for RBI output.
231+ mounted_helpers_module . instance_methods ( false ) . each do |method_name |
232+ next if method_name == :main_app
233+ # Only process the public methods (non-underscore-prefixed)
234+ next if method_name . start_with? ( "_" )
235+
236+ private_name = :"_#{ method_name } "
237+ next unless mounted_helpers_module . instance_methods ( false ) . include? ( private_name )
238+
239+ begin
240+ proxy = mounted_helpers_module . instance_method ( private_name ) . bind_call ( context )
241+ engine_routes = proxy . instance_variable_get ( :@routes )
242+ engine_class = routes_to_engine [ engine_routes ]
243+ T . must ( @engine_mount_names ) [ method_name ] = engine_class if engine_class
244+ rescue
245+ # If we can't resolve the mapping for this mount name, skip it
246+ next
247+ end
248+ end
249+ end
250+
144251 # Returns `true` if `mod` "directly" includes `helper`.
145252 # For classes, this method will return false if the `helper` is included only by a superclass
146253 #: (Module[top] mod, Module[top] helper) -> bool
147- private def includes_helper? ( mod , helper )
254+ def includes_helper? ( mod , helper )
148255 ancestors = ancestors_of ( mod )
149256
150257 own_ancestors = if Class === mod && ( superclass = superclass_of ( mod ) )
@@ -178,6 +285,107 @@ def generate_module_for(root, constant)
178285 end
179286 end
180287
288+ #: (RBI::Tree root) -> void
289+ def generate_mounted_helpers_module ( root )
290+ engine_mount_names = self . class . engine_mount_names
291+
292+ root . create_module ( "GeneratedMountedHelpers" ) do |mod |
293+ # main_app always returns a plain RoutesProxy
294+ mod . create_method (
295+ "main_app" ,
296+ return_type : "ActionDispatch::Routing::RoutesProxy" ,
297+ )
298+
299+ # One proxy method per mounted engine (only those with routes)
300+ engine_mount_names . each do |mount_name , engine_class |
301+ engine_name = name_of ( engine_class )
302+ next unless engine_name
303+ next unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
304+
305+ proxy_class_name = "#{ engine_name } ::GeneratedRoutesProxy"
306+
307+ mod . create_method (
308+ mount_name . to_s ,
309+ return_type : proxy_class_name ,
310+ )
311+ end
312+ end
313+
314+ # Generate GeneratedRoutesProxy class for each engine (only those with routes)
315+ engine_mount_names . each_value do |engine_class |
316+ engine_name = name_of ( engine_class )
317+ next unless engine_name
318+ next unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
319+
320+ proxy_class_name = "#{ engine_name } ::GeneratedRoutesProxy"
321+ path_helpers_name = "#{ engine_name } ::GeneratedPathHelpersModule"
322+ url_helpers_name = "#{ engine_name } ::GeneratedUrlHelpersModule"
323+
324+ root . create_class ( proxy_class_name , superclass_name : "::ActionDispatch::Routing::RoutesProxy" ) do |klass |
325+ klass . create_include ( path_helpers_name )
326+ klass . create_include ( url_helpers_name )
327+ end
328+ end
329+ end
330+
331+ #: (Module[top] mod) -> bool
332+ def engine_helper_module? ( mod )
333+ Rails . application . railties . grep ( ::Rails ::Engine ) . any? do |engine_instance |
334+ engine_class = engine_instance . class
335+ next false if engine_class == Rails . application . class
336+ next false unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
337+
338+ mod == engine_class . const_get ( :GeneratedPathHelpersModule ) ||
339+ mod == engine_class . const_get ( :GeneratedUrlHelpersModule )
340+ end
341+ end
342+
343+ #: -> void
344+ def generate_url_helper_includer
345+ root . create_path ( constant ) do |mod |
346+ create_mixins_for ( mod , GeneratedUrlHelpersModule )
347+ create_mixins_for ( mod , GeneratedPathHelpersModule )
348+
349+ # GeneratedMountedHelpers is Module.new (for naming), so check
350+ # against the real mounted_helpers module for ancestor detection.
351+ # Only controllers/framework classes actually have mounted_helpers
352+ # in their ancestor chain; plain url_helpers includers do not.
353+ mounted_helpers = Rails . application . routes . mounted_helpers
354+ include_mounted = constant . ancestors . include? ( mounted_helpers ) ||
355+ NON_DISCOVERABLE_INCLUDERS . include? ( constant )
356+ extend_mounted = constant . singleton_class . ancestors . include? ( mounted_helpers )
357+
358+ mod . create_include ( "GeneratedMountedHelpers" ) if include_mounted
359+ mod . create_extend ( "GeneratedMountedHelpers" ) if extend_mounted
360+
361+ # Add engine-specific helper module mixins.
362+ # Do NOT use create_mixins_for for engine helpers — its
363+ # NON_DISCOVERABLE_INCLUDERS fallback would incorrectly add
364+ # engine-specific modules to IntegrationTest and ActionView::Helpers.
365+ Rails . application . railties . grep ( ::Rails ::Engine ) . each do |engine_instance |
366+ engine_class = engine_instance . class
367+ next if engine_class == Rails . application . class
368+ next unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
369+
370+ engine_path_mod = engine_class . const_get ( :GeneratedPathHelpersModule )
371+ engine_url_mod = engine_class . const_get ( :GeneratedUrlHelpersModule )
372+
373+ if constant . ancestors . include? ( engine_url_mod )
374+ mod . create_include ( engine_url_mod . name )
375+ end
376+ if constant . singleton_class . ancestors . include? ( engine_url_mod )
377+ mod . create_extend ( engine_url_mod . name )
378+ end
379+ if constant . ancestors . include? ( engine_path_mod )
380+ mod . create_include ( engine_path_mod . name )
381+ end
382+ if constant . singleton_class . ancestors . include? ( engine_path_mod )
383+ mod . create_extend ( engine_path_mod . name )
384+ end
385+ end
386+ end
387+ end
388+
181389 #: (RBI::Scope mod, Module[top] helper_module) -> void
182390 def create_mixins_for ( mod , helper_module )
183391 include_helper = constant . ancestors . include? ( helper_module ) || NON_DISCOVERABLE_INCLUDERS . include? ( constant )
0 commit comments