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

Filter out mixins that weren't performed in current gem #1012

Merged
merged 3 commits into from
Jun 29, 2022

Conversation

egiurleo
Copy link
Contributor

@egiurleo egiurleo commented Jun 27, 2022

Closes #890

Motivation

This is the culmination of all the dynamic mixin work!

Currently, when RBIs are generated for mixins, tapioca doesn't take into account whether a mixin was performed in the current gem or in another gem. This results in strange scenarios where one gem will re-open a class to perform a mixin, and then that mixin is reflected in another gem's RBI. We should only generate RBI for mixins that are performed in the current gem.

Implementation

This PR can be read commit-by-commit.

  1. The first commit refactors the mixin tracker to remove the mixin_locations_for method, which is no longer used, and modify the constants_with_mixin method to keep track of mixin locations.
  2. The second commit moves the constant_name_from_singleton_class method into Runtime::Reflection so it can be shared with the Module extension coming from the mixin tracker.
  3. The second commit modifies the mixins pipeline listener to use the mixin_locations_for method to skip any mixins that weren't performed in the current gem.

Tests

Modified existing tests to remove mixins that should be filtered out. Un-commented two lines in a cli test that now pass because of these changes!

Tophatting 🎩

Can generate gem RBIs on Shopify core with these changes.

@egiurleo egiurleo marked this pull request as ready for review June 27, 2022 15:27
@egiurleo egiurleo force-pushed the emily/mixin-filtering branch from b9aca82 to 50ada09 Compare June 27, 2022 16:07
@@ -24,23 +24,25 @@ def on_scope(event)
# The way we identify these "foreign constants" is by asking the mixin tracker which
# constants have mixed in the current module that we are handling. We add all the
# constants that we discover to the pipeline to be processed.
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |constant, location|
next unless mixed_in_by_gem?(location)
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |_, location_info|
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The changes in this file are a result of the refactor to constants_with_mixin. Functionality is not changed.

Copy link
Member

Choose a reason for hiding this comment

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

Should this be:

Suggested change
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |_, location_info|
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each_value do |location_info|

instead?

# Some extend mixins are actually includes or prepend on the singleton class.
# If an extend mixin is not found in the tracker, these lines search for a prepend
# or include on the singleton class and use the resulting file as the mixin location.
if mixin_location.nil? && mixin_type == Runtime::Trackers::Mixin::Type::Extend
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sometimes, extension mixins are actually tracked in the mixin tracker as prepends or includes to the singleton class. If the mixin is not found under the Runtime::Trackers::Mixin::Type::Extend type, these lines search the mixin tracker for a prepend or include on the singleton class.

This feels a little bit hacky to me...

Copy link
Member

Choose a reason for hiding this comment

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

This is something we can rectify at mixin registration time, I guess. If a mixin is being prepended/included into a singleton class, then we can record that as an extend on the attached class at that time.

# actionpack RBI should have nothing in it about `TypedParameters`
# refute_includes(actionpack_rbi, "TypedParameters")
refute_includes(actionpack_rbi, "TypedParameters")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This assertion passes now 🎉

spec/tapioca/gem/pipeline_spec.rb Show resolved Hide resolved
@egiurleo egiurleo requested a review from a team June 27, 2022 16:09
mods
.select do |mod|
name = @pipeline.name_of(mod)

name && !filtered_mixin?(name)
end
.map do |mod|
mixin_location = mixin_location(constant, mod, mixin_type)

# Some extend mixins are actually includes or prepend on the singleton class.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# Some extend mixins are actually includes or prepend on the singleton class.
# Some extend mixins are actually includes or prepends on the singleton class.

location: String,
).returns(T::Boolean)
end
def mixed_in_by_gem?(location)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The method is mixed_in_by_gem? but works with a location only, which doesn't seem strictly related to mixins, should we rename it location_in_gem??

Or maybe mixed_in_by_gem? should take a constant?

Comment on lines 77 to 81
sig do
params(
location: String,
).returns(T::Boolean)
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's inline signatures < 120 chars please 🙏

@@ -24,23 +24,25 @@ def on_scope(event)
# The way we identify these "foreign constants" is by asking the mixin tracker which
# constants have mixed in the current module that we are handling. We add all the
# constants that we discover to the pipeline to be processed.
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |constant, location|
next unless mixed_in_by_gem?(location)
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |_, location_info|
Copy link
Member

