Skip to content

Commit

Permalink
Merge pull request #903 from Shopify/unreachable-constants
Browse files Browse the repository at this point in the history
Compile RBIs for foreign constants
  • Loading branch information
Morriar authored Jun 13, 2022
2 parents 3327062 + fb9ce40 commit 2de2918
Show file tree
Hide file tree
Showing 19 changed files with 468 additions and 13 deletions.
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

sig { params(symbol: String, constant: Module).void }
def initialize(symbol, constant)
super
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

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)
# 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
# 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)
next unless mixed_in_by_gem?(locations)

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?
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)
@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

0 comments on commit 2de2918

Please sign in to comment.