# typed: strict # frozen_string_literal: true module Tapioca module Gem module Listeners class Methods < Base extend T::Sig include RBIHelper include Runtime::Reflection private sig { override.params(event: ScopeNodeAdded).void } def on_scope(event) symbol = event.symbol constant = event.constant node = event.node compile_method(node, symbol, constant, initialize_method_for(constant)) compile_directly_owned_methods(node, symbol, constant) compile_directly_owned_methods(node, symbol, singleton_class_of(constant)) end sig do params( tree: RBI::Tree, module_name: String, mod: Module, for_visibility: T::Array[Symbol] ).void end def compile_directly_owned_methods(tree, module_name, mod, for_visibility = [:public, :protected, :private]) method_names_by_visibility(mod) .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) } .each do |visibility, method_list| method_list.sort!.map do |name| next if name == :initialize vis = case visibility when :protected RBI::Protected.new when :private RBI::Private.new else RBI::Public.new end compile_method(tree, module_name, mod, mod.instance_method(name), vis) end end end sig do params( tree: RBI::Tree, symbol_name: String, constant: Module, method: T.nilable(UnboundMethod), visibility: RBI::Visibility ).void end def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public.new) return unless method return unless method_owned_by_constant?(method, constant) return if @pipeline.symbol_in_payload?(symbol_name) && !@pipeline.method_in_gem?(method) signature = signature_of(method) method = T.let(signature.method, UnboundMethod) if signature method_name = method.name.to_s return unless valid_method_name?(method_name) return if struct_method?(constant, method_name) return if method_name.start_with?("__t_props_generated_") parameters = T.let(method.parameters, T::Array[[Symbol, T.nilable(Symbol)]]) sanitized_parameters = parameters.each_with_index.map do |(type, name), index| fallback_arg_name = "_arg#{index}" name = if name name.to_s else # For attr_writer methods, Sorbet signatures have the name # of the method (without the trailing = sign) as the name of # the only parameter. So, if the parameter does not have a name # then the replacement name should be the name of the method # (minus trailing =) if and only if there is a signature for the # method and the parameter is required and there is a single # parameter and the signature also defines a single parameter and # the name of the method ends with a = character. writer_method_with_sig = ( signature && type == :req && parameters.size == 1 && signature.arg_types.size == 1 && method_name[-1] == "=" ) if writer_method_with_sig method_name.delete_suffix("=") else fallback_arg_name end end # Sanitize param names name = fallback_arg_name unless valid_parameter_name?(name) [type, name] end rbi_method = RBI::Method.new( method_name, is_singleton: constant.singleton_class?, visibility: visibility ) sanitized_parameters.each do |type, name| case type when :req rbi_method << RBI::Param.new(name) when :opt rbi_method << RBI::OptParam.new(name, "T.unsafe(nil)") when :rest rbi_method << RBI::RestParam.new(name) when :keyreq rbi_method << RBI::KwParam.new(name) when :key rbi_method << RBI::KwOptParam.new(name, "T.unsafe(nil)") when :keyrest rbi_method << RBI::KwRestParam.new(name) when :block rbi_method << RBI::BlockParam.new(name) end end @pipeline.push_method(symbol_name, constant, rbi_method, signature, sanitized_parameters) tree << rbi_method end # Check whether the method is defined by the constant. # # In most cases, it works to check that the constant is the method owner. However, # in the case that a method is also defined in a module prepended to the constant, it # will be owned by the prepended module, not the constant. # # This method implements a better way of checking whether a constant defines a method. # It walks up the ancestor tree via the `super_method` method; if any of the super # methods are owned by the constant, it means that the constant declares the method. sig { params(method: UnboundMethod, constant: Module).returns(T::Boolean) } def method_owned_by_constant?(method, constant) # Widen the type of `method` to be nilable method = T.let(method, T.nilable(UnboundMethod)) while method return true if method.owner == constant method = method.super_method end false end sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) } def method_names_by_visibility(mod) { public: public_instance_methods_of(mod), protected: protected_instance_methods_of(mod), private: private_instance_methods_of(mod), } end sig { params(constant: Module, method_name: String).returns(T::Boolean) } def struct_method?(constant, method_name) return false unless T::Props::ClassMethods === constant constant .props .keys .include?(method_name.gsub(/=$/, "").to_sym) end sig { params(constant: Module).returns(T.nilable(UnboundMethod)) } def initialize_method_for(constant) constant.instance_method(:initialize) rescue nil end sig { override.params(event: NodeAdded).returns(T::Boolean) } def ignore?(event) event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) end end end end end