@@ -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,55 @@ 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
110114 Object . const_set ( :GeneratedUrlHelpersModule , url_helpers_module )
111115 Object . const_set ( :GeneratedPathHelpersModule , path_helpers_module )
116+ Object . const_set ( :GeneratedMountedHelpers , Module . new )
117+
118+ # Build engine registry: { mount_name => engine_class }
119+ @engine_mount_names = T . let (
120+ { } ,
121+ T . nilable ( T ::Hash [ Symbol , T . class_of ( ::Rails ::Engine ) ] ) ,
122+ )
123+ engine_helper_modules = setup_engine_helper_modules ( mounted_helpers_module )
112124
113125 constants = all_modules . select do |mod |
114126 next unless name_of ( mod )
115127
116128 # Fast-path to quickly disqualify most cases
117- next false unless url_helpers_module > mod || # rubocop:disable Style/InvertibleUnlessCondition
129+ has_helpers = url_helpers_module > mod ||
118130 path_helpers_module > mod ||
119131 url_helpers_module > mod . singleton_class ||
120132 path_helpers_module > mod . singleton_class
121133
134+ has_helpers ||= engine_helper_modules . any? do |engine_mod |
135+ engine_mod > mod || engine_mod > mod . singleton_class
136+ end
137+
138+ next false unless has_helpers
139+
122140 includes_helper? ( mod , url_helpers_module ) ||
123141 includes_helper? ( mod , path_helpers_module ) ||
124142 includes_helper? ( mod . singleton_class , url_helpers_module ) ||
125- includes_helper? ( mod . singleton_class , path_helpers_module )
143+ includes_helper? ( mod . singleton_class , path_helpers_module ) ||
144+ engine_helper_modules . any? { |engine_mod | includes_helper? ( mod , engine_mod ) || includes_helper? ( mod . singleton_class , engine_mod ) }
126145 end
127146
128- constants . concat ( NON_DISCOVERABLE_INCLUDERS ) . push ( GeneratedUrlHelpersModule , GeneratedPathHelpersModule )
147+ constants
148+ . concat ( NON_DISCOVERABLE_INCLUDERS )
149+ . push ( GeneratedUrlHelpersModule , GeneratedPathHelpersModule )
150+ . push ( GeneratedMountedHelpers )
151+ . concat ( engine_helper_modules )
152+ end
153+
154+ #: -> Hash[Symbol, singleton(::Rails::Engine)]
155+ def engine_mount_names
156+ @engine_mount_names || { }
129157 end
130158
159+ private
160+
131161 #: -> Array[Module[top]]
132162 def gather_non_discoverable_includers
133163 [ ] . tap do |includers |
@@ -141,10 +171,79 @@ def gather_non_discoverable_includers
141171 end . freeze
142172 end
143173
174+ #: (Module[top] mounted_helpers_module) -> Array[Module[top]]
175+ def setup_engine_helper_modules ( mounted_helpers_module )
176+ routes_to_engine = { }
177+ engine_helper_modules = [ ] #: Array[Module[top]]
178+
179+ Rails . application . railties . grep ( ::Rails ::Engine ) . each do |engine_instance |
180+ engine_class = engine_instance . class
181+ next if engine_class == Rails . application . class
182+
183+ routes_to_engine [ engine_instance . routes ] = engine_class
184+
185+ engine_path_helpers = engine_instance . routes . named_routes . path_helpers_module
186+ engine_url_helpers = engine_instance . routes . named_routes . url_helpers_module
187+
188+ # Skip engines with no routes
189+ next if engine_path_helpers . instance_methods ( false ) . empty? &&
190+ engine_url_helpers . instance_methods ( false ) . empty?
191+
192+ unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
193+ engine_class . const_set ( :GeneratedPathHelpersModule , engine_path_helpers )
194+ end
195+
196+ unless engine_class . const_defined? ( :GeneratedUrlHelpersModule , false )
197+ engine_class . const_set ( :GeneratedUrlHelpersModule , engine_url_helpers )
198+ end
199+
200+ engine_helper_modules << engine_class . const_get ( :GeneratedPathHelpersModule )
201+ engine_helper_modules << engine_class . const_get ( :GeneratedUrlHelpersModule )
202+ end
203+
204+ register_engine_mount_names ( mounted_helpers_module , routes_to_engine )
205+
206+ engine_helper_modules
207+ end
208+
209+ # Map mount names to engine classes by inspecting mounted_helpers methods.
210+ # Rails' mounted_helpers methods call `_routes_context` on `self`, so we
211+ # create a minimal context object that satisfies that interface. This lets
212+ # us instantiate the RoutesProxy and read its @routes to match back to an
213+ # engine's RouteSet.
214+ #: (Module[top] mounted_helpers_module, Hash[untyped, untyped] routes_to_engine) -> void
215+ def register_engine_mount_names ( mounted_helpers_module , routes_to_engine )
216+ context = Object . new
217+ context . define_singleton_method ( :_routes_context ) { self }
218+
219+ # Rails defines both public (blog) and private (_blog) mounted helpers.
220+ # The public method delegates to the private one, which creates the
221+ # RoutesProxy. We call the private method on our dummy context (since
222+ # the public one would fail), but record the public name for RBI output.
223+ mounted_helpers_module . instance_methods ( false ) . each do |method_name |
224+ next if method_name == :main_app
225+ # Only process the public methods (non-underscore-prefixed)
226+ next if method_name . start_with? ( "_" )
227+
228+ private_name = :"_#{ method_name } "
229+ next unless mounted_helpers_module . instance_methods ( false ) . include? ( private_name )
230+
231+ begin
232+ proxy = mounted_helpers_module . instance_method ( private_name ) . bind_call ( context )
233+ engine_routes = proxy . instance_variable_get ( :@routes )
234+ engine_class = routes_to_engine [ engine_routes ]
235+ T . must ( @engine_mount_names ) [ method_name ] = engine_class if engine_class
236+ rescue
237+ # If we can't resolve the mapping for this mount name, skip it
238+ next
239+ end
240+ end
241+ end
242+
144243 # Returns `true` if `mod` "directly" includes `helper`.
145244 # For classes, this method will return false if the `helper` is included only by a superclass
146245 #: (Module[top] mod, Module[top] helper) -> bool
147- private def includes_helper? ( mod , helper )
246+ def includes_helper? ( mod , helper )
148247 ancestors = ancestors_of ( mod )
149248
150249 own_ancestors = if Class === mod && ( superclass = superclass_of ( mod ) )
@@ -178,6 +277,107 @@ def generate_module_for(root, constant)
178277 end
179278 end
180279
280+ #: (RBI::Tree root) -> void
281+ def generate_mounted_helpers_module ( root )
282+ engine_mount_names = self . class . engine_mount_names
283+
284+ root . create_module ( "GeneratedMountedHelpers" ) do |mod |
285+ # main_app always returns a plain RoutesProxy
286+ mod . create_method (
287+ "main_app" ,
288+ return_type : "ActionDispatch::Routing::RoutesProxy" ,
289+ )
290+
291+ # One proxy method per mounted engine (only those with routes)
292+ engine_mount_names . each do |mount_name , engine_class |
293+ engine_name = name_of ( engine_class )
294+ next unless engine_name
295+ next unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
296+
297+ proxy_class_name = "#{ engine_name } ::GeneratedRoutesProxy"
298+
299+ mod . create_method (
300+ mount_name . to_s ,
301+ return_type : proxy_class_name ,
302+ )
303+ end
304+ end
305+
306+ # Generate GeneratedRoutesProxy class for each engine (only those with routes)
307+ engine_mount_names . each_value do |engine_class |
308+ engine_name = name_of ( engine_class )
309+ next unless engine_name
310+ next unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
311+
312+ proxy_class_name = "#{ engine_name } ::GeneratedRoutesProxy"
313+ path_helpers_name = "#{ engine_name } ::GeneratedPathHelpersModule"
314+ url_helpers_name = "#{ engine_name } ::GeneratedUrlHelpersModule"
315+
316+ root . create_class ( proxy_class_name , superclass_name : "::ActionDispatch::Routing::RoutesProxy" ) do |klass |
317+ klass . create_include ( path_helpers_name )
318+ klass . create_include ( url_helpers_name )
319+ end
320+ end
321+ end
322+
323+ #: (Module[top] mod) -> bool
324+ def engine_helper_module? ( mod )
325+ Rails . application . railties . grep ( ::Rails ::Engine ) . any? do |engine_instance |
326+ engine_class = engine_instance . class
327+ next false if engine_class == Rails . application . class
328+ next false unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
329+
330+ mod == engine_class . const_get ( :GeneratedPathHelpersModule ) ||
331+ mod == engine_class . const_get ( :GeneratedUrlHelpersModule )
332+ end
333+ end
334+
335+ #: -> void
336+ def generate_url_helper_includer
337+ root . create_path ( constant ) do |mod |
338+ create_mixins_for ( mod , GeneratedUrlHelpersModule )
339+ create_mixins_for ( mod , GeneratedPathHelpersModule )
340+
341+ # GeneratedMountedHelpers is Module.new (for naming), so check
342+ # against the real mounted_helpers module for ancestor detection.
343+ # Only controllers/framework classes actually have mounted_helpers
344+ # in their ancestor chain; plain url_helpers includers do not.
345+ mounted_helpers = Rails . application . routes . mounted_helpers
346+ include_mounted = constant . ancestors . include? ( mounted_helpers ) ||
347+ NON_DISCOVERABLE_INCLUDERS . include? ( constant )
348+ extend_mounted = constant . singleton_class . ancestors . include? ( mounted_helpers )
349+
350+ mod . create_include ( "GeneratedMountedHelpers" ) if include_mounted
351+ mod . create_extend ( "GeneratedMountedHelpers" ) if extend_mounted
352+
353+ # Add engine-specific helper module mixins.
354+ # Do NOT use create_mixins_for for engine helpers — its
355+ # NON_DISCOVERABLE_INCLUDERS fallback would incorrectly add
356+ # engine-specific modules to IntegrationTest and ActionView::Helpers.
357+ Rails . application . railties . grep ( ::Rails ::Engine ) . each do |engine_instance |
358+ engine_class = engine_instance . class
359+ next if engine_class == Rails . application . class
360+ next unless engine_class . const_defined? ( :GeneratedPathHelpersModule , false )
361+
362+ engine_path_mod = engine_class . const_get ( :GeneratedPathHelpersModule )
363+ engine_url_mod = engine_class . const_get ( :GeneratedUrlHelpersModule )
364+
365+ if constant . ancestors . include? ( engine_url_mod )
366+ mod . create_include ( engine_url_mod . name )
367+ end
368+ if constant . singleton_class . ancestors . include? ( engine_url_mod )
369+ mod . create_extend ( engine_url_mod . name )
370+ end
371+ if constant . ancestors . include? ( engine_path_mod )
372+ mod . create_include ( engine_path_mod . name )
373+ end
374+ if constant . singleton_class . ancestors . include? ( engine_path_mod )
375+ mod . create_extend ( engine_path_mod . name )
376+ end
377+ end
378+ end
379+ end
380+
181381 #: (RBI::Scope mod, Module[top] helper_module) -> void
182382 def create_mixins_for ( mod , helper_module )
183383 include_helper = constant . ancestors . include? ( helper_module ) || NON_DISCOVERABLE_INCLUDERS . include? ( constant )
0 commit comments