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

Adds support for attr_* method references #2848

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
40 changes: 39 additions & 1 deletion lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,51 @@ def on_def_node_leave(node)

sig { params(node: Prism::CallNode).void }
def on_call_node_enter(node)
if @target.is_a?(MethodTarget) && (name = node.name.to_s) == @target.method_name
return unless @target.is_a?(MethodTarget)

if (name = node.name.to_s) == @target.method_name
@references << Reference.new(name, T.must(node.message_loc), declaration: false)
elsif attr_method_references?(node)
@references << Reference.new(@target.method_name, T.must(node.message_loc), declaration: true)
end
end

private

sig { params(node: Prism::CallNode).returns(T::Boolean) }
def attr_method_references?(node)
case node.name
when :attr_reader
attr_reader_references?(unescaped_argument_names(node))
when :attr_writer
attr_writer_references?(unescaped_argument_names(node))
when :attr_accessor
attr_accessor_references?(unescaped_argument_names(node))
else
false
end
end

sig { params(node: Prism::CallNode).returns(T::Array[String]) }
def unescaped_argument_names(node)
node.arguments.arguments.select { |arg| arg.respond_to?(:unescaped) }.map(&:unescaped)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the middle of typing, we may find empty arguments, so we have to handle that. Also, we need to account for symbol and string nodes as arguments.

Suggested change
node.arguments.arguments.select { |arg| arg.respond_to?(:unescaped) }.map(&:unescaped)
arguments = node.arguments.arguments
return [] unless arguments
arguments.filter_map do |arg|
case arg
when Prism::StringNode
arg.unescaped
when Prism::SymbolNode
arg.value
end
end

end

sig { params(argument_names: T::Array[String]).returns(T::Boolean) }
def attr_reader_references?(argument_names)
argument_names.include?(@target.method_name)
end

sig { params(argument_names: T::Array[String]).returns(T::Boolean) }
def attr_writer_references?(argument_names)
argument_names.any? { |arg| "#{arg}=" == @target.method_name }
end

sig { params(argument_names: T::Array[String]).returns(T::Boolean) }
def attr_accessor_references?(argument_names)
argument_names.any? { |arg| "#{arg}=" == @target.method_name || arg == @target.method_name }
end

sig { params(name: String).returns(T::Array[String]) }
def actual_nesting(name)
nesting = @stack + [name]
Expand Down
146 changes: 146 additions & 0 deletions lib/ruby_indexer/test/reference_finder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,152 @@ def baz
assert_equal(9, refs[1].location.start_line)
end

def test_matches_attr_writer_with_call_node_argument
refs = find_method_references("foo=", <<~RUBY)
class Bar
attr_reader :foo, bar
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be attr_writer or attr_accessor?


def baz
self.foo = 1
self.foo
end
end
RUBY

assert_equal(1, refs.size)

assert_equal("foo=", refs[0].name)
assert_equal(5, refs[0].location.start_line)
end

def test_matches_attr_writer
refs = find_method_references("foo=", <<~RUBY)
class Bar
def foo
end

attr_writer :foo

def baz
self.foo = 1
self.foo
end
end
RUBY

# We want to match `foo=` but not `foo`
assert_equal(2, refs.size)

assert_equal("foo=", refs[0].name)
assert_equal(5, refs[0].location.start_line)

assert_equal("foo=", refs[1].name)
assert_equal(8, refs[1].location.start_line)
end

def test_matches_attr_reader
refs = find_method_references("foo", <<~RUBY)
class Bar
def foo=(value)
end

attr_reader :foo

def baz
self.foo = 1
self.foo
end
end
RUBY

# We want to match `foo=` but not `foo`
assert_equal(2, refs.size)

assert_equal("foo", refs[0].name)
assert_equal(5, refs[0].location.start_line)

assert_equal("foo", refs[1].name)
assert_equal(9, refs[1].location.start_line)
end

def test_matches_attr_accessor
refs = find_method_references("foo=", <<~RUBY)
class Bar
attr_accessor :foo

def baz
self.foo = 1
self.foo
end
end
RUBY

# We want to match `foo=` but not `foo`
assert_equal(2, refs.size)

assert_equal("foo=", refs[0].name)
assert_equal(2, refs[0].location.start_line)

assert_equal("foo=", refs[1].name)
assert_equal(5, refs[1].location.start_line)

refs = find_method_references("foo", <<~RUBY)
class Bar
attr_accessor :foo

def baz
self.foo = 1
self.foo
end
end
RUBY

assert_equal("foo", refs[0].name)
assert_equal(2, refs[0].location.start_line)

assert_equal("foo", refs[1].name)
assert_equal(6, refs[1].location.start_line)
end

def test_matches_attr_accessor_multi
refs = find_method_references("foo=", <<~RUBY)
class Bar
attr_accessor :bar, :foo

def baz
self.foo = 1
self.foo
end
end
RUBY

# We want to match `foo=` but not `foo`
assert_equal(2, refs.size)

assert_equal("foo=", refs[0].name)
assert_equal(2, refs[0].location.start_line)

assert_equal("foo=", refs[1].name)
assert_equal(5, refs[1].location.start_line)

refs = find_method_references("foo", <<~RUBY)
class Bar
attr_accessor :bar, :foo

def baz
self.foo = 1
self.foo
end
end
RUBY

assert_equal("foo", refs[0].name)
assert_equal(2, refs[0].location.start_line)

assert_equal("foo", refs[1].name)
assert_equal(6, refs[1].location.start_line)
end

def test_find_inherited_methods
refs = find_method_references("foo", <<~RUBY)
class Bar
Expand Down
Loading