Choose a reason for hiding this comment

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

Should this be:

Suggested change
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |_, location_info|
Runtime::Trackers::Mixin.constants_with_mixin(mixin).each_value do |location_info|

instead?

).returns(T.nilable(String))
end
def mixin_location(constant, mixin, mixin_type)
T.must(Runtime::Trackers::Mixin.constants_with_mixin(mixin)[mixin_type])[constant]
Copy link
Member

Choose a reason for hiding this comment

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

Can we use dig instead?

Suggested change
T.must(Runtime::Trackers::Mixin.constants_with_mixin(mixin)[mixin_type])[constant]
Runtime::Trackers::Mixin.constants_with_mixin(mixin).dig(mixin_type, constant)

Comment on lines 107 to 108
T.must(mixin_locations[Runtime::Trackers::Mixin::Type::Include])[singleton_class] ||
T.must(mixin_locations[Runtime::Trackers::Mixin::Type::Prepend])[singleton_class]
Copy link
Member

Choose a reason for hiding this comment

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

Again, dig would be much nicer:

Suggested change
T.must(mixin_locations[Runtime::Trackers::Mixin::Type::Include])[singleton_class] ||
T.must(mixin_locations[Runtime::Trackers::Mixin::Type::Prepend])[singleton_class]
mixin_locations.dig(Runtime::Trackers::Mixin::Type::Include, singleton_class) ||
mixin_locations.dig(Runtime::Trackers::Mixin::Type::Prepend, singleton_class)

spec/tapioca/gem/pipeline_spec.rb Show resolved Hide resolved
# Some extend mixins are actually includes or prepend on the singleton class.
# If an extend mixin is not found in the tracker, these lines search for a prepend
# or include on the singleton class and use the resulting file as the mixin location.
if mixin_location.nil? && mixin_type == Runtime::Trackers::Mixin::Type::Extend
Copy link
Member

Choose a reason for hiding this comment

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

This is something we can rectify at mixin registration time, I guess. If a mixin is being prepended/included into a singleton class, then we can record that as an extend on the attached class at that time.

egiurleo added 3 commits June 28, 2022 12:50
Refactor the Trackers::Mixin#constants_with_mixin method to keep track
of the type of each mixin. This will be used to filter mixins that were
not performed in the current gem.
Uses `Trackers::Mixin` to find the location of each mixin and filter out
any that did not occur in the current gem to avoid generating
unnecessary RBI.
@egiurleo egiurleo force-pushed the emily/mixin-filtering branch from 50ada09 to 445d9ef Compare June 28, 2022 20:37
@@ -168,6 +168,17 @@ def required_from_location

required_location.absolute_path || ""
end

sig { params(constant: Module).returns(T.nilable(String)) }
def constant_name_from_singleton_class(constant)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved these two methods into reflection because I had to use them in Trackers::Mixin to find the attached class based on a singleton class.

Comment on lines -56 to -59
sig { params(constant: Module).returns(T.nilable(String)) }
def constant_name_from_singleton_class(constant)
constant.to_s.match("#<Class:(.+)>")&.captures&.first
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method was moved into Runtime::Reflection

lib/tapioca/gem/listeners/mixins.rb Show resolved Hide resolved
# attached class. Registering the mixin as an extend on the attached class ensures that
# 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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This functionality was moved from Listeners::Mixins so that an include/prepend on a singleton class can also be found as an extend on the attached class.

Comment on lines -323 to -325
.gsub(/^\s+include ::Minitest::Expectations\s/, "")
.gsub(/^\s+include ::JSON::Ext::Generator::GeneratorMethods::Object\s/, "")
.gsub(/^\s+include ::PP::ObjectMixin\s/, "")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@paracycle Is this gsub you were talking about?

@egiurleo egiurleo requested review from paracycle and Morriar June 28, 2022 20:42
@Morriar Morriar merged commit b61b074 into main Jun 29, 2022
@Morriar Morriar deleted the emily/mixin-filtering branch June 29, 2022 14:40
@shopify-shipit shopify-shipit bot temporarily deployed to production July 7, 2022 17:53 Inactive
@shopify-shipit shopify-shipit bot temporarily deployed to 0-9-stable August 19, 2022 20:37 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Better mixin generation for gem RBIs
3 participants