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

Compile RBIs for foreign constants #903

Merged
merged 4 commits into from
Jun 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/tapioca/gem/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ def initialize(symbol, constant)
end
end

class ForeignConstantFound < ConstantFound
extend T::Sig

sig { override.returns(Module) }
def constant
T.cast(@constant, Module)
end
egiurleo marked this conversation as resolved.
Show resolved Hide resolved

sig { params(symbol: String, constant: Module).void }
def initialize(symbol, constant)
super
egiurleo marked this conversation as resolved.
Show resolved Hide resolved
end
end

class NodeAdded < Event
extend T::Helpers
extend T::Sig
Expand Down Expand Up @@ -88,6 +102,8 @@ def initialize(symbol, constant, node)
end
end

class ForeignScopeNodeAdded < ScopeNodeAdded; end
egiurleo marked this conversation as resolved.
Show resolved Hide resolved

class MethodNodeAdded < NodeAdded
extend T::Sig

Expand Down
1 change: 1 addition & 0 deletions lib/tapioca/gem/listeners.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
require "tapioca/gem/listeners/sorbet_signatures"
require "tapioca/gem/listeners/sorbet_type_variables"
require "tapioca/gem/listeners/subconstants"
require "tapioca/gem/listeners/foreign_constants"
require "tapioca/gem/listeners/yard_doc"
11 changes: 11 additions & 0 deletions lib/tapioca/gem/listeners/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def initialize(pipeline)

sig { params(event: NodeAdded).void }
def dispatch(event)
return if ignore?(event)

case event
when ConstNodeAdded
on_const(event)
Expand All @@ -42,6 +44,15 @@ def on_scope(event)
sig { params(event: MethodNodeAdded).void }
def on_method(event)
end

