# frozen_string_literal: true # typed: true class T::Private::Methods::Signature attr_reader :method, :method_name, :arg_types, :kwarg_types, :block_type, :block_name, :rest_type, :rest_name, :keyrest_type, :keyrest_name, :bind, :return_type, :mode, :req_arg_count, :req_kwarg_names, :has_rest, :has_keyrest, :check_level, :parameters, :on_failure, :override_allow_incompatible def self.new_untyped(method:, mode: T::Private::Methods::Modes.untyped, parameters: method.parameters) # Using `Untyped` ensures we'll get an error if we ever try validation on these. not_typed = T::Private::Types::NotTyped.new raw_return_type = not_typed raw_arg_types = parameters.map do |_param_kind, param_name| [param_name, not_typed] end.to_h self.new( method: method, method_name: method.name, raw_arg_types: raw_arg_types, raw_return_type: raw_return_type, bind: nil, mode: mode, check_level: :never, parameters: parameters, on_failure: nil, ) end def initialize(method:, method_name:, raw_arg_types:, raw_return_type:, bind:, mode:, check_level:, parameters: method.parameters, on_failure:, override_allow_incompatible: false) @method = method @method_name = method_name @arg_types = [] @kwarg_types = {} @block_type = nil @block_name = nil @rest_type = nil @rest_name = nil @keyrest_type = nil @keyrest_name = nil @return_type = T::Utils.coerce(raw_return_type) @bind = bind ? T::Utils.coerce(bind) : bind @mode = mode @check_level = check_level @req_arg_count = 0 @req_kwarg_names = [] @has_rest = false @has_keyrest = false @parameters = parameters @on_failure = on_failure @override_allow_incompatible = override_allow_incompatible declared_param_names = raw_arg_types.keys # If sig params are declared but there is a single parameter with a missing name # **and** the method ends with a "=", assume it is a writer method generated # by attr_writer or attr_accessor writer_method = declared_param_names != [nil] && parameters == [[:req]] && method_name[-1] == "=" # For writer methods, map the single parameter to the method name without the "=" at the end parameters = [[:req, method_name[0...-1].to_sym]] if writer_method param_names = parameters.map {|_, name| name} missing_names = param_names - declared_param_names extra_names = declared_param_names - param_names if !missing_names.empty? raise "The declaration for `#{method.name}` is missing parameter(s): #{missing_names.join(', ')}" end if !extra_names.empty? raise "The declaration for `#{method.name}` has extra parameter(s): #{extra_names.join(', ')}" end if parameters.size != raw_arg_types.size raise "The declaration for `#{method.name}` has arguments with duplicate names" end parameters.zip(raw_arg_types) do |(param_kind, param_name), (type_name, raw_type)| if type_name != param_name hint = "" # Ruby reorders params so that required keyword arguments # always precede optional keyword arguments. We can't tell # whether the culprit is the Ruby reordering or user error, so # we error but include a note if param_kind == :keyreq && parameters.any? {|k, _| k == :key} hint = "\n\nNote: Any required keyword arguments must precede any optional keyword " \ "arguments. If your method declaration matches your `def`, try reordering any " \ "optional keyword parameters to the end of the method list." end raise "Parameter `#{type_name}` is declared out of order (declared as arg number " \ "#{declared_param_names.index(type_name) + 1}, defined in the method as arg number " \ "#{param_names.index(type_name) + 1}).#{hint}\nMethod: #{method_desc}" end type = T::Utils.coerce(raw_type) case param_kind when :req if @arg_types.length > @req_arg_count # Note that this is actually is supported by Ruby, but it would add complexity to # support it here, and I'm happy to discourage its use anyway. raise "Required params after optional params are not supported in method declarations. Method: #{method_desc}" end @arg_types << [param_name, type] @req_arg_count += 1 when :opt @arg_types << [param_name, type] when :key, :keyreq @kwarg_types[param_name] = type if param_kind == :keyreq @req_kwarg_names << param_name end when :block @block_name = param_name @block_type = type when :rest @has_rest = true @rest_name = param_name @rest_type = type when :keyrest @has_keyrest = true @keyrest_name = param_name @keyrest_type = type else raise "Unexpected param_kind: `#{param_kind}`. Method: #{method_desc}" end end end attr_writer :method_name protected :method_name= def as_alias(alias_name) new_sig = clone new_sig.method_name = alias_name new_sig end def arg_count @arg_types.length end def kwarg_names @kwarg_types.keys end def owner @method.owner end def dsl_method "#{@mode}_method" end # @return [Hash] a mapping like {arg_name: [val, type], ...}, for only those args actually present. def each_args_value_type(args) # Manually split out args and kwargs based on ruby's behavior. Do not try to implement this by # getting ruby to determine the kwargs for you (e.g., by defining this method to take *args and # **kwargs). That won't work, because ruby's behavior for determining kwargs is dependent on the # the other parameters in the method definition, and our method definition here doesn't (and # can't) match the definition of the method we're validating. In addition, Ruby has a bug that # causes forwarding **kwargs to do the wrong thing: see https://bugs.ruby-lang.org/issues/10708 # and https://bugs.ruby-lang.org/issues/11860. args_length = args.length if (args_length > @req_arg_count) && (!@kwarg_types.empty? || @has_keyrest) && args[-1].is_a?(Hash) kwargs = args[-1] args_length -= 1 else kwargs = EMPTY_HASH end arg_types = @arg_types if @has_rest arg_types += [[@rest_name, @rest_type]] * (args_length - @arg_types.length) elsif (args_length < @req_arg_count) || (args_length > @arg_types.length) expected_str = @req_arg_count.to_s if @arg_types.length != @req_arg_count expected_str += "..#{@arg_types.length}" end raise ArgumentError.new("wrong number of arguments (given #{args_length}, expected #{expected_str})") end begin it = 0 while it < args_length yield arg_types[it][0], args[it], arg_types[it][1] it += 1 end end kwargs.each do |name, val| type = @kwarg_types[name] if !type && @has_keyrest type = @keyrest_type end yield name, val, type if type end end def method_desc if @method.source_location loc = @method.source_location.join(':') else loc = "" end "#{@method} at #{loc}" end EMPTY_HASH = {}.freeze end