Class: RubyIndexer::Entry::Signature

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_indexer/lib/ruby_indexer/entry.rb

Overview

Ruby doesn’t support method overloading, so a method will have only one signature. However RBS can represent the concept of method overloading, with different return types based on the arguments passed, so we need to store all the signatures.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parameters) ⇒ Signature

: (Array parameters) -> void



493
494
495
# File 'lib/ruby_indexer/lib/ruby_indexer/entry.rb', line 493

def initialize(parameters)
  @parameters = parameters
end

Instance Attribute Details

#parametersObject (readonly)

: Array



490
491
492
# File 'lib/ruby_indexer/lib/ruby_indexer/entry.rb', line 490

def parameters
  @parameters
end

Instance Method Details

#formatObject

Returns a string with the decorated names of the parameters of this member. E.g.: ‘(a, b = 1, c: 2)` : -> String



499
500
501
# File 'lib/ruby_indexer/lib/ruby_indexer/entry.rb', line 499

def format
  @parameters.map(&:decorated_name).join(", ")
end

#keyword_arguments_match?(args, names) ⇒ Boolean

: (Array? args, Array names) -> bool



584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/ruby_indexer/lib/ruby_indexer/entry.rb', line 584

def keyword_arguments_match?(args, names)
  return true unless args
  return true if args.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }

  arg_names = args.filter_map do |arg|
    next unless arg.is_a?(Prism::AssocNode)

    key = arg.key
    key.value&.to_sym if key.is_a?(Prism::SymbolNode)
  end

  (arg_names - names).empty?
end

#matches?(arguments) ⇒ Boolean

Returns ‘true` if the given call node arguments array matches this method signature. This method will prefer returning `true` for situations that cannot be analyzed statically, like the presence of splats, keyword splats or forwarding arguments.

Since this method is used to detect which overload should be displayed in signature help, it will also return ‘true` if there are missing arguments since the user may not be done typing yet. For example:

“‘ruby def foo(a, b); end # All of the following are considered matches because the user might be in the middle of typing and we have to # show them the signature foo foo(1) foo(1, 2) “` : (Array arguments) -> bool



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# File 'lib/ruby_indexer/lib/ruby_indexer/entry.rb', line 519

def matches?(arguments)
  min_pos = 0
  max_pos = 0 #: (Integer | Float)
  names = []
  has_forward = false #: bool
  has_keyword_rest = false #: bool

  @parameters.each do |param|
    case param
    when RequiredParameter
      min_pos += 1
      max_pos += 1
    when OptionalParameter
      max_pos += 1
    when RestParameter
      max_pos = Float::INFINITY
    when ForwardingParameter
      max_pos = Float::INFINITY
      has_forward = true
    when KeywordParameter, OptionalKeywordParameter
      names << param.name
    when KeywordRestParameter
      has_keyword_rest = true
    end
  end

  keyword_hash_nodes, positional_args = arguments.partition { |arg| arg.is_a?(Prism::KeywordHashNode) }
  keyword_args = keyword_hash_nodes.first #: as Prism::KeywordHashNode?
    &.elements
  forwarding_arguments, positionals = positional_args.partition do |arg|
    arg.is_a?(Prism::ForwardingArgumentsNode)
  end

  return true if has_forward && min_pos == 0

  # If the only argument passed is a forwarding argument, then anything will match
  (positionals.empty? && forwarding_arguments.any?) ||
    (
      # Check if positional arguments match. This includes required, optional, rest arguments. We also need to
      # verify if there's a trailing forwarding argument, like `def foo(a, ...); end`
      positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) &&
      # If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest
      # arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest
      # (**kwargs), then we can't analyze statically because the user could be passing a hash and we don't know
      # what the runtime values inside the hash are.
      #
      # If none of those match, then we verify if the user is passing the expect names for the keyword arguments
      (has_forward || has_keyword_rest || keyword_arguments_match?(keyword_args, names))
    )
end

#positional_arguments_match?(positional_args, forwarding_arguments, keyword_args, min_pos, max_pos) ⇒ Boolean

: (Array positional_args, Array forwarding_arguments, Array? keyword_args, Integer min_pos, (Integer | Float) max_pos) -> bool



571
572
573
574
575
576
577
578
579
580
581
# File 'lib/ruby_indexer/lib/ruby_indexer/entry.rb', line 571

def positional_arguments_match?(positional_args, forwarding_arguments, keyword_args, min_pos, max_pos)
  # If the method accepts at least one positional argument and a splat has been passed
  (min_pos > 0 && positional_args.any? { |arg| arg.is_a?(Prism::SplatNode) }) ||
    # If there's at least one positional argument unaccounted for and a keyword splat has been passed
    (min_pos - positional_args.length > 0 && keyword_args&.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }) ||
    # If there's at least one positional argument unaccounted for and a forwarding argument has been passed
    (min_pos - positional_args.length > 0 && forwarding_arguments.any?) ||
    # If the number of positional arguments is within the expected range
    (min_pos > 0 && positional_args.length <= max_pos) ||
    (min_pos == 0 && positional_args.empty?)
end