From a6d0eb69bfeb5f2adde1b5cbc2f47f8d69be4cb1 Mon Sep 17 00:00:00 2001 From: Andy Waite <13400+andyw8@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:47:34 -0400 Subject: [PATCH] Index method parameters using RBS --- .../lib/ruby_indexer/rbs_indexer.rb | 102 +++++++++++++++++- lib/ruby_indexer/test/rbs_indexer_test.rb | 86 +++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) diff --git a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb b/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb index d02f63c5d9..9eb2ee659b 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb @@ -123,7 +123,15 @@ def handle_method(member, owner) end real_owner = member.singleton? ? existing_or_new_singleton_klass(owner) : owner - @index.add(Entry::Method.new(name, file_path, location, comments, [], visibility, real_owner)) + @index.add(Entry::Method.new( + name, + file_path, + location, + comments, + build_parameters(member.overloads), + visibility, + real_owner, + )) end sig { params(owner: Entry::Namespace).returns(T.nilable(Entry::Class)) } @@ -143,5 +151,97 @@ def existing_or_new_singleton_klass(owner) @index.add(entry, skip_prefix_tree: true) entry end + + sig do + params(overloads: T::Array[RBS::AST::Members::MethodDefinition::Overload]).returns(T::Array[Entry::Parameter]) + end + def build_parameters(overloads) + parameters = {} + overloads.each do |overload| + process_overload(overload, parameters) + end + parameters.values + end + + sig do + params( + overload: RBS::AST::Members::MethodDefinition::Overload, + parameters: T::Hash[Symbol, Entry::Parameter], + ).void + end + def process_overload(overload, parameters) + function = overload.method_type.type + process_required_positionals(function, parameters) if function.required_positionals + process_optional_positionals(function, parameters) if function.optional_positionals + process_trailing_positionals(function, parameters) if function.trailing_positionals + process_required_keywords(function, parameters) if function.trailing_positionals + process_optional_keywords(function, parameters) if function.optional_keywords + process_rest_keywords(function, parameters) if function.rest_keywords + end + + sig { params(function: RBS::Types::Function, parameters: T::Hash[Symbol, Entry::Parameter]).void } + def process_required_positionals(function, parameters) + function.required_positionals.each do |param| + name = param.name + + next unless name + + parameters[name] = Entry::RequiredParameter.new(name: name) + end + optional_argument_names = parameters.keys - function.required_positionals.map(&:name) + optional_argument_names.each do |optional_argument_name| + parameters[optional_argument_name] = Entry::OptionalParameter.new(name: optional_argument_name) + end + end + + sig { params(function: RBS::Types::Function, parameters: T::Hash[Symbol, Entry::Parameter]).void } + def process_optional_positionals(function, parameters) + function.optional_positionals.each do |param| + name = param.name + next unless name + + parameters[name] = Entry::OptionalParameter.new(name: name) + end + rest = function.rest_positionals + + if rest + rest_name = rest.name || Entry::RestParameter::DEFAULT_NAME + return if rest_name == :selector_0 + + parameters[rest_name] = Entry::RestParameter.new(name: rest_name) + end + end + + sig { params(function: RBS::Types::Function, parameters: T::Hash[Symbol, Entry::Parameter]).void } + def process_trailing_positionals(function, parameters) + function.trailing_positionals.each do |param| + name = param.name + parameters[name] = Entry::OptionalParameter.new(name: param.name) + end + end + + sig { params(function: RBS::Types::Function, parameters: T::Hash[Symbol, Entry::Parameter]).void } + def process_required_keywords(function, parameters) + function.required_keywords.each do |param| + name = param.first + parameters[name] = Entry::KeywordParameter.new(name: name) + end + end + + sig { params(function: RBS::Types::Function, parameters: T::Hash[Symbol, Entry::Parameter]).void } + def process_optional_keywords(function, parameters) + function.optional_keywords.each do |param| + name = param.first.to_s.to_sym # hack + parameters[name] = Entry::OptionalKeywordParameter.new(name: name) + end + end + + sig { params(function: RBS::Types::Function, parameters: T::Hash[Symbol, Entry::Parameter]).void } + def process_rest_keywords(function, parameters) + keyword_rest = function.rest_keywords + + keyword_rest_name = keyword_rest.name || Entry::KeywordRestParameter::DEFAULT_NAME + parameters[keyword_rest_name] = Entry::KeywordRestParameter.new(name: keyword_rest_name) + end end end diff --git a/lib/ruby_indexer/test/rbs_indexer_test.rb b/lib/ruby_indexer/test/rbs_indexer_test.rb index a73653b962..642088eb74 100644 --- a/lib/ruby_indexer/test/rbs_indexer_test.rb +++ b/lib/ruby_indexer/test/rbs_indexer_test.rb @@ -55,6 +55,92 @@ def test_index_methods assert_operator(entry.location.end_column, :>, 0) end + def test_rbs_method_with_required_positionals + entries = @index["crypt"] # https://rubyapi.org/3.3/o/string#method-i-crypt + assert_equal(1, entries.length) + + entry = entries.first + parameters = entry.parameters + + assert_equal(1, parameters.length) + assert_kind_of(Entry::RequiredParameter, parameters[0]) + assert_equal(:salt_str, parameters[0].name) + end + + def test_rbs_method_with_optional_parameter + entries = @index["chomp"] # https://rubyapi.org/3.3/o/string#method-i-chomp + assert_equal(1, entries.length) + + entry = entries.first + parameters = entry.parameters + + assert_equal(1, parameters.length) + assert_kind_of(Entry::OptionalParameter, parameters[0]) + assert_equal(:separator, parameters[0].name) + end + + def test_rbs_method_with_required_and_optional_parameters + entries = @index["gsub"] # https://rubyapi.org/3.3/o/string#method-i-gsub + assert_equal(1, entries.length) + + entry = entries.first + + parameters = entry.parameters + + assert_equal(2, parameters.length) + assert_kind_of(Entry::RequiredParameter, parameters[0]) + assert_kind_of(Entry::OptionalParameter, parameters[1]) + assert_equal(:pattern, parameters[0].name) + assert_equal(:replacement, parameters[1].name) + end + + def test_rbs_method_with_rest_positionals + entries = @index["count"] # https://rubyapi.org/3.3/o/string#method-i-count + entry = entries.find { |entry| entry.owner.name == "String" } + + parameters = entry.parameters + + # TODO: In RBS, this is represented as having two arguments: + # + # def count: (selector selector_0, *selector more_selectors) -> Integer + # + # but perhaps that is confusing? + assert_equal(2, parameters.length) + assert_kind_of(RubyIndexer::Entry::RequiredParameter, parameters[0]) + assert_kind_of(RubyIndexer::Entry::RestParameter, parameters[1]) + end + + def test_rbs_method_with_trailing_positionals + entries = @index["select"] # https://rubyapi.org/3.3/o/io#method-c-select + entry = entries.find { |entry| entry.owner.name == "IO::" } + + parameters = entry.parameters + + assert_equal(4, parameters.length) + assert_kind_of(Entry::RequiredParameter, parameters[0]) + assert_kind_of(Entry::OptionalParameter, parameters[1]) + assert_kind_of(Entry::OptionalParameter, parameters[2]) + assert_kind_of(Entry::OptionalParameter, parameters[3]) + end + + def test_rbs_method_with_optional_keywords + entries = @index["step"] # https://rubyapi.org/3.3/o/numeric#method-i-step + entry = entries.find { |entry| entry.owner.name == "Numeric" } + + parameters = entry.parameters + + assert_equal(4, parameters.length) + assert_equal([:limit, :step, :by, :to], parameters.map(&:name)) + assert_kind_of(Entry::OptionalParameter, parameters[0]) + assert_kind_of(Entry::OptionalParameter, parameters[1]) + assert_kind_of(Entry::OptionalKeywordParameter, parameters[2]) + assert_kind_of(Entry::OptionalKeywordParameter, parameters[3]) + end + + def test_rbs_method_with_required_keywords + # Investigating if there are any methods in Core for this + end + def test_attaches_correct_owner_to_singleton_methods entries = @index["basename"] refute_nil(entries)