Skip to content
48 changes: 42 additions & 6 deletions lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,17 +282,53 @@ def on_instance_variable_target_node_enter(node)

#: (Prism::CallNode node) -> void
def on_call_node_enter(node)
if @target.is_a?(MethodTarget) && (name = node.name.to_s) == @target.method_name
@references << Reference.new(
name,
node.message_loc, #: as !nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Does type checking pass without this #: as !nil?

Copy link
Author

Choose a reason for hiding this comment

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

Opps, yep something is definitely off here, will look into it. I'm a little new to the type checking 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

All good, let me know if you need any support on it.

Copy link
Member

Choose a reason for hiding this comment

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

I think we do need to cast it because message_loc can be nil. I checked out the branch and type checking is failing without the narrows.

declaration: false,
)
return unless @target.is_a?(MethodTarget)

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

private

#: (Prism::CallNode node) -> bool
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

#: (Prism::CallNode node) -> Array[String]
def unescaped_argument_names(node)
return [] if node.arguments&.arguments.nil?

node.arguments.arguments.select { |arg| arg.respond_to?(:unescaped) }.map(&:unescaped)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can node.arguments.arguments be nil?

Copy link
Member

Choose a reason for hiding this comment

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

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

end

#: (Array[String] argument_names) -> bool
def attr_reader_references?(argument_names)
argument_names.include?(@target.method_name)
end

#: (Array[String] argument_names) -> bool
def attr_writer_references?(argument_names)
argument_names.any? { |arg| "#{arg}=" == @target.method_name }
end

#: (Array[String] argument_names) -> bool
def attr_accessor_references?(argument_names)
argument_names.any? { |arg| ["#{arg}=", arg].include?(@target.method_name) }
end

#: (String name, Prism::Location location) -> void
def collect_constant_references(name, location)
return unless @target.is_a?(ConstTarget)
Expand Down
212 changes: 212 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,218 @@ 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_writer :foo, bar

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

assert_equal(2, refs.size)

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

assert_equal("foo=", refs[1].name)
assert_equal(5, refs[1].location.start_line)
refute(refs[1].declaration)
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(refs[0].declaration)

assert_equal("foo=", refs[1].name)
assert_equal(8, refs[1].location.start_line)
refute(refs[1].declaration)
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(refs[0].declaration)

assert_equal("foo", refs[1].name)
assert_equal(9, refs[1].location.start_line)
refute(refs[1].declaration)
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(refs[0].declaration)

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

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(refs[0].declaration)

assert_equal("foo", refs[1].name)
assert_equal(6, refs[1].location.start_line)
refute(refs[1].declaration)
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(refs[0].declaration)

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

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(refs[0].declaration)

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

def test_matches_attr_emtpy
ruby_code = <<~RUBY
class Bar
def foo=(value)
end

attr_reader

def baz
foo
end
end
RUBY
refs = find_method_references("foo", ruby_code)
assert_equal(1, refs.size)
assert_equal(8, refs[0].location.start_line)
refute(refs[0].declaration)
refs = find_method_references("foo=", ruby_code)
assert_equal(1, refs.size)
assert_equal(2, refs[0].location.start_line)
assert(refs[0].declaration)
refs = find_method_references("baz", ruby_code)
assert_equal(1, refs.size)
assert_equal(7, refs[0].location.start_line)
assert(refs[0].declaration)
end

def test_matches_attr_string
ruby_code = <<~RUBY
class Bar
def foo=(value)
end

attr_reader 'foo'

def baz
foo
end
end
RUBY

refs = find_method_references("foo", ruby_code)
assert_equal(2, refs.size)
assert_equal(5, refs[0].location.start_line)
assert(refs[0].declaration)
assert_equal(8, refs[1].location.start_line)
refute(refs[1].declaration)
end

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