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

Improve eager loading Rails engines when Zeitwerk is present #1329

Merged
merged 3 commits into from
Jan 10, 2023

Conversation

egiurleo
Copy link
Contributor

@egiurleo egiurleo commented Dec 22, 2022

Motivation

Closes #671
Closes Shopify/rbi-central#135

RBI generation for Rails engines is currently implemented with a best effort approach. We identify every file in the application's eager_load_path and attempt to require those files. This does not account for dependency order, so we keep track of any files that fail in the process and then try to require them again once all the other files are loaded.

This approach is not exhaustive and can sometimes miss constants defined in engines. For example, if an engine defines a constant (e.g. Turbo::Streams) as a namespace for subconstants, but never on its own, the current approach will never generate type annotations for that constant.

Implementation

This implementation improves engine eager loading when Zeitwerk is present in the Rails application. Rather than relying on the best effort approach, we use the behavior already implemented in Zeitwerk to eager load every file in the application's eager_load_path, which is much more reliable. The resulting RBI will contain constants defined in the engine that wouldn't have been found by the previous approach.

If Zeitwerk is not present, then we fall back to the best effort approach that we were already using.

Tests

We have added a test that passes with these changes and fails without them.

@egiurleo egiurleo self-assigned this Dec 22, 2022
@egiurleo egiurleo force-pushed the emily/properly-load-rails-engines branch from 05a9bef to 965c3ca Compare December 22, 2022 22:00
@paracycle paracycle force-pushed the emily/properly-load-rails-engines branch from fa18dfe to 2d93c45 Compare December 23, 2022 22:28
@egiurleo egiurleo force-pushed the emily/properly-load-rails-engines branch from 715a2c4 to 97aec17 Compare January 4, 2023 18:56
spec/tapioca/cli/gem_spec.rb Outdated Show resolved Hide resolved
spec/tapioca/cli/gem_spec.rb Outdated Show resolved Hide resolved
spec/tapioca/cli/gem_spec.rb Outdated Show resolved Hide resolved
spec/tapioca/cli/gem_spec.rb Outdated Show resolved Hide resolved
@egiurleo egiurleo force-pushed the emily/properly-load-rails-engines branch 3 times, most recently from 988dd2e to 7f512ac Compare January 4, 2023 21:56
spec/tapioca/cli/gem_spec.rb Outdated Show resolved Hide resolved
@egiurleo egiurleo force-pushed the emily/properly-load-rails-engines branch 2 times, most recently from 4588d91 to 401b188 Compare January 5, 2023 17:12
@egiurleo egiurleo marked this pull request as ready for review January 5, 2023 18:40
@egiurleo egiurleo requested a review from a team as a code owner January 5, 2023 18:40
Copy link
Member

@paracycle paracycle left a comment

Choose a reason for hiding this comment

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

Thank you!

Copy link
Contributor

@KaanOzkan KaanOzkan left a comment

Choose a reason for hiding this comment

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

🎉

I think I'll close Shopify/rbi-central#135 since Zeitwerk is default for Rails 6+

lib/tapioca/loaders/loader.rb Outdated Show resolved Hide resolved
def rails_engines
return [] unless Object.const_defined?("Rails::Engine")

safe_require("active_support/core_ext/class/subclasses")

project_path = Bundler.default_gemfile.parent.expand_path
# We can use `Class#descendants` here, since we know Rails is loaded
Object.const_get("Rails::Engine")
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious, was there a reason these used to be const_get?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is a really good question and I'm not sure why, besides maybe it needed to be that way at some point and nobody ever fixed it? I'm assuming that because we already had the Object.const_defined?("Rails::Engine") check in place, this change could have been made earlier. @paracycle might know!

Copy link
Member

Choose a reason for hiding this comment

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

I think it was a combination of me being less experienced when I originally wrote this code about 3.5 years ago, and, also, the fact that Tapioca did not have a dev-time Rails dependency, thus, Sorbet didn't know about constants like Rails and would raise static type-checking errors.

Comment on lines 151 to 152
rescue NameError
nil
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can still get a NameError in this method, due to the defined? check. So we can remove this.

Rails.app_class = Rails.application = rails_application
end

T::Sig::WithoutRuntime.sig { returns(T::Array[T.class_of(Rails::Engine)]) }
def rails_engines
return [] unless Object.const_defined?("Rails::Engine")
Copy link
Member

Choose a reason for hiding this comment

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

I think we can make this:

Suggested change
return [] unless Object.const_defined?("Rails::Engine")
return [] unless defined?(Rails::Engine)

which reads better than const_defined? and the constant reference can be typechecked.

egiurleo and others added 3 commits January 10, 2023 09:17
RBI generation for Rails engines is currently implemented with a best effort approach. We identify every file in the application's eager_load_path and attempt to require those files. This does not account for dependency order, so we keep track of any files that fail in the process and then try to require them again once all the other files are loaded.

This approach is not exhaustive and can sometimes miss constants defined in engines. For example, if an engine defines a constant (e.g. Turbo::Streams) as a namespace for subconstants, but never on its own, the current approach will never generate type annotations for that constant.

This implementation improves engine eager loading when Zeitwerk is present in the Rails application. Rather than relying on the best effort approach, we use the behavior already implemented in Zeitwerk to eager load every file in the application's eager_load_path, which is much more reliable. The resulting RBI will contain constants defined in the engine that wouldn't have been found by the previous approach.

If Zeitwerk is not present, then we fall back to the best effort approach that we were already using.
@egiurleo egiurleo force-pushed the emily/properly-load-rails-engines branch from 96d3953 to b1beb6c Compare January 10, 2023 15:57
@egiurleo
Copy link
Contributor Author

Okay I think I've addressed everyone's comments. I also squashed all the little commits into one big one and gave it a good description.

@egiurleo egiurleo merged commit 951fa6c into main Jan 10, 2023
@egiurleo egiurleo deleted the emily/properly-load-rails-engines branch January 10, 2023 20:52
@shopify-shipit shopify-shipit bot temporarily deployed to production February 21, 2023 12:01 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.

Missing type declaration ::Turbo::Streams::ActionHelper for turbo-rails.
4 participants