class T::Private::Methods::Signature

typed: true

Constants

EMPTY_HASH

Attributes

arg_types[R]
bind[R]
block_name[R]
block_type[R]
check_level[R]
defined_raw[R]
has_keyrest[R]
has_rest[R]
keyrest_name[R]
keyrest_type[R]
kwarg_types[R]
method[R]
method_name[RW]
mode[R]
on_failure[R]
override_allow_incompatible[R]
parameters[R]
req_arg_count[R]
req_kwarg_names[R]
rest_name[R]
rest_type[R]
return_type[R]

Public Class Methods

new(method:, method_name:, raw_arg_types:, raw_return_type:, bind:, mode:, check_level:, on_failure:, parameters: method.parameters, override_allow_incompatible: false, defined_raw: false) click to toggle source
# File lib/types/private/methods/signature.rb, line 36
def initialize(method:, method_name:, raw_arg_types:, raw_return_type:, bind:, mode:, check_level:, on_failure:, parameters: method.parameters, override_allow_incompatible: false, defined_raw: 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
  @defined_raw = defined_raw

  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
new_untyped(method:, mode: T::Private::Methods::Modes.untyped, parameters: method.parameters) click to toggle source
# File lib/types/private/methods/signature.rb, line 11
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
  # Map missing parameter names to "argN" positionally
  parameters = parameters.each_with_index.map do |(param_kind, param_name), index|
    [param_kind, param_name || "arg#{index}"]
  end
  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

Public Instance Methods

arg_count() click to toggle source
# File lib/types/private/methods/signature.rb, line 143
def arg_count
  @arg_types.length
end
as_alias(alias_name) click to toggle source
# File lib/types/private/methods/signature.rb, line 137
def as_alias(alias_name)
  new_sig = clone
  new_sig.method_name = alias_name
  new_sig
end
dsl_method() click to toggle source
# File lib/types/private/methods/signature.rb, line 155
def dsl_method
  "#{@mode}_method"
end
each_args_value_type(args) { |arg_types[0], args, arg_types[1]| ... } click to toggle source

@return [Hash] a mapping like {arg_name: [val, type], …}, for only those args actually present.

# File lib/types/private/methods/signature.rb, line 160
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
kwarg_names() click to toggle source
# File lib/types/private/methods/signature.rb, line 147
def kwarg_names
  @kwarg_types.keys
end
method_desc() click to toggle source
# File lib/types/private/methods/signature.rb, line 206
def method_desc
  loc = if @method.source_location
    @method.source_location.join(':')
  else
    "<unknown location>"
  end
  "#{@method} at #{loc}"
end
owner() click to toggle source
# File lib/types/private/methods/signature.rb, line 151
def owner
  @method.owner
end