Skip to content

Commit

Permalink
New approach to finding attached class
Browse files Browse the repository at this point in the history
Historically, to identify the attached class of a singleton class, we've
parsed the name (result of the #inspect method) of the singleton class
and then constantized the result.

However, this doesn't work when the #inspect method of a singleton class
is overridden because the return value of #inspect no longer conforms to
the structure we expected.

Instead, we can search ObjectSpace for the class that has the correct
singleton class. This is less performant but more correct.
  • Loading branch information
egiurleo committed Aug 5, 2022
1 parent 9e46b80 commit da38f30
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 13 deletions.
6 changes: 2 additions & 4 deletions lib/tapioca/gem/listeners/foreign_constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ def on_scope(event)
# base constant. Then, generate RBIs as if the base constant is extending the mixin,
# which is functionally equivalent to including or prepending to the singleton class.
if !name && constant.singleton_class?
name = constant_name_from_singleton_class(constant)
next unless name

constant = T.cast(constantize(name), Module)
attached_class = attached_class_of(constant)
constant = attached_class if attached_class
end

@pipeline.push_foreign_constant(name, constant) if name
Expand Down
15 changes: 7 additions & 8 deletions lib/tapioca/runtime/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,14 @@ def resolve_loc(locations)
resolved_loc.absolute_path || ""
end

sig { params(constant: Module).returns(T.nilable(String)) }
def constant_name_from_singleton_class(constant)
constant.to_s.match("#<Class:(.+)>")&.captures&.first
end
sig { params(singleton_class: Module).returns(T.nilable(Module)) }
def attached_class_of(singleton_class)
# https://stackoverflow.com/a/36622320/98634
result = ObjectSpace.each_object(singleton_class).find do |klass|
singleton_class_of(T.cast(klass, Module)) == singleton_class
end

sig { params(constant: Module).returns(T.nilable(BasicObject)) }
def constant_from_singleton_class(constant)
constant_name = constant_name_from_singleton_class(constant)
constantize(constant_name) if constant_name
T.cast(result, Module)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/tapioca/runtime/trackers/mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def extend_object(obj)
# this mixin can be found whether searching for an include/prepend on the singleton class
# or an extend on the attached class.
def register_extend_on_attached_class(constant)
attached_class = Tapioca::Runtime::Reflection.constant_from_singleton_class(constant)
attached_class = Tapioca::Runtime::Reflection.attached_class_of(constant)

Tapioca::Runtime::Trackers::Mixin.register(
T.cast(attached_class, Module),
Expand Down
27 changes: 27 additions & 0 deletions spec/tapioca/cli/gem_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,33 @@ module Foo; end
RBI
end

it "must generate RBIs for foreign constants whose singleton class overrides #inspect" do
bar = mock_gem("bar", "0.0.2") do
write("lib/bar.rb", <<~RBI)
class Bar
def self.inspect
"Override!"
end
end
RBI
end

foo = mock_gem("foo", "0.0.1") do
write("lib/foo.rb", <<~RBI)
module Foo; end
Bar.singleton_class.include(Foo)
RBI
end

@project.require_mock_gem(bar)
@project.require_mock_gem(foo)
@project.bundle_install

result = @project.tapioca("gem foo")
assert_empty_stderr(result)
end

it "must not load engines in the application" do
@project.write("config/application.rb", <<~RB)
require "rails"
Expand Down

0 comments on commit da38f30

Please sign in to comment.