Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track rest, keyword rest and post parameters #1228

Merged
merged 1 commit into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
vinistock marked this conversation as resolved.
Show resolved Hide resolved
DEFAULT_NAME = T.let(:"<anonymous splat>", Symbol)
end

# A keyword rest method parameter, e.g. `def foo(**a)`
class KeywordRestParameter < Parameter
Morriar marked this conversation as resolved.
Show resolved Hide resolved
vinistock marked this conversation as resolved.
Show resolved Hide resolved
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)
vinistock marked this conversation as resolved.
Show resolved Hide resolved
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
Loading