diff --git a/lib/tapioca/gem/listeners/methods.rb b/lib/tapioca/gem/listeners/methods.rb index 5bfdffd48..bfc0a3e82 100644 --- a/lib/tapioca/gem/listeners/methods.rb +++ b/lib/tapioca/gem/listeners/methods.rb @@ -66,7 +66,7 @@ def compile_directly_owned_methods(tree, module_name, mod, for_visibility = [:pu end def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public.new) return unless method - return unless method.owner == constant + return unless method_owned_by_constant?(method, constant) return if @pipeline.symbol_in_payload?(symbol_name) && !@pipeline.method_in_gem?(method) signature = signature_of(method) @@ -142,6 +142,29 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public tree << rbi_method end + # Check whether the method is defined by the constant. + # + # In most cases, it works to check that the constant is the method owner. However, + # in the case that a method is also defined in a module prepended to the constant, it + # will be owned by the prepended module, not the constant. + # + # This method implements a better way of checking whether a constant defines a method. + # It walks up the ancestor tree via the `super_method` method; if any of the super + # methods are owned by the constant, it means that the constant declares the method. + sig { params(method: UnboundMethod, constant: Module).returns(T::Boolean) } + def method_owned_by_constant?(method, constant) + # Widen the type of `method` to be nilable + method = T.let(method, T.nilable(UnboundMethod)) + + while method + return true if method.owner == constant + + method = method.super_method + end + + false + end + sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) } def method_names_by_visibility(mod) { diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index 6c075b4ad..eade7d3f1 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -1210,6 +1210,64 @@ class Bar; end assert_equal(output, compile) end + it "compiles method that is also prepended" do + add_ruby_file("foo.rb", <<~RUBY) + module Foo + def bar + super + end + end + + class Baz + prepend Foo + + def bar; end + end + RUBY + + output = template(<<~RBI) + class Baz + include ::Foo + + def bar; end + end + + module Foo + def bar; end + end + RBI + + assert_equal(output, compile) + end + + it "compiles a method that is prepended without calling super" do + add_ruby_file("foo.rb", <<~RUBY) + module Foo + def bar; end + end + + class Baz + prepend Foo + + def bar; end + end + RUBY + + output = template(<<~RBI) + class Baz + include ::Foo + + def bar; end + end + + module Foo + def bar; end + end + RBI + + assert_equal(output, compile) + end + it "ignores methods on other objects" do add_ruby_file("bar.rb", <<~RUBY) class Bar