Skip to content

Add an ActionMailerHelpers compiler for mailers' HelperMethods modules - #2761

Open
ndbroadbent wants to merge 2 commits into
Shopify:mainfrom
DocSpring:nathan/mailer-helper-methods
Open

ndbroadbent wants to merge 2 commits into
Shopify:mainfrom
DocSpring:nathan/mailer-helper-methods

Conversation

@ndbroadbent

Copy link
Copy Markdown

Stacked on #2760: review that one first.

Motivation

A mailer that calls helper or helper_method gets its own HelperMethods module from AbstractController::Helpers, as a controller does, and its views include it. Tapioca generates these only for controllers, so Sorbet doesn't know NotifierMailer::HelperMethods exists, nor the methods that helper_method and helper do ... end define in it. We hit this type-checking mailer views, whose classes include their mailer's module.

Implementation

  • A new ActionMailerHelpers compiler writes each mailer's HelperMethods, without the helpers method and HelperProxy class, which mailers don't have.
  • The code that builds the module moves from ActionControllerHelpers into Tapioca::Dsl::Helpers::HelperMethodsHelper, which both compilers include, so mailers also include their parent mailer's module and only the modules they add.
  • The helper module uses #: self as HelperMethodsHelper & Compiler[untyped] in the one method that calls create_method_from_def, because requires_ancestor can't name the generic Compiler. Happy to share the code another way if you prefer, for example a separate compiler with its own copy.

Tests

action_mailer_helpers_spec.rb, modelled on the controller compiler's: which mailers it gathers, a module with an included helper, a helper_method with a sig, one whose target doesn't exist and a helper block, a parent mailer's module, and a single requested mailer. The controller spec passes unchanged.

…ts helpers

A controller's `HelperMethods` includes its parent controller's module, and
the compiler listed every module from that chain again in each controller's
RBI. Rails calls `helper :all` on each direct subclass of
`ActionController::Base` (`include_all_helpers`, on by default), so every
controller below `ApplicationController` that has helpers of its own lists
every module in app/helpers, and adding one helper module rewrites all of
their RBIs.

When the parent's module is one this compiler generates an RBI for, include
it and only the modules the controller adds. A direct subclass of
`ActionController::Base` still lists every module, because the gem RBI for
`ActionController::Base::HelperMethods` can't know the helpers other gems add
to it at runtime.

The parent is found from the class chain, not from the gathered constants, so
`tapioca dsl UserController` generates the same RBI as `tapioca dsl`.
A mailer that calls `helper` or `helper_method` gets its own `HelperMethods`
module from `AbstractController::Helpers`, just like a controller, and its
views include it. Tapioca generated these only for controllers, so Sorbet
didn't know `NotifierMailer::HelperMethods` existed, nor the methods that
`helper_method` and `helper do ... end` define in it.

The new compiler writes the same module for mailers, without the
`helpers`/`HelperProxy` pair, which mailers don't have. The code that builds
the module moves from ActionControllerHelpers into
`Tapioca::Dsl::Helpers::HelperMethodsHelper`, which both compilers include,
so mailers also include their parent mailer's module and only the helper
modules they add.
@ndbroadbent
ndbroadbent requested a review from a team as a code owner November 3, 2026 07:13

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant