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 =~ /\[.*\]$/