Class: Tapioca::Dsl::Compilers::UrlHelpers

Inherits:
Tapioca::Dsl::Compiler show all
Defined in:
lib/tapioca/dsl/compilers/url_helpers.rb

Overview

Tapioca::Dsl::Compilers::UrlHelpers generates RBI files for classes that include or extend Rails.application.routes.url_helpers.

The compiler registers generated constants to represent the Rails route helper modules:

  1. GeneratedPathHelpersModule holds the main application's path helpers, such as post_path.

  2. GeneratedUrlHelpersModule holds the main application's URL helpers, such as post_url.

  3. GeneratedMountedHelpers is a synthetic module for mounted application and engine helpers, such as main_app and articles. Rails exposes these helpers through an anonymous dynamic module, so the compiler creates a named RBI module that can be included or extended by classes that receive mounted helpers at runtime. It is only generated for applications that mount an engine that defines its own routes.

For mounted engines, the compiler also registers engine-scoped GeneratedPathHelpersModule and GeneratedUrlHelpersModule constants. Mounted engine helper methods return a synthetic GeneratedRoutesProxy subclass that includes those engine-scoped helper modules.

For example, with the following setup:

# config/application.rb
class Application < Rails::Application
  routes.draw do
    resource :index

    mount Blog::Engine, at: "/blog", as: "articles"
  end
end
app/models/post.rb
class Post
  # Use `T.unsafe` so that Sorbet does not complain about a dynamic
  # module being included. This allows the `include` to happen properly
  # at runtime but Sorbet won't see the include. However, since this
  # compiler will generate the proper RBI files for the include,
  # static type checking will work as expected.
  T.unsafe(self).include Rails.application.routes.url_helpers
end

this compiler will produce the following RBI files:

# generated_path_helpers_module.rbi
# typed: true
module GeneratedPathHelpersModule
  include ActionDispatch::Routing::PolymorphicRoutes
  include ActionDispatch::Routing::UrlFor

  sig { params(args: T.untyped).returns(String) }
  def articles_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def edit_index_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def index_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_index_path(*args); end
end
# generated_url_helpers_module.rbi
# typed: true
module GeneratedUrlHelpersModule
  include ActionDispatch::Routing::PolymorphicRoutes
  include ActionDispatch::Routing::UrlFor

  sig { params(args: T.untyped).returns(String) }
  def articles_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def edit_index_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def index_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_index_url(*args); end
end
# post.rbi
# typed: true
class Post
  include GeneratedPathHelpersModule
  include GeneratedUrlHelpersModule
end
# blog/config/routes.rb
Blog::Engine.routes.draw do
  resources :posts
end
# blog/engine/generated_path_helpers_module.rbi
# typed: true
module Blog::Engine::GeneratedPathHelpersModule
  include ActionDispatch::Routing::PolymorphicRoutes
  include ActionDispatch::Routing::UrlFor

  sig { params(args: T.untyped).returns(String) }
  def edit_post_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_post_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def post_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def posts_path(*args); end
end
# blog/engine/generated_url_helpers_module.rbi
# typed: true
module Blog::Engine::GeneratedUrlHelpersModule
  include ActionDispatch::Routing::PolymorphicRoutes
  include ActionDispatch::Routing::UrlFor

  sig { params(args: T.untyped).returns(String) }
  def edit_post_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_post_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def post_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def posts_url(*args); end
end
# generated_mounted_helpers.rbi
# typed: true
module GeneratedMountedHelpers
  sig { returns(Blog::Engine::GeneratedRoutesProxy) }
  def articles; end

  sig { returns(GeneratedRoutesProxy) }
  def main_app; end
end
# generated_routes_proxy.rbi
# typed: true
class GeneratedRoutesProxy < ::ActionDispatch::Routing::RoutesProxy
  include GeneratedPathHelpersModule
  include GeneratedUrlHelpersModule
end
# blog/engine/generated_routes_proxy.rbi
# typed: true
class Blog::Engine::GeneratedRoutesProxy < ::ActionDispatch::Routing::RoutesProxy
  include Blog::Engine::GeneratedPathHelpersModule
  include Blog::Engine::GeneratedUrlHelpersModule
end

: [ConstantType = Module[top]]

Constant Summary collapse

NON_DISCOVERABLE_INCLUDERS =

: Array[Module[top]]

gather_non_discoverable_includers

Constants included from Runtime::Reflection

Runtime::Reflection::ANCESTORS_METHOD, Runtime::Reflection::CLASS_METHOD, Runtime::Reflection::CONSTANTS_METHOD, Runtime::Reflection::EQUAL_METHOD, Runtime::Reflection::METHOD_METHOD, Runtime::Reflection::NAME_METHOD, Runtime::Reflection::OBJECT_ID_METHOD, Runtime::Reflection::PRIVATE_INSTANCE_METHODS_METHOD, Runtime::Reflection::PROTECTED_INSTANCE_METHODS_METHOD, Runtime::Reflection::PUBLIC_INSTANCE_METHODS_METHOD, Runtime::Reflection::REQUIRED_FROM_LABELS, Runtime::Reflection::SINGLETON_CLASS_METHOD, Runtime::Reflection::SUPERCLASS_METHOD, Runtime::Reflection::SignatureBlockError, Runtime::Reflection::UNDEFINED_CONSTANT

Constants included from RBIHelper

RBIHelper::TYPE_PARAMETER_MATCHER

Constants included from SorbetHelper

SorbetHelper::FEATURE_REQUIREMENTS, SorbetHelper::SORBET_BIN, SorbetHelper::SORBET_EXE_PATH_ENV_VAR, SorbetHelper::SORBET_GEM_SPEC, SorbetHelper::SORBET_PAYLOAD_URL, SorbetHelper::SPOOM_CONTEXT

Class Attribute Summary collapse

Attributes inherited from Tapioca::Dsl::Compiler

#constant, #options, #root

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Tapioca::Dsl::Compiler

#add_error, #compiler_enabled?, handles?, #initialize, processable_constants, requested_constants=, reset_state

Methods included from Runtime::Reflection

#abstract_type_of, #ancestors_of, #are_equal?, #attached_class_of, #class_of, #const_source_location, #constant_defined?, #constantize, #constants_of, #descendants_of, #file_candidates_for, #final_module?, #inherited_ancestors_of, #method_of, #name_of, #name_of_type, #object_id_of, #private_instance_methods_of, #protected_instance_methods_of, #public_instance_methods_of, #qualified_name_of, #resolve_loc, #sealed_module?, #signature_of, #signature_of!, #singleton_class_of, #superclass_of

Methods included from RBIHelper

#anonymous_parameter_name?, #as_nilable_type, #as_non_nilable_type, #create_block_param, #create_kw_opt_param, #create_kw_param, #create_kw_rest_param, #create_opt_param, #create_param, #create_rest_param, #create_typed_param, #extract_type_parameters, #sanitize_signature_types, serialize_type_variable, #valid_method_name?, #valid_parameter_name?

Methods included from SorbetHelper

#sorbet, #sorbet_path, #sorbet_supports?, #sorbet_syntax_check!

Constructor Details

This class inherits a constructor from Tapioca::Dsl::Compiler

Class Attribute Details

.engine_mount_names ⇒ Object (readonly)

: Hash[Symbol, singleton(::Rails::Engine)]



210
211
212
# File 'lib/tapioca/dsl/compilers/url_helpers.rb', line 210

def engine_mount_names
  @engine_mount_names
end

Class Method Details

.gather_constants ⇒ Object

: -> Enumerable[Module[top]]



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/tapioca/dsl/compilers/url_helpers.rb', line 214

def gather_constants
  return [] unless defined?(Rails.application) && Rails.application

  # Load routes if they haven't been loaded yet (see https://github.com/rails/rails/pull/51614).
  routes_reloader = Rails.application.routes_reloader
  routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)

  url_helpers_module = Rails.application.routes.named_routes.url_helpers_module
  path_helpers_module = Rails.application.routes.named_routes.path_helpers_module

  Object.const_set(:GeneratedUrlHelpersModule, url_helpers_module)
  Object.const_set(:GeneratedPathHelpersModule, path_helpers_module)

  @engine_mount_names = mounted_engine_names
  engine_helper_modules = register_engine_route_helpers

  # Only synthesize the mounted helpers module when at least one mounted engine
  # contributes its own route helpers. A mount of a routeless engine (or an app with
  # no mounts) would leave nothing but `main_app`, which we don't generate on its own.
  # This predicate mirrors `proxied_engines` in `generate_mounted_helpers_module`.
  mounts_engine_with_helpers = @engine_mount_names.values.any? do |engine_class|
    name_of(engine_class) && engine_class.const_defined?(:GeneratedPathHelpersModule, false)
  end

  if mounts_engine_with_helpers
    Object.const_set(:GeneratedMountedHelpers, Module.new)
  end

  constants = all_modules.select do |mod|
    next unless name_of(mod)

    # Fast-path to quickly disqualify most cases
    has_helpers = url_helpers_module > mod ||
      path_helpers_module > mod ||
      url_helpers_module > mod.singleton_class ||
      path_helpers_module > mod.singleton_class

    has_helpers ||= engine_helper_modules.any? do |engine_mod|
      engine_mod > mod || engine_mod > mod.singleton_class
    end

    next false unless has_helpers

    includes_helper?(mod, url_helpers_module) ||
      includes_helper?(mod, path_helpers_module) ||
      includes_helper?(mod.singleton_class, url_helpers_module) ||
      includes_helper?(mod.singleton_class, path_helpers_module) ||
      engine_helper_modules.any? { |engine_mod| includes_helper?(mod, engine_mod) || includes_helper?(mod.singleton_class, engine_mod) }
  end

  constants
    .concat(NON_DISCOVERABLE_INCLUDERS)
    .push(GeneratedUrlHelpersModule, GeneratedPathHelpersModule)
    .concat(engine_helper_modules)

  constants.push(GeneratedMountedHelpers) if defined?(GeneratedMountedHelpers)

  constants
end

Instance Method Details

#decorate ⇒ Object

: -> void



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/tapioca/dsl/compilers/url_helpers.rb', line 188

def decorate
  case constant
  when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
    generate_module_for(root, constant)
  else
    # `GeneratedMountedHelpers` is only defined when an engine is mounted (see `gather_constants`).
    if defined?(::GeneratedMountedHelpers) && GeneratedMountedHelpers.singleton_class === constant
      generate_mounted_helpers_module(root)
    elsif engine_helper_module?(constant)
      generate_module_for(root, constant)
    else
      generate_url_helper_includer
    end
  end
end