From 3fdd088d77ccc74eaf0bac523f9354d4c212c7bb Mon Sep 17 00:00:00 2001 From: Reginald Alfret Date: Sat, 7 Nov 2026 14:40:44 +0530 Subject: [PATCH] Support finding references from constant definition sites When invoking Find References on a constant definition (such as ConstantWriteNode, ConstantOrWriteNode, ConstantAndWriteNode, ConstantOperatorWriteNode, or ConstantTargetNode), Ruby LSP previously returned empty results because locate was only matching read nodes and constant path nodes. This commit expands RubyDocument.locate to locate constant write/target nodes, resolves their fully qualified name using index.resolve, and guards against invoking references when the cursor is in the value position of the assignment. Additionally, this removes a duplicate :on_constant_or_write_node_enter dispatcher listener in RubyIndexer::ReferenceFinder that caused or-write constants (CONST ||= 1) to be emitted twice. Signed-off-by: Reginald Alfret --- .../lib/ruby_indexer/reference_finder.rb | 1 - .../test/reference_finder_test.rb | 15 ++++ lib/ruby_lsp/requests/references.rb | 47 +++++++++-- test/requests/references_test.rb | 82 +++++++++++++++++++ 4 files changed, 137 insertions(+), 8 deletions(-) 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)