diff --git a/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb b/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb index e521a4d070..d7564a882f 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb @@ -85,7 +85,6 @@ def initialize(target, index, dispatcher, uri, include_declarations: true) :on_constant_path_or_write_node_enter, :on_constant_path_operator_write_node_enter, :on_constant_path_and_write_node_enter, - :on_constant_or_write_node_enter, :on_constant_path_node_enter, :on_constant_read_node_enter, :on_constant_write_node_enter, diff --git a/lib/ruby_indexer/test/reference_finder_test.rb b/lib/ruby_indexer/test/reference_finder_test.rb index ed5028d5af..3a394cf67b 100644 --- a/lib/ruby_indexer/test/reference_finder_test.rb +++ b/lib/ruby_indexer/test/reference_finder_test.rb @@ -27,6 +27,21 @@ class Bar assert_equal(8, refs[2].location.start_line) end + def test_does_not_duplicate_references_for_or_write_constants + refs = find_const_references("Foo::BAR", <<~RUBY) + module Foo + BAR ||= 1 + BAR + end + RUBY + + assert_equal(2, refs.size) + assert_equal("BAR", refs[0].name) + assert_equal(2, refs[0].location.start_line) + assert_equal("BAR", refs[1].name) + assert_equal(3, refs[1].location.start_line) + end + def test_finds_constant_references_inside_singleton_contexts refs = find_const_references("Foo::::Bar", <<~RUBY) class Foo diff --git a/lib/ruby_lsp/requests/references.rb b/lib/ruby_lsp/requests/references.rb index f400224a47..b988b9becb 100644 --- a/lib/ruby_lsp/requests/references.rb +++ b/lib/ruby_lsp/requests/references.rb @@ -32,6 +32,11 @@ def perform Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode, + Prism::ConstantWriteNode, + Prism::ConstantOrWriteNode, + Prism::ConstantAndWriteNode, + Prism::ConstantOperatorWriteNode, + Prism::ConstantTargetNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, @@ -53,9 +58,11 @@ def perform parent, position, ) + elsif position_outside_target?(position, target) + return @locations end - target = target #: as Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode | Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode | Prism::CallNode | Prism::DefNode, + target = target #: as Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode | Prism::ConstantWriteNode | Prism::ConstantOrWriteNode | Prism::ConstantAndWriteNode | Prism::ConstantOperatorWriteNode | Prism::ConstantTargetNode | Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode | Prism::CallNode | Prism::DefNode, reference_target = create_reference_target(target, node_context) return @locations unless reference_target @@ -81,19 +88,21 @@ def perform private - #: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode | Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode | Prism::CallNode | Prism::DefNode) target_node, NodeContext node_context) -> RubyIndexer::ReferenceFinder::Target? + #: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode | Prism::ConstantWriteNode | Prism::ConstantOrWriteNode | Prism::ConstantAndWriteNode | Prism::ConstantOperatorWriteNode | Prism::ConstantTargetNode | Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode | Prism::CallNode | Prism::DefNode) target_node, NodeContext node_context) -> RubyIndexer::ReferenceFinder::Target? def create_reference_target(target_node, node_context) case target_node when Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode name = RubyIndexer::Index.constant_name(target_node) return unless name - entries = @global_state.index.resolve(name, node_context.nesting) - return unless entries + resolve_const_reference_target(name, node_context) + when Prism::ConstantWriteNode, + Prism::ConstantOrWriteNode, + Prism::ConstantAndWriteNode, + Prism::ConstantOperatorWriteNode, + Prism::ConstantTargetNode - fully_qualified_name = entries.first #: as !nil - .name - RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name) + resolve_const_reference_target(target_node.name.to_s, node_context) when Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode, @@ -111,6 +120,30 @@ def create_reference_target(target_node, node_context) end end + #: (String name, NodeContext node_context) -> RubyIndexer::ReferenceFinder::ConstTarget? + def resolve_const_reference_target(name, node_context) + entries = @global_state.index.resolve(name, node_context.nesting) + return unless entries + + fully_qualified_name = entries.first #: as !nil + .name + RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name) + end + + #: (Hash[Symbol, untyped] position, Prism::Node? target) -> bool + def position_outside_target?(position, target) + case target + when Prism::ConstantWriteNode, + Prism::ConstantOrWriteNode, + Prism::ConstantAndWriteNode, + Prism::ConstantOperatorWriteNode + + !covers_position?(target.name_loc, position) + else + false + end + end + #: (RubyIndexer::ReferenceFinder::Target target, Prism::LexResult parse_result, URI::Generic uri) -> void def collect_references(target, parse_result, uri) dispatcher = Prism::Dispatcher.new diff --git a/test/requests/references_test.rb b/test/requests/references_test.rb index 9297db34ef..959b9b4c21 100644 --- a/test/requests/references_test.rb +++ b/test/requests/references_test.rb @@ -12,11 +12,93 @@ def test_finds_constant_references assert_equal([0, 3], refs) end + def test_finds_references_from_constant_write_definition + source = <<~RUBY + class Foo + BAR = 1 + BAR + end + + Foo::BAR + RUBY + + refs = find_references_for_source(source, { line: 1, character: 2 }).map do |ref| + ref.range.start.line + end + + assert_equal([1, 2, 5], refs) + end + + def test_finds_references_from_constant_or_write_definition + source = <<~RUBY + class Foo + BAR ||= 1 + BAR + end + + Foo::BAR + RUBY + + refs = find_references_for_source(source, { line: 1, character: 2 }).map do |ref| + ref.range.start.line + end + + assert_equal([1, 2, 5], refs) + end + + def test_finds_references_from_constant_operator_write_definition + source = <<~RUBY + class Foo + BAR = 1 + BAR += 2 + BAR + end + RUBY + + refs = find_references_for_source(source, { line: 2, character: 2 }).map do |ref| + ref.range.start.line + end + + assert_equal([1, 2, 3], refs) + end + + def test_finds_references_from_constant_target_multi_write + source = <<~RUBY + CONST_A, CONST_B = 1, 2 + CONST_A + CONST_B + RUBY + + refs_a = find_references_for_source(source, { line: 0, character: 0 }).map do |ref| + ref.range.start.line + end + assert_equal([0, 1], refs_a) + + refs_b = find_references_for_source(source, { line: 0, character: 9 }).map do |ref| + ref.range.start.line + end + assert_equal([0, 2], refs_b) + end + + def test_returns_empty_when_cursor_is_on_constant_assignment_value + source = <<~RUBY + FOO = 123 + FOO + RUBY + + refs = find_references_for_source(source, { line: 0, character: 7 }) + assert_empty(refs) + end + private def find_references(fixture_path, position) source = File.read(fixture_path) path = File.expand_path(fixture_path) + find_references_for_source(source, position, path: path) + end + + def find_references_for_source(source, position, path: File.expand_path("test/fixtures/fake.rb")) global_state = RubyLsp::GlobalState.new global_state.index.index_single(URI::Generic.from_path(path: path), source)