Skip to content

Commit 19d0287

Browse files
committed
find instance variable references with matched scopes (#3377)
* find instance variable references with matched scope * handling top level references and refactoring
1 parent fe1cbbb commit 19d0287

File tree

7 files changed

+303
-18
lines changed

7 files changed

+303
-18
lines changed

lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ class InstanceVariableTarget < Target
3232
#: String
3333
attr_reader :name
3434

35-
#: (String name) -> void
36-
def initialize(name)
35+
#: Array[String]
36+
attr_reader :owner_ancestors
37+
38+
#: (String name, Array[String] owner_ancestors) -> void
39+
def initialize(name, owner_ancestors)
3740
super()
3841
@name = name
42+
@owner_ancestors = owner_ancestors
3943
end
4044
end
4145

@@ -322,7 +326,10 @@ def collect_constant_references(name, location)
322326
def collect_instance_variable_references(name, location, declaration)
323327
return unless @target.is_a?(InstanceVariableTarget) && name == @target.name
324328

325-
@references << Reference.new(name, location, declaration: declaration)
329+
receiver_type = Index.actual_nesting(@stack, nil).join("::")
330+
if @target.owner_ancestors.include?(receiver_type)
331+
@references << Reference.new(name, location, declaration: declaration)
332+
end
326333
end
327334
end
328335
end

lib/ruby_indexer/test/reference_finder_test.rb

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -216,22 +216,43 @@ def foo
216216
assert_equal(11, refs[2].location.start_line)
217217
end
218218

219-
def test_finds_instance_variable_read_references
220-
refs = find_instance_variable_references("@foo", <<~RUBY)
219+
def test_finds_instance_variable_references
220+
refs = find_instance_variable_references("@name", ["Foo"], <<~RUBY)
221221
class Foo
222-
def foo
223-
@foo
222+
def initialize
223+
@name = "foo"
224+
end
225+
def name
226+
@name
227+
end
228+
def name_capital
229+
@name[0]
230+
end
231+
end
232+
233+
class Bar
234+
def initialize
235+
@name = "foo"
236+
end
237+
def name
238+
@name
224239
end
225240
end
226241
RUBY
227-
assert_equal(1, refs.size)
242+
assert_equal(3, refs.size)
228243

229-
assert_equal("@foo", refs[0].name)
244+
assert_equal("@name", refs[0].name)
230245
assert_equal(3, refs[0].location.start_line)
246+
247+
assert_equal("@name", refs[1].name)
248+
assert_equal(6, refs[1].location.start_line)
249+
250+
assert_equal("@name", refs[2].name)
251+
assert_equal(9, refs[2].location.start_line)
231252
end
232253

233254
def test_finds_instance_variable_write_references
234-
refs = find_instance_variable_references("@foo", <<~RUBY)
255+
refs = find_instance_variable_references("@foo", ["Foo"], <<~RUBY)
235256
class Foo
236257
def write
237258
@foo = 1
@@ -252,26 +273,70 @@ def write
252273
assert_equal(7, refs[4].location.start_line)
253274
end
254275

255-
def test_finds_instance_variable_references_ignore_context
256-
refs = find_instance_variable_references("@name", <<~RUBY)
257-
class Foo
276+
def test_finds_instance_variable_references_in_owner_ancestors
277+
refs = find_instance_variable_references("@name", ["Foo", "Base", "Top", "Parent"], <<~RUBY)
278+
module Base
279+
def change_name(name)
280+
@name = name
281+
end
258282
def name
283+
@name
284+
end
285+
286+
module ::Top
287+
def name
288+
@name
289+
end
290+
end
291+
end
292+
293+
class Parent
294+
def initialize
295+
@name = "parent"
296+
end
297+
def name_capital
298+
@name[0]
299+
end
300+
end
301+
302+
class Foo < Parent
303+
include Base
304+
def initialize
259305
@name = "foo"
260306
end
307+
def name
308+
@name
309+
end
261310
end
311+
262312
class Bar
263313
def name
264314
@name = "bar"
265315
end
266316
end
267317
RUBY
268-
assert_equal(2, refs.size)
318+
assert_equal(7, refs.size)
269319

270320
assert_equal("@name", refs[0].name)
271321
assert_equal(3, refs[0].location.start_line)
272322

273323
assert_equal("@name", refs[1].name)
274-
assert_equal(8, refs[1].location.start_line)
324+
assert_equal(6, refs[1].location.start_line)
325+
326+
assert_equal("@name", refs[2].name)
327+
assert_equal(11, refs[2].location.start_line)
328+
329+
assert_equal("@name", refs[3].name)
330+
assert_equal(18, refs[3].location.start_line)
331+
332+
assert_equal("@name", refs[4].name)
333+
assert_equal(21, refs[4].location.start_line)
334+
335+
assert_equal("@name", refs[5].name)
336+
assert_equal(28, refs[5].location.start_line)
337+
338+
assert_equal("@name", refs[6].name)
339+
assert_equal(31, refs[6].location.start_line)
275340
end
276341

277342
def test_accounts_for_reopened_classes
@@ -310,8 +375,8 @@ def find_method_references(method_name, source)
310375
find_references(target, source)
311376
end
312377

313-
def find_instance_variable_references(instance_variable_name, source)
314-
target = ReferenceFinder::InstanceVariableTarget.new(instance_variable_name)
378+
def find_instance_variable_references(instance_variable_name, owner_ancestors, source)
379+
target = ReferenceFinder::InstanceVariableTarget.new(instance_variable_name, owner_ancestors)
315380
find_references(target, source)
316381
end
317382

lib/ruby_lsp/listeners/hover.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Hover
77
include Requests::Support::Common
88

99
ALLOWED_TARGETS = [
10+
Prism::BreakNode,
1011
Prism::CallNode,
1112
Prism::ConstantReadNode,
1213
Prism::ConstantWriteNode,
@@ -54,6 +55,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so
5455

5556
dispatcher.register(
5657
self,
58+
:on_break_node_enter,
5759
:on_constant_read_node_enter,
5860
:on_constant_write_node_enter,
5961
:on_constant_path_node_enter,
@@ -244,6 +246,11 @@ def on_class_variable_write_node_enter(node)
244246
handle_class_variable_hover(node.name.to_s)
245247
end
246248

249+
#: (Prism::BreakNode node) -> void
250+
def on_break_node_enter(node)
251+
handle_keyword_documentation(node.keyword)
252+
end
253+
247254
private
248255

249256
#: ((Prism::InterpolatedStringNode | Prism::StringNode) node) -> void

lib/ruby_lsp/requests/references.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ def create_reference_target(target_node, node_context)
101101
Prism::InstanceVariableReadNode,
102102
Prism::InstanceVariableTargetNode,
103103
Prism::InstanceVariableWriteNode
104-
RubyIndexer::ReferenceFinder::InstanceVariableTarget.new(target_node.name.to_s)
104+
receiver_type = @global_state.type_inferrer.infer_receiver_type(node_context)
105+
return unless receiver_type
106+
107+
ancestors = @global_state.index.linearized_ancestors_of(receiver_type.name)
108+
RubyIndexer::ReferenceFinder::InstanceVariableTarget.new(target_node.name.to_s, ancestors)
105109
when Prism::CallNode, Prism::DefNode
106110
RubyIndexer::ReferenceFinder::MethodTarget.new(target_node.name.to_s)
107111
end

lib/ruby_lsp/static_docs.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ module RubyLsp
1515
# A map of keyword => short documentation to be displayed on hover or completion
1616
KEYWORD_DOCS = {
1717
"yield" => "Invokes the passed block with the given arguments",
18+
"case" => "Starts a case expression for pattern matching or multiple condition checking",
19+
"break" => "Terminates the execution of a block, loop, or method",
1820
}.freeze #: Hash[String, String]
1921
end

static_docs/break.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Break
2+
3+
In Ruby, the `break` keyword is used to exit a loop or block prematurely. Unlike `next` which skips to the next iteration, `break` terminates the loop entirely and continues with the code after the loop.
4+
5+
```ruby
6+
# Basic break usage in a loop
7+
5.times do |i|
8+
break if i == 3
9+
10+
puts i
11+
end
12+
# Output:
13+
# 0
14+
# 1
15+
# 2
16+
```
17+
18+
The `break` statement can be used with any of Ruby's iteration methods or loops.
19+
20+
```ruby
21+
# Using break with different types of loops
22+
array = [1, 2, 3, 4, 5]
23+
24+
# With each
25+
array.each do |num|
26+
break if num > 3
27+
28+
puts "Number: #{num}"
29+
end
30+
# Output:
31+
# Number: 1
32+
# Number: 2
33+
# Number: 3
34+
35+
# With infinite loop
36+
i = 0
37+
loop do
38+
i += 1
39+
break if i >= 5
40+
41+
puts "Count: #{i}"
42+
end
43+
```
44+
45+
## Break with a Value
46+
47+
When used inside a block, `break` can return a value that becomes the result of the method call.
48+
49+
```ruby
50+
# Using break with a return value
51+
result = [1, 2, 3, 4, 5].map do |num|
52+
break "Too large!" if num > 3
53+
54+
num * 2
55+
end
56+
puts result # Output: "Too large!"
57+
58+
# Break in find method
59+
number = (1..100).find do |n|
60+
break n if n > 50 && n.even?
61+
end
62+
puts number # Output: 52
63+
```
64+
65+
## Break in Nested Loops
66+
67+
When using `break` in nested loops, it only exits the innermost loop unless explicitly used with a label (not commonly used in Ruby).
68+
69+
```ruby
70+
# Break in nested iteration
71+
result = (1..3).each do |i|
72+
puts "Outer loop: #{i}"
73+
74+
(1..3).each do |j|
75+
break if j == 2
76+
77+
puts " Inner loop: #{j}"
78+
end
79+
end
80+
# Output:
81+
# Outer loop: 1
82+
# Inner loop: 1
83+
# Outer loop: 2
84+
# Inner loop: 1
85+
# Outer loop: 3
86+
# Inner loop: 1
87+
88+
# Breaking from nested loops using a flag
89+
found = false
90+
(1..3).each do |i|
91+
(1..3).each do |j|
92+
if i * j == 4
93+
found = true
94+
break
95+
end
96+
end
97+
break if found
98+
end
99+
```
100+
101+
The `break` keyword is essential for controlling loop execution and implementing early exit conditions. It's particularly useful when you've found what you're looking for and don't need to continue iterating.

0 commit comments

Comments
 (0)