Skip to content

Commit

Permalink
Track rest, keyword rest and post parameters (#1228)
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock authored Dec 1, 2023
1 parent 5d1ce4f commit 8bf60ca
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 3 deletions.
46 changes: 43 additions & 3 deletions lib/ruby_indexer/lib/ruby_indexer/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(:"<anonymous splat>", Symbol)
end

# A keyword rest method parameter, e.g. `def foo(**a)`
class KeywordRestParameter < Parameter
DEFAULT_NAME = T.let(:"<anonymous keyword splat>", Symbol)
end

class Member < Entry
extend T::Sig
extend T::Helpers
Expand Down Expand Up @@ -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})"
Expand Down
112 changes: 112 additions & 0 deletions lib/ruby_indexer/test/method_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8bf60ca

Please sign in to comment.