Skip to content

Commit 46b0411

Browse files
author
Andy Waite
authored
Add method support to References request (#2650)
* Add find references support for methods * Format on one line * Delete commented-out code * Explain matching * Fix tests * Count matches in test * Add test for matching writers * PR feedback * Mark Target as abstract * More tests
1 parent 0a710db commit 46b0411

4 files changed

Lines changed: 276 additions & 33 deletions

File tree

lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ module RubyIndexer
55
class ReferenceFinder
66
extend T::Sig
77

8+
class Target
9+
extend T::Helpers
10+
11+
abstract!
12+
end
13+
14+
class ConstTarget < Target
15+
extend T::Sig
16+
17+
sig { returns(String) }
18+
attr_reader :fully_qualified_name
19+
20+
sig { params(fully_qualified_name: String).void }
21+
def initialize(fully_qualified_name)
22+
super()
23+
@fully_qualified_name = fully_qualified_name
24+
end
25+
end
26+
27+
class MethodTarget < Target
28+
extend T::Sig
29+
30+
sig { returns(String) }
31+
attr_reader :method_name
32+
33+
sig { params(method_name: String).void }
34+
def initialize(method_name)
35+
super()
36+
@method_name = method_name
37+
end
38+
end
39+
840
class Reference
941
extend T::Sig
1042

@@ -27,14 +59,14 @@ def initialize(name, location, declaration:)
2759

2860
sig do
2961
params(
30-
fully_qualified_name: String,
62+
target: Target,
3163
index: RubyIndexer::Index,
3264
dispatcher: Prism::Dispatcher,
3365
include_declarations: T::Boolean,
3466
).void
3567
end
36-
def initialize(fully_qualified_name, index, dispatcher, include_declarations: true)
37-
@fully_qualified_name = fully_qualified_name
68+
def initialize(target, index, dispatcher, include_declarations: true)
69+
@target = target
3870
@index = index
3971
@include_declarations = include_declarations
4072
@stack = T.let([], T::Array[String])
@@ -62,6 +94,7 @@ def initialize(fully_qualified_name, index, dispatcher, include_declarations: tr
6294
:on_constant_or_write_node_enter,
6395
:on_constant_and_write_node_enter,
6496
:on_constant_operator_write_node_enter,
97+
:on_call_node_enter,
6598
)
6699
end
67100

@@ -78,7 +111,7 @@ def on_class_node_enter(node)
78111
name = constant_path.slice
79112
nesting = actual_nesting(name)
80113

81-
if nesting.join("::") == @fully_qualified_name
114+
if @target.is_a?(ConstTarget) && nesting.join("::") == @target.fully_qualified_name
82115
@references << Reference.new(name, constant_path.location, declaration: true)
83116
end
84117

@@ -96,7 +129,7 @@ def on_module_node_enter(node)
96129
name = constant_path.slice
97130
nesting = actual_nesting(name)
98131

99-
if nesting.join("::") == @fully_qualified_name
132+
if @target.is_a?(ConstTarget) && nesting.join("::") == @target.fully_qualified_name
100133
@references << Reference.new(name, constant_path.location, declaration: true)
101134
end
102135

@@ -213,6 +246,10 @@ def on_constant_operator_write_node_enter(node)
213246

214247
sig { params(node: Prism::DefNode).void }
215248
def on_def_node_enter(node)
249+
if @target.is_a?(MethodTarget) && (name = node.name.to_s) == @target.method_name
250+
@references << Reference.new(name, node.name_loc, declaration: true)
251+
end
252+
216253
if node.receiver.is_a?(Prism::SelfNode)
217254
@stack << "<Class:#{@stack.last}>"
218255
end
@@ -225,6 +262,13 @@ def on_def_node_leave(node)
225262
end
226263
end
227264

265+
sig { params(node: Prism::CallNode).void }
266+
def on_call_node_enter(node)
267+
if @target.is_a?(MethodTarget) && (name = node.name.to_s) == @target.method_name
268+
@references << Reference.new(name, T.must(node.message_loc), declaration: false)
269+
end
270+
end
271+
228272
private
229273

230274
sig { params(name: String).returns(T::Array[String]) }
@@ -243,13 +287,15 @@ def actual_nesting(name)
243287

244288
sig { params(name: String, location: Prism::Location).void }
245289
def collect_constant_references(name, location)
290+
return unless @target.is_a?(ConstTarget)
291+
246292
entries = @index.resolve(name, @stack)
247293
return unless entries
248294

249295
previous_reference = @references.last
250296

251297
entries.each do |entry|
252-
next unless entry.name == @fully_qualified_name
298+
next unless entry.name == @target.fully_qualified_name
253299

254300
# When processing a class/module declaration, we eagerly handle the constant reference. To avoid duplicates,
255301
# when we find the constant node defining the namespace, then we have to check if it wasn't already added

lib/ruby_indexer/test/reference_finder_test.rb

Lines changed: 161 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
module RubyIndexer
77
class ReferenceFinderTest < Minitest::Test
88
def test_finds_constant_references
9-
refs = find_references("Foo::Bar", <<~RUBY)
9+
refs = find_const_references("Foo::Bar", <<~RUBY)
1010
module Foo
1111
class Bar
1212
end
@@ -28,7 +28,7 @@ class Bar
2828
end
2929

3030
def test_finds_constant_references_inside_singleton_contexts
31-
refs = find_references("Foo::<Class:Foo>::Bar", <<~RUBY)
31+
refs = find_const_references("Foo::<Class:Foo>::Bar", <<~RUBY)
3232
class Foo
3333
class << self
3434
class Bar
@@ -47,7 +47,7 @@ class Bar
4747
end
4848

4949
def test_finds_top_level_constant_references
50-
refs = find_references("Bar", <<~RUBY)
50+
refs = find_const_references("Bar", <<~RUBY)
5151
class Bar
5252
end
5353
@@ -70,15 +70,171 @@ class << self
7070
assert_equal(8, refs[2].location.start_line)
7171
end
7272

73+
def test_finds_method_references
74+
refs = find_method_references("foo", <<~RUBY)
75+
class Bar
76+
def foo
77+
end
78+
79+
def baz
80+
foo
81+
end
82+
end
83+
RUBY
84+
85+
assert_equal(2, refs.size)
86+
87+
assert_equal("foo", refs[0].name)
88+
assert_equal(2, refs[0].location.start_line)
89+
90+
assert_equal("foo", refs[1].name)
91+
assert_equal(6, refs[1].location.start_line)
92+
end
93+
94+
def test_does_not_mismatch_on_readers_and_writers
95+
refs = find_method_references("foo", <<~RUBY)
96+
class Bar
97+
def foo
98+
end
99+
100+
def foo=(value)
101+
end
102+
103+
def baz
104+
self.foo = 1
105+
self.foo
106+
end
107+
end
108+
RUBY
109+
110+
# We want to match `foo` but not `foo=`
111+
assert_equal(2, refs.size)
112+
113+
assert_equal("foo", refs[0].name)
114+
assert_equal(2, refs[0].location.start_line)
115+
116+
assert_equal("foo", refs[1].name)
117+
assert_equal(10, refs[1].location.start_line)
118+
end
119+
120+
def test_matches_writers
121+
refs = find_method_references("foo=", <<~RUBY)
122+
class Bar
123+
def foo
124+
end
125+
126+
def foo=(value)
127+
end
128+
129+
def baz
130+
self.foo = 1
131+
self.foo
132+
end
133+
end
134+
RUBY
135+
136+
# We want to match `foo=` but not `foo`
137+
assert_equal(2, refs.size)
138+
139+
assert_equal("foo=", refs[0].name)
140+
assert_equal(5, refs[0].location.start_line)
141+
142+
assert_equal("foo=", refs[1].name)
143+
assert_equal(9, refs[1].location.start_line)
144+
end
145+
146+
def test_find_inherited_methods
147+
refs = find_method_references("foo", <<~RUBY)
148+
class Bar
149+
def foo
150+
end
151+
end
152+
153+
class Baz < Bar
154+
super.foo
155+
end
156+
RUBY
157+
158+
assert_equal(2, refs.size)
159+
160+
assert_equal("foo", refs[0].name)
161+
assert_equal(2, refs[0].location.start_line)
162+
163+
assert_equal("foo", refs[1].name)
164+
assert_equal(7, refs[1].location.start_line)
165+
end
166+
167+
def test_finds_methods_created_in_mixins
168+
refs = find_method_references("foo", <<~RUBY)
169+
module Mixin
170+
def foo
171+
end
172+
end
173+
174+
class Bar
175+
include Mixin
176+
end
177+
178+
Bar.foo
179+
RUBY
180+
181+
assert_equal(2, refs.size)
182+
183+
assert_equal("foo", refs[0].name)
184+
assert_equal(2, refs[0].location.start_line)
185+
186+
assert_equal("foo", refs[1].name)
187+
assert_equal(10, refs[1].location.start_line)
188+
end
189+
190+
def test_finds_singleton_methods
191+
# The current implementation matches on both `Bar.foo` and `Bar#foo` even though they are different
192+
193+
refs = find_method_references("foo", <<~RUBY)
194+
class Bar
195+
class << self
196+
def foo
197+
end
198+
end
199+
200+
def foo
201+
end
202+
end
203+
204+
Bar.foo
205+
RUBY
206+
207+
assert_equal(3, refs.size)
208+
209+
assert_equal("foo", refs[0].name)
210+
assert_equal(3, refs[0].location.start_line)
211+
212+
assert_equal("foo", refs[1].name)
213+
assert_equal(7, refs[1].location.start_line)
214+
215+
assert_equal("foo", refs[2].name)
216+
assert_equal(11, refs[2].location.start_line)
217+
end
218+
73219
private
74220

75-
def find_references(fully_qualified_name, source)
221+
def find_const_references(const_name, source)
222+
target = ReferenceFinder::ConstTarget.new(const_name)
223+
find_references(target, source)
224+
end
225+
226+
def find_method_references(method_name, source)
227+
target = ReferenceFinder::MethodTarget.new(method_name)
228+
find_references(target, source)
229+
end
230+
231+
def find_references(target, source)
76232
file_path = "/fake.rb"
77233
index = Index.new
78234
index.index_single(IndexablePath.new(nil, file_path), source)
79235
parse_result = Prism.parse(source)
80236
dispatcher = Prism::Dispatcher.new
81-
finder = ReferenceFinder.new(fully_qualified_name, index, dispatcher)
237+
finder = ReferenceFinder.new(target, index, dispatcher)
82238
dispatcher.visit(parse_result.value)
83239
finder.references
84240
end

0 commit comments

Comments
 (0)