sig { params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Morriar I have changed this method to ignore? and reversed the logic, but the base class still ignores nothing.

# Some listeners do not have to take any action on certain events. For example,
# almost every listener should skip ForeignScopeNodeAdded events in order not to generate
# unnecessary RBIs for foreign constants. This method should be overridden by listener
# subclasses to skip any events that aren't relevant to them.
false
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/dynamic_mixins.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def on_scope(event)
@pipeline.push_symbol(name) if name
end
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
86 changes: 86 additions & 0 deletions lib/tapioca/gem/listeners/foreign_constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# typed: strict
# frozen_string_literal: true

module Tapioca
module Gem
module Listeners
class ForeignConstants < Base
extend T::Sig

include Runtime::Reflection

private

sig { override.params(event: ScopeNodeAdded).void }
def on_scope(event)
mixin = event.constant
return if Class === mixin # Classes can't be mixed into other constants

# There are cases where we want to process constants not declared by the current
egiurleo marked this conversation as resolved.
Show resolved Hide resolved
# gem, i.e. "foreign constant". These are constants defined in another gem to which
# this gem is applying a mix-in. This pattern is especially common for gems that add
# behavior to Ruby standard library classes by mixing in modules to them.
#
# 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, locations|
next if defined_by_application?(constant)
egiurleo marked this conversation as resolved.
Show resolved Hide resolved
next unless mixed_in_by_gem?(locations)
egiurleo marked this conversation as resolved.
Show resolved Hide resolved

name = @pipeline.name_of(constant)

# Calling Tapioca::Gem::Pipeline#name_of on a singleton class returns `nil`.
# To handle this case, use string parsing to get the name of the singleton class's
# 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?
egiurleo marked this conversation as resolved.
Show resolved Hide resolved
name = constant_name_from_singleton_class(constant)
next unless name

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

@pipeline.push_foreign_constant(name, constant) if name
end
end

sig do
params(
locations: T::Array[String]
).returns(T::Boolean)
end
def mixed_in_by_gem?(locations)
locations.compact.any? { |location| @pipeline.gem.contains_path?(location) }
end

sig do
params(
constant: Module
).returns(T::Boolean)
end
def defined_by_application?(constant)
application_dir = (Bundler.default_gemfile / "..").to_s
Tapioca::Runtime::Trackers::ConstantDefinition.files_for(constant).any? do |location|
location.start_with?(application_dir) && !in_bundle_path?(location)
end
end

sig { params(path: String).returns(T::Boolean) }
def in_bundle_path?(path)
path.start_with?(Bundler.bundle_path.to_s, Bundler.app_cache.to_s)
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 { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ def initialize_method_for(constant)
rescue
nil
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class RemoveEmptyPayloadScopes < Base
def on_scope(event)
event.node.detach if @pipeline.symbol_in_payload?(event.symbol) && event.node.empty?
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/sorbet_enums.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ def on_scope(event)

event.node << RBI::TEnumBlock.new(enums)
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/sorbet_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ def on_scope(event)
node << RBI::Helper.new("final") if T::Private::Final.final_module?(constant)
node << RBI::Helper.new("sealed") if T::Private::Sealed.sealed_module?(constant)
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/sorbet_props.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ def on_scope(event)
end
end
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/sorbet_required_ancestors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def on_scope(event)
event.node << RBI::RequiresAncestor.new(ancestor.to_s)
end
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/sorbet_signatures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ def signature_final?(signature)

final_methods.include?(signature.method_name)
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/sorbet_type_variables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ def compile_type_variable_declarations(tree, constant)

tree << RBI::Extend.new("T::Generic")
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/subconstants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def on_scope(event)
@pipeline.push_constant(name, subconstant)
end
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/listeners/yard_doc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ def documentation_comments(name, sigs: [])

comments
end

sig { override.params(event: NodeAdded).returns(T::Boolean) }
def ignore?(event)
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
end
end
end
end
Expand Down
45 changes: 37 additions & 8 deletions lib/tapioca/gem/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def initialize(gem, include_doc: false)
@node_listeners << Gem::Listeners::SorbetSignatures.new(self)
@node_listeners << Gem::Listeners::Subconstants.new(self)
@node_listeners << Gem::Listeners::YardDoc.new(self) if include_doc
@node_listeners << Gem::Listeners::ForeignConstants.new(self)
egiurleo marked this conversation as resolved.
Show resolved Hide resolved
@node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
end

Expand All @@ -60,16 +61,30 @@ def push_constant(symbol, constant)
@events << Gem::ConstantFound.new(symbol, constant)
end

sig { params(symbol: String, constant: Module).void.checked(:never) }
def push_foreign_constant(symbol, constant)
@events << Gem::ForeignConstantFound.new(symbol, constant)
end

sig { params(symbol: String, constant: Module, node: RBI::Const).void.checked(:never) }
def push_const(symbol, constant, node)
@events << Gem::ConstNodeAdded.new(symbol, constant, node)
end

sig { params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) }
sig do
params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
end
def push_scope(symbol, constant, node)
@events << Gem::ScopeNodeAdded.new(symbol, constant, node)
end

sig do
params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
end
def push_foreign_scope(symbol, constant, node)
@events << Gem::ForeignScopeNodeAdded.new(symbol, constant, node)
end

sig do
params(
symbol: String,
Expand Down Expand Up @@ -152,11 +167,15 @@ def on_constant(event)
return if alias_namespaced?(name)
return if seen?(name)

constant = event.constant
return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
return if T::Enum === event.constant # T::Enum instances are defined via `compile_enums`

mark_seen(name)
compile_constant(name, constant)

if event.is_a?(Gem::ForeignConstantFound)
compile_foreign_constant(name, event.constant)
else
compile_constant(name, event.constant)
end
end

sig { params(event: Gem::NodeAdded).void }
Expand All @@ -166,6 +185,11 @@ def on_node(event)

# Compile

sig { params(symbol: String, constant: Module).void }
def compile_foreign_constant(symbol, constant)
compile_module(symbol, constant, foreign_constant: true)
end

sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
def compile_constant(symbol, constant)
case constant
Expand Down Expand Up @@ -228,9 +252,9 @@ def compile_object(name, value)
@root << node
end

sig { params(name: String, constant: Module).void }
def compile_module(name, constant)
return unless defined_in_gem?(constant, strict: false)
sig { params(name: String, constant: Module, foreign_constant: T::Boolean).void }
def compile_module(name, constant, foreign_constant: false)
return unless defined_in_gem?(constant, strict: false) || foreign_constant
return if Tapioca::TypeVariableModule === constant

scope =
Expand All @@ -241,7 +265,12 @@ def compile_module(name, constant)
RBI::Module.new(name)
end

push_scope(name, constant, scope)
if foreign_constant
push_foreign_scope(name, constant, scope)
else
push_scope(name, constant, scope)
end

@root << scope
end

Expand Down
Loading