lib/tapioca/gem/pipeline.rb in tapioca-0.11.8 vs lib/tapioca/gem/pipeline.rb in tapioca-0.11.9
- old
+ new
@@ -48,10 +48,12 @@
def compile
dispatch(next_event) until @events.empty?
@root
end
+ # Events handling
+
sig { params(symbol: String).void }
def push_symbol(symbol)
@events << Gem::SymbolFound.new(symbol)
end
@@ -96,38 +98,53 @@
end
def push_method(symbol, constant, method, node, signature, parameters) # rubocop:disable Metrics/ParameterLists
@events << Gem::MethodNodeAdded.new(symbol, constant, method, node, signature, parameters)
end
+ # Constants and properties filtering
+
sig { params(symbol_name: String).returns(T::Boolean) }
def symbol_in_payload?(symbol_name)
symbol_name = symbol_name[2..-1] if symbol_name.start_with?("::")
return false unless symbol_name
@payload_symbols.include?(symbol_name)
end
+ # this looks something like:
+ # "(eval at /path/to/file.rb:123)"
+ # and we are just interested in the "/path/to/file.rb" part
+ EVAL_SOURCE_FILE_PATTERN = T.let(/\(eval at (.+):\d+\)/, Regexp)
+
sig { params(name: T.any(String, Symbol)).returns(T::Boolean) }
def constant_in_gem?(name)
return true unless Object.respond_to?(:const_source_location)
- source_location, _ = Object.const_source_location(name)
- return true unless source_location
+ source_file, _ = Object.const_source_location(name)
+ return true unless source_file
# If the source location of the constant is "(eval)", all bets are off.
- return true if source_location == "(eval)"
+ return true if source_file == "(eval)"
- gem.contains_path?(source_location)
+ # Ruby 3.3 adds automatic definition of source location for evals if
+ # `file` and `line` arguments are not provided. This results in the source
+ # file being something like `(eval at /path/to/file.rb:123)`. We try to parse
+ # this string to get the actual source file.
+ source_file = source_file.sub(EVAL_SOURCE_FILE_PATTERN, "\\1")
+
+ gem.contains_path?(source_file)
end
sig { params(method: UnboundMethod).returns(T::Boolean) }
def method_in_gem?(method)
source_location = method.source_location&.first
return false if source_location.nil?
@gem.contains_path?(source_location)
end
+ # Helpers
+
sig { params(constant: Module).returns(T.nilable(String)) }
def name_of(constant)
name = name_of_proxy_target(constant, super(class_of(constant)))
return name if name
@@ -147,10 +164,12 @@
gem_symbols = Static::SymbolLoader.gem_symbols(gem)
gem_symbols.union(engine_symbols)
end
+ # Events handling
+
sig { returns(Gem::Event) }
def next_event
T.must(@events.shift)
end
@@ -169,27 +188,21 @@
end
sig { params(event: Gem::SymbolFound).void }
def on_symbol(event)
symbol = event.symbol.delete_prefix("::")
- return if symbol_in_payload?(symbol) && !@bootstrap_symbols.include?(symbol)
+ return if skip_symbol?(symbol)
constant = constantize(symbol)
push_constant(symbol, constant) if Runtime::Reflection.constant_defined?(constant)
end
sig { params(event: Gem::ConstantFound).void.checked(:never) }
def on_constant(event)
name = event.symbol
+ return if skip_constant?(name, event.constant)
- return if name.strip.empty?
- return if name.start_with?("#<")
- return if name.downcase == name
- return if alias_namespaced?(name)
-
- return if T::Enum === event.constant # T::Enum instances are defined via `compile_enums`
-
if event.is_a?(Gem::ForeignConstantFound)
compile_foreign_constant(name, event.constant)
else
compile_constant(name, event.constant)
end
@@ -198,15 +211,21 @@
sig { params(event: Gem::NodeAdded).void }
def on_node(event)
@node_listeners.each { |listener| listener.dispatch(event) }
end
- # Compile
+ # Compiling
sig { params(symbol: String, constant: Module).void }
def compile_foreign_constant(symbol, constant)
- compile_module(symbol, constant, foreign_constant: true)
+ return if skip_foreign_constant?(symbol, constant)
+ return if seen?(symbol)
+
+ seen!(symbol)
+
+ scope = compile_scope(symbol, constant)
+ push_foreign_scope(symbol, constant, scope)
end
sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
def compile_constant(symbol, constant)
case constant
@@ -223,14 +242,13 @@
sig { params(name: String, constant: Module).void }
def compile_alias(name, constant)
return if seen?(name)
- mark_seen(name)
+ seen!(name)
- return if symbol_in_payload?(name)
- return unless constant_in_gem?(name)
+ return if skip_alias?(name, constant)
target = name_of(constant)
# If target has no name, let's make it an anonymous class or module with `Class.new` or `Module.new`
target = "#{constant.class}.new" unless target
@@ -245,14 +263,13 @@
sig { params(name: String, value: BasicObject).void.checked(:never) }
def compile_object(name, value)
return if seen?(name)
- mark_seen(name)
+ seen!(name)
- return if symbol_in_payload?(name)
- return unless constant_in_gem?(name)
+ return if skip_object?(name, value)
klass = class_of(value)
klass_name = if klass == ObjectSpace::WeakMap
sorbet_supports?(:non_generic_weak_map) ? "ObjectSpace::WeakMap" : "ObjectSpace::WeakMap[T.untyped]"
@@ -277,33 +294,33 @@
node = RBI::Const.new(name, "T.let(T.unsafe(nil), #{type_name})")
push_const(name, klass, node)
@root << node
end
- 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
+ sig { params(name: String, constant: Module).void }
+ def compile_module(name, constant)
+ return if skip_module?(name, constant)
return if seen?(name)
- mark_seen(name)
+ seen!(name)
- scope =
- if constant.is_a?(Class)
- superclass = compile_superclass(constant)
- RBI::Class.new(name, superclass_name: superclass)
- else
- RBI::Module.new(name)
- end
+ scope = compile_scope(name, constant)
+ push_scope(name, constant, scope)
+ end
- if foreign_constant
- push_foreign_scope(name, constant, scope)
+ sig { params(name: String, constant: Module).returns(RBI::Scope) }
+ def compile_scope(name, constant)
+ scope = if constant.is_a?(Class)
+ superclass = compile_superclass(constant)
+ RBI::Class.new(name, superclass_name: superclass)
else
- push_scope(name, constant, scope)
+ RBI::Module.new(name)
end
@root << scope
+
+ scope
end
sig { params(constant: T::Class[T.anything]).returns(T.nilable(String)) }
def compile_superclass(constant)
superclass = T.let(nil, T.nilable(T::Class[T.anything])) # rubocop:disable Lint/UselessAssignment
@@ -351,10 +368,58 @@
push_symbol(name)
"::#{name}"
end
+ # Constants and properties filtering
+
+ sig { params(name: String).returns(T::Boolean) }
+ def skip_symbol?(name)
+ symbol_in_payload?(name) && !@bootstrap_symbols.include?(name)
+ end
+
+ sig { params(name: String, constant: T.anything).returns(T::Boolean).checked(:never) }
+ def skip_constant?(name, constant)
+ return true if name.strip.empty?
+ return true if name.start_with?("#<")
+ return true if name.downcase == name
+ return true if alias_namespaced?(name)
+
+ return true if T::Enum === constant # T::Enum instances are defined via `compile_enums`
+
+ false
+ end
+
+ sig { params(name: String, constant: Module).returns(T::Boolean) }
+ def skip_alias?(name, constant)
+ return true if symbol_in_payload?(name)
+ return true unless constant_in_gem?(name)
+
+ false
+ end
+
+ sig { params(name: String, constant: BasicObject).returns(T::Boolean).checked(:never) }
+ def skip_object?(name, constant)
+ return true if symbol_in_payload?(name)
+ return true unless constant_in_gem?(name)
+
+ false
+ end
+
+ sig { params(name: String, constant: Module).returns(T::Boolean) }
+ def skip_foreign_constant?(name, constant)
+ Tapioca::TypeVariableModule === constant
+ end
+
+ sig { params(name: String, constant: Module).returns(T::Boolean) }
+ def skip_module?(name, constant)
+ return true unless defined_in_gem?(constant, strict: false)
+ return true if Tapioca::TypeVariableModule === constant
+
+ false
+ end
+
sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
def defined_in_gem?(constant, strict: true)
files = get_file_candidates(constant)
.merge(Runtime::Trackers::ConstantDefinition.files_for(constant))
@@ -383,17 +448,19 @@
name.start_with?(namespace)
end
end
sig { params(name: String).void }
- def mark_seen(name)
+ def seen!(name)
@seen.add(name)
end
sig { params(name: String).returns(T::Boolean) }
def seen?(name)
@seen.include?(name)
end
+
+ # Helpers
sig { params(constant: T.all(Module, T::Generic)).returns(String) }
def generic_name_of(constant)
type_name = T.must(constant.name)
return type_name if type_name =~ /\[.*\]$/