Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More effectively determine method ownership for RBI generation #896

Merged
merged 1 commit into from Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 24 additions & 1 deletion lib/tapioca/gem/listeners/methods.rb
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
{
Expand Down
58 changes: 58 additions & 0 deletions spec/tapioca/gem/pipeline_spec.rb
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an example that does not call 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
Expand Down