diff --git a/lib/ruby_indexer/lib/ruby_indexer/entry.rb b/lib/ruby_indexer/lib/ruby_indexer/entry.rb index a8ce1b988..7fa84fa8a 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/entry.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/entry.rb @@ -106,6 +106,16 @@ class KeywordParameter < Parameter class OptionalKeywordParameter < Parameter end + # A rest method parameter, e.g. `def foo(*a)` + class RestParameter < Parameter + DEFAULT_NAME = T.let(:"", Symbol) + end + + # A keyword rest method parameter, e.g. `def foo(**a)` + class KeywordRestParameter < Parameter + DEFAULT_NAME = T.let(:"", Symbol) + end + class Member < Entry extend T::Sig extend T::Helpers @@ -203,17 +213,47 @@ def list_params(parameters_node) end end + rest = parameters_node.rest + + if rest + rest_name = rest.name || RestParameter::DEFAULT_NAME + parameters << RestParameter.new(name: rest_name) + end + + keyword_rest = parameters_node.keyword_rest + + if keyword_rest.is_a?(Prism::KeywordRestParameterNode) + keyword_rest_name = parameter_name(keyword_rest) || KeywordRestParameter::DEFAULT_NAME + parameters << KeywordRestParameter.new(name: keyword_rest_name) + end + + parameters_node.posts.each do |post| + name = parameter_name(post) + next unless name + + parameters << RequiredParameter.new(name: name) + end + parameters end - sig { params(node: Prism::Node).returns(T.nilable(Symbol)) } + sig { params(node: T.nilable(Prism::Node)).returns(T.nilable(Symbol)) } def parameter_name(node) case node when Prism::RequiredParameterNode, Prism::OptionalParameterNode, - Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode + Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode, + Prism::RestParameterNode, Prism::KeywordRestParameterNode node.name when Prism::MultiTargetNode - names = [*node.lefts, *node.rest, *node.rights].map { |parameter_node| parameter_name(parameter_node) } + names = node.lefts.map { |parameter_node| parameter_name(parameter_node) } + + rest = node.rest + if rest.is_a?(Prism::SplatNode) + name = rest.expression&.slice + names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym) + end + + names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) }) names_with_commas = names.join(", ") :"(#{names_with_commas})" diff --git a/lib/ruby_indexer/test/method_test.rb b/lib/ruby_indexer/test/method_test.rb index eca5fdbc9..a4342773a 100644 --- a/lib/ruby_indexer/test/method_test.rb +++ b/lib/ruby_indexer/test/method_test.rb @@ -106,6 +106,118 @@ def bar(a:, b: 123) assert_instance_of(Entry::OptionalKeywordParameter, b) end + def test_method_with_rest_and_keyword_rest_parameters + index(<<~RUBY) + class Foo + def bar(*a, **b) + end + end + RUBY + + assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") + entry = T.must(@index["bar"].first) + assert_equal(2, entry.parameters.length) + a, b = entry.parameters + + assert_equal(:a, a.name) + assert_instance_of(Entry::RestParameter, a) + + assert_equal(:b, b.name) + assert_instance_of(Entry::KeywordRestParameter, b) + end + + def test_method_with_post_parameters + index(<<~RUBY) + class Foo + def bar(*a, b) + end + + def baz(**a, b) + end + + def qux(*a, (b, c)) + end + RUBY + + assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") + entry = T.must(@index["bar"].first) + assert_equal(2, entry.parameters.length) + a, b = entry.parameters + + assert_equal(:a, a.name) + assert_instance_of(Entry::RestParameter, a) + + assert_equal(:b, b.name) + assert_instance_of(Entry::RequiredParameter, b) + + entry = T.must(@index["baz"].first) + assert_equal(2, entry.parameters.length) + a, b = entry.parameters + + assert_equal(:a, a.name) + assert_instance_of(Entry::KeywordRestParameter, a) + + assert_equal(:b, b.name) + assert_instance_of(Entry::RequiredParameter, b) + + entry = T.must(@index["qux"].first) + assert_equal(2, entry.parameters.length) + _a, second = entry.parameters + + assert_equal(:"(b, c)", second.name) + assert_instance_of(Entry::RequiredParameter, second) + end + + def test_method_with_destructured_rest_parameters + index(<<~RUBY) + class Foo + def bar((a, *b)) + end + end + RUBY + + assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") + entry = T.must(@index["bar"].first) + assert_equal(1, entry.parameters.length) + param = entry.parameters.first + + assert_equal(:"(a, *b)", param.name) + assert_instance_of(Entry::RequiredParameter, param) + end + + def test_method_with_anonymous_rest_parameters + index(<<~RUBY) + class Foo + def bar(*, **) + end + end + RUBY + + assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") + entry = T.must(@index["bar"].first) + assert_equal(2, entry.parameters.length) + first, second = entry.parameters + + assert_equal(Entry::RestParameter::DEFAULT_NAME, first.name) + assert_instance_of(Entry::RestParameter, first) + + assert_equal(Entry::KeywordRestParameter::DEFAULT_NAME, second.name) + assert_instance_of(Entry::KeywordRestParameter, second) + end + + def test_method_with_forbidden_keyword_splat_parameter + index(<<~RUBY) + class Foo + def bar(**nil) + end + end + RUBY + + assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") + entry = T.must(@index["bar"].first) + assert_empty(entry.parameters) + end + def test_keeps_track_of_method_owner index(<<~RUBY) class Foo