From 60197b3db191d300a33464ff89ccb8a14e35abc1 Mon Sep 17 00:00:00 2001 From: Adam Traver Date: Fri, 17 Apr 2026 02:47:08 +0000 Subject: [PATCH] Filter prepended-into-ancestors modules in ActionControllerHelpers When a module is prepended into another ancestor in the helpers chain (e.g. `DEBUGGER__::TrapInterceptor` prepended into `::Kernel` by debug.gem, or `ActiveSupport::CoreExt::ERBUtil*` prepended into `::ERB::Util` by ActiveSupport), it appears in `mod.ancestors` even though the user never explicitly included it. Emitting `include ::X` for those modules pollutes generated RBIs and, in the DEBUGGER case, causes Sorbet to fail with "Unable to resolve constant DEBUGGER__" because debug.gem is not typechecked. This shows up in practice when the Ruby LSP Tapioca add-on regenerates DSL RBIs inside a ruby-lsp process that has `debug/session` loaded -- the chain is visible in the LSP but not in plain-shell invocations of `bin/tapioca dsl`, which is very confusing to diagnose. Fix: in `gather_includes`, compute the set of modules that appear in any ancestor's prepend chain and exclude them from the output. --- .../compilers/action_controller_helpers.rb | 26 ++++++++++++++-- .../action_controller_helpers_spec.rb | 30 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/tapioca/dsl/compilers/action_controller_helpers.rb b/lib/tapioca/dsl/compilers/action_controller_helpers.rb index 3350255ef..cf7908eb1 100644 --- a/lib/tapioca/dsl/compilers/action_controller_helpers.rb +++ b/lib/tapioca/dsl/compilers/action_controller_helpers.rb @@ -150,8 +150,30 @@ def create_unknown_proxy_method(helper_methods, method_name) #: (Module[top] mod) -> Array[String] def gather_includes(mod) - mod.ancestors - .reject { |ancestor| ancestor.is_a?(Class) || ancestor == mod || name_of(ancestor).nil? } + ancestors = mod.ancestors + + # Exclude modules that were prepended into another ancestor in the chain + # rather than explicitly included by the user. Otherwise, modules like + # `DEBUGGER__::TrapInterceptor` (prepended into `::Kernel` by the `debug` + # gem when loaded in-process by, e.g., the Ruby LSP Tapioca add-on) leak + # into generated RBIs. + prepended_into_ancestors = ancestors.each_with_object(Set.new) do |ancestor, set| + next unless ancestor.is_a?(Module) + + ancestor.ancestors.each do |sub| + break if sub == ancestor + + set << sub + end + end + + ancestors + .reject do |ancestor| + ancestor.is_a?(Class) || + ancestor == mod || + name_of(ancestor).nil? || + prepended_into_ancestors.include?(ancestor) + end .map { |ancestor| T.must(qualified_name_of(ancestor)) } .reverse end diff --git a/spec/tapioca/dsl/compilers/action_controller_helpers_spec.rb b/spec/tapioca/dsl/compilers/action_controller_helpers_spec.rb index f73d31717..f00c8283a 100644 --- a/spec/tapioca/dsl/compilers/action_controller_helpers_spec.rb +++ b/spec/tapioca/dsl/compilers/action_controller_helpers_spec.rb @@ -383,6 +383,36 @@ class HelperProxy < ::ActionView::Base RBI assert_equal expected, rbi_for(:UserController) end + + it "does not include modules prepended into an ancestor (e.g. debug.gem's TrapInterceptor)" do + # `debug/session` prepends `DEBUGGER__::TrapInterceptor` (and related + # modules) into `::Kernel`. The Ruby LSP Tapioca add-on loads + # `debug/session` in-process, so when it triggers DSL regeneration, + # modules prepended into `::Kernel` leak into any helper whose ancestor + # chain traverses `::Kernel`. + # + # These modules were not explicitly included by the user and should not + # appear in generated RBIs. + require "debug/session" + + add_ruby_file("kernel_helper.rb", <<~RUBY) + module KernelHelper + include Kernel + end + RUBY + + add_ruby_file("controller.rb", <<~RUBY) + class UserController < ActionController::Base + helper KernelHelper + helper_method :foo + def foo + "bar" + end + end + RUBY + + refute_includes(rbi_for(:UserController), "DEBUGGER__") + end end end end