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