Skip to content

Commit 5c158d1

Browse files
authored
Use method receivers to narrow reference search results (#4091)
1 parent d10a5d4 commit 5c158d1

2 files changed

Lines changed: 74 additions & 31 deletions

File tree

lib/ruby_lsp/requests/references.rb

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def resolve_method_references(message, node_context, include_declarations)
145145
end
146146
return if declarations.empty?
147147

148-
collect_references(method_references_for(message), declarations, include_declarations)
148+
collect_references(method_references_for(message, declarations), declarations, include_declarations)
149149
end
150150

151151
# Handles instance and class variable references. Resolves the receiver type from the node context to locate
@@ -187,14 +187,28 @@ def handle_def_node_references(target, node_context, include_declarations)
187187
declaration = owner.find_member("#{method_name}()")
188188
return unless declaration
189189

190-
collect_references(method_references_for(method_name), [declaration], include_declarations)
190+
collect_references(method_references_for(method_name, [declaration]), [declaration], include_declarations)
191191
end
192192

193-
# Method references in Rubydex are not yet resolved to specific declarations, so we filter from the global
194-
# method references by name
195-
#: (String) -> Array[Rubydex::MethodReference]
196-
def method_references_for(method_name)
197-
@graph.method_references.select { |reference| reference.name == method_name }
193+
#: (String, Array[Rubydex::Declaration]) -> Array[Rubydex::MethodReference]
194+
def method_references_for(method_name, declarations)
195+
target_owner_names = declarations.map do |d|
196+
d.owner #: as Rubydex::Namespace
197+
.name
198+
end
199+
200+
@graph.method_references.select do |reference|
201+
next false unless reference.name == method_name
202+
203+
receiver = reference.receiver
204+
next true if receiver.nil? || target_owner_names.empty?
205+
206+
if receiver.is_a?(Rubydex::Namespace)
207+
receiver.ancestors.any? { |ancestor| target_owner_names.include?(ancestor.name) }
208+
else
209+
target_owner_names.include?(receiver.name)
210+
end
211+
end
198212
end
199213

200214
#: (Enumerable[Rubydex::Reference] references, Array[Rubydex::Declaration] declarations, bool include_declarations) -> void

test/requests/references_test.rb

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,43 +71,72 @@ def bar
7171
assert_includes(ref_lines, 5) # reference
7272
end
7373

74-
def test_does_not_match_simple_names_when_method_receiver_is_known
75-
source = <<~RUBY
76-
class Foo
77-
def self.bar
78-
end
74+
AMBIGUOUS_BAR_SOURCE = <<~RUBY
75+
class Foo
76+
def self.bar
7977
end
78+
end
8079
81-
class Qux
82-
def bar
83-
end
80+
class Qux
81+
def bar
82+
end
83+
end
84+
85+
class Other
86+
def self.bar
8487
end
88+
end
8589
86-
it = Qux.new
87-
it.bar
90+
it = Qux.new
91+
it.bar
8892
89-
Foo.bar
90-
RUBY
93+
Foo.bar
94+
Other.bar
95+
RUBY
9196

92-
# Cursor on `bar` in `Foo.bar` — the receiver is the `Foo` singleton, so only the `Foo.bar`
93-
# declaration is a valid target. We must not surface `Qux#bar` as a candidate declaration.
94-
refs = find_references(source, { line: 13, character: 4 }, include_declarations: true)
97+
def test_filters_method_references_when_call_site_receiver_is_a_known_constant
98+
refs = find_references(AMBIGUOUS_BAR_SOURCE, { line: 18, character: 4 }, include_declarations: true)
9599
ref_lines = refs.map { |r| r.range.start.line }
100+
96101
assert_includes(ref_lines, 1) # def self.bar declaration
97-
assert_includes(ref_lines, 13) # Foo.bar call site
98102
refute_includes(ref_lines, 6) # def bar (Qux) must not appear
103+
refute_includes(ref_lines, 11) # def self.bar (Other) must not appear
104+
assert_includes(ref_lines, 16) # it.bar included because receiver is unresolved
105+
assert_includes(ref_lines, 18) # Foo.bar call site
106+
refute_includes(ref_lines, 19) # Other.bar call site is filtered out by its resolved receiver
107+
end
99108

100-
# it.bar must not appear, but we don't currently expose method reference receivers in the API
101-
assert_includes(ref_lines, 11)
102-
103-
# Cursor on `bar` in `it.bar` — the type of the local `it` can't be inferred, so we fall back
104-
# to including every `bar` declaration and call site as candidates.
105-
refs = find_references(source, { line: 11, character: 3 }, include_declarations: true)
109+
def test_falls_back_to_all_candidates_when_call_site_receiver_is_unresolvable
110+
refs = find_references(AMBIGUOUS_BAR_SOURCE, { line: 16, character: 3 }, include_declarations: true)
106111
ref_lines = refs.map { |r| r.range.start.line }
112+
107113
assert_includes(ref_lines, 1) # Foo.bar declaration
108114
assert_includes(ref_lines, 6) # Qux#bar declaration
109-
assert_includes(ref_lines, 11) # it.bar call site
110-
assert_includes(ref_lines, 13) # Foo.bar call site
115+
assert_includes(ref_lines, 11) # Other.bar declaration
116+
assert_includes(ref_lines, 16) # it.bar call site
117+
assert_includes(ref_lines, 18) # Foo.bar call site
118+
assert_includes(ref_lines, 19) # Other.bar call site
119+
end
120+
121+
def test_method_references_match_through_superclass_chain
122+
source = <<~RUBY
123+
class Parent
124+
def self.bar
125+
end
126+
end
127+
128+
class Child < Parent
129+
end
130+
131+
Parent.bar
132+
Child.bar
133+
RUBY
134+
135+
refs = find_references(source, { line: 1, character: 11 }, include_declarations: true)
136+
ref_lines = refs.map { |r| r.range.start.line }
137+
assert_includes(ref_lines, 1) # Parent.bar declaration
138+
assert_includes(ref_lines, 8) # Parent.bar call site
139+
assert_includes(ref_lines, 9) # Child.bar call site (matched through Child::<Child>'s ancestors)
111140
end
112141

113142
def test_finds_references_from_def_node

0 commit comments

Comments
 (0)