Shopify / tapioca

The swiss army knife of RBI generation
MIT License
723 stars 120 forks source link

Add a DSL generator for requiring the right ancestors in helpers #621

Open vinistock opened 2 years ago

vinistock commented 2 years ago

Because view helpers are dynamically included by Rails, Sorbet is not aware that the modules will be included into certain classes. We can solve this by generating RBIs that add the right requires_ancestor to each helper.

There are two possible behaviors outlined in the Rails documentation for include_all_helpers.

If include_all_helpers is turned on, then every helper is included into ActionView::Base. If not, then application_helper is the only one included into ActionView::Base and every other gets included only in the controller views they match with (e.g.: UsersHelper gets included only in views from UsersController, but not into views from other controllers).

Because Rails loads every module ending in Helper as a helper module, I suspect the DSL generator can just filter all_modules based on whether their name ends with Helper or not and then produce an RBI that looks like this (depending on the value of the configuration, which may alter the requires_ancestor statement).

# users_helper.rbi
# When include_all_helpers is turned on
module UsersHelper
  requires_ancestor { ActionView::Base }
end

# When include_all_helpers is turned off
module UsersHelper
  requires_ancestor { ??? } # Not sure if it would be UsersController or some internal Rails class
end
connorshea commented 2 years ago

This'd be very useful for me, I'm trying to move to tapioca's DSL generators from sorbet-rails and one major thing I'm missing is the "Extra helper includes" config option that sorbet-rails has.

connorshea commented 2 years ago

I don't know if this will be particularly useful, but I've finally moved my Rails app over to Tapioca DSLs and created a custom DSL compiler to get this functionality working:

# typed: true

module Tapioca
  module Compilers
    # This compiler will generate RBIs for all the helper modules in the repo.
    #
    # Rails creates these helper modules with no parent class, so Sorbet
    # doesn't know what should be included in them.
    #
    # This is based heavily on the Helpers RBI generator in the sorbet-rails
    # gem.
    class Helpers < Tapioca::Dsl::Compiler
      extend T::Sig

      ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }

      sig { override.returns(T::Enumerable[Module]) }
      def self.gather_constants
        # API controller does not include ActionController::Helpers
        if ApplicationController < ActionController::Helpers
          helpers = ApplicationController.modules_for_helpers([:all])
        end

        # If ApplicationController doesn't work or doesn't return any helpers,
        # try using ActionController::Base.
        if ApplicationController < ActionController::Helpers && helpers.blank?
          helpers = ActionController::Base.modules_for_helpers([:all])
        end

        # Collect all helpers
        helpers
      end

      sig { override.void }
      def decorate
        root.create_path(constant) do |klass|
          # Default includes:
          klass.create_include('Kernel')
          klass.create_include('ActionView::Helpers')

          # CUSTOM INCLUDES GO HERE:
          klass.create_include('Devise::Controllers::Helpers')
        end
      end
    end
  end
end
vmrocha commented 1 year ago

Hello @connorshea, can you share how can I use that helper on my own Rails application? I'm using Devise for authentication and I'm finding that Sorbet does not correctly identify the current_user method.

Malet commented 10 months ago

@vmrocha I just created config/initializers/tapioca_compilers_helpers.rb with the contents above and that does generate rbis for a lot of other helpers, but doesn't fix the issue with current_user :(