Skip to content

Commit fa2e928

Browse files
authored
Add super support for definition and hover (#2245)
* Infer receiver type for super calls * Allow resolving only inherited methods * Support super in definition * Support super in hover
1 parent 816e188 commit fa2e928

9 files changed

Lines changed: 229 additions & 34 deletions

File tree

lib/ruby_indexer/lib/ruby_indexer/index.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,14 +331,17 @@ def follow_aliased_namespace(name, seen_names = [])
331331
params(
332332
method_name: String,
333333
receiver_name: String,
334+
inherited_only: T::Boolean,
334335
).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)]))
335336
end
336-
def resolve_method(method_name, receiver_name)
337+
def resolve_method(method_name, receiver_name, inherited_only: false)
337338
method_entries = self[method_name]
338339
return unless method_entries
339340

340341
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
341342
ancestors.each do |ancestor|
343+
next if inherited_only && ancestor == receiver_name
344+
342345
found = method_entries.filter_map do |entry|
343346
case entry
344347
when Entry::Member, Entry::MethodAlias

lib/ruby_indexer/test/index_test.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,43 @@ def bar; end
285285
assert_includes(second_entry.comments, "Hello from second `bar`")
286286
end
287287

288+
def test_resolve_method_inherited_only
289+
index(<<~RUBY)
290+
class Bar
291+
def baz; end
292+
end
293+
294+
class Foo < Bar
295+
def baz; end
296+
end
297+
RUBY
298+
299+
entry = T.must(@index.resolve_method("baz", "Foo", inherited_only: true).first)
300+
301+
assert_equal("Bar", T.must(entry.owner).name)
302+
end
303+
304+
def test_resolve_method_inherited_only_for_prepended_module
305+
index(<<~RUBY)
306+
module Bar
307+
def baz
308+
super
309+
end
310+
end
311+
312+
class Foo
313+
prepend Bar
314+
315+
def baz; end
316+
end
317+
RUBY
318+
319+
# This test is just to document the fact that we don't yet support resolving inherited methods for modules that
320+
# are prepended. The only way to support this is to find all namespaces that have the module a subtype, so that we
321+
# can show the results for everywhere the module has been prepended.
322+
assert_nil(@index.resolve_method("baz", "Bar", inherited_only: true))
323+
end
324+
288325
def test_prefix_search_for_methods
289326
index(<<~RUBY)
290327
module Foo

lib/ruby_lsp/listeners/definition.rb

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def initialize(response_builder, global_state, language_id, uri, node_context, d
4646
:on_instance_variable_or_write_node_enter,
4747
:on_instance_variable_target_node_enter,
4848
:on_string_node_enter,
49+
:on_super_node_enter,
50+
:on_forwarding_super_node_enter,
4951
)
5052
end
5153

@@ -133,8 +135,30 @@ def on_instance_variable_target_node_enter(node)
133135
handle_instance_variable_definition(node.name.to_s)
134136
end
135137

138+
sig { params(node: Prism::SuperNode).void }
139+
def on_super_node_enter(node)
140+
handle_super_node_definition
141+
end
142+
143+
sig { params(node: Prism::ForwardingSuperNode).void }
144+
def on_forwarding_super_node_enter(node)
145+
handle_super_node_definition
146+
end
147+
136148
private
137149

150+
sig { void }
151+
def handle_super_node_definition
152+
surrounding_method = @node_context.surrounding_method
153+
return unless surrounding_method
154+
155+
handle_method_definition(
156+
surrounding_method,
157+
@type_inferrer.infer_receiver_type(@node_context),
158+
inherited_only: true,
159+
)
160+
end
161+
138162
sig { params(name: String).void }
139163
def handle_instance_variable_definition(name)
140164
type = @type_inferrer.infer_receiver_type(@node_context)
@@ -158,10 +182,10 @@ def handle_instance_variable_definition(name)
158182
# If by any chance we haven't indexed the owner, then there's no way to find the right declaration
159183
end
160184

161-
sig { params(message: String, receiver_type: T.nilable(String)).void }
162-
def handle_method_definition(message, receiver_type)
185+
sig { params(message: String, receiver_type: T.nilable(String), inherited_only: T::Boolean).void }
186+
def handle_method_definition(message, receiver_type, inherited_only: false)
163187
methods = if receiver_type
164-
@index.resolve_method(message, receiver_type)
188+
@index.resolve_method(message, receiver_type, inherited_only: inherited_only)
165189
else
166190
# If the method doesn't have a receiver, then we provide a few candidates to jump to
167191
# But we don't want to provide too many candidates, as it can be overwhelming

lib/ruby_lsp/listeners/hover.rb

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class Hover
2121
Prism::InstanceVariableWriteNode,
2222
Prism::SymbolNode,
2323
Prism::StringNode,
24+
Prism::SuperNode,
25+
Prism::ForwardingSuperNode,
2426
],
2527
T::Array[T.class_of(Prism::Node)],
2628
)
@@ -64,6 +66,8 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, ty
6466
:on_instance_variable_operator_write_node_enter,
6567
:on_instance_variable_or_write_node_enter,
6668
:on_instance_variable_target_node_enter,
69+
:on_super_node_enter,
70+
:on_forwarding_super_node_enter,
6771
)
6872
end
6973

@@ -106,17 +110,7 @@ def on_call_node_enter(node)
106110
message = node.message
107111
return unless message
108112

109-
type = @type_inferrer.infer_receiver_type(@node_context)
110-
return unless type
111-
112-
methods = @index.resolve_method(message, type)
113-
return unless methods
114-
115-
title = "#{message}#{T.must(methods.first).decorated_parameters}"
116-
117-
categorized_markdown_from_index_entries(title, methods).each do |category, content|
118-
@response_builder.push(content, category: category)
119-
end
113+
handle_method_hover(message)
120114
end
121115

122116
sig { params(node: Prism::InstanceVariableReadNode).void }
@@ -149,8 +143,41 @@ def on_instance_variable_target_node_enter(node)
149143
handle_instance_variable_hover(node.name.to_s)
150144
end
151145

146+
sig { params(node: Prism::SuperNode).void }
147+
def on_super_node_enter(node)
148+
handle_super_node_hover
149+
end
150+
151+
sig { params(node: Prism::ForwardingSuperNode).void }
152+
def on_forwarding_super_node_enter(node)
153+
handle_super_node_hover
154+
end
155+
152156
private
153157

158+
sig { void }
159+
def handle_super_node_hover
160+
surrounding_method = @node_context.surrounding_method
161+
return unless surrounding_method
162+
163+
handle_method_hover(surrounding_method, inherited_only: true)
164+
end
165+
166+
sig { params(message: String, inherited_only: T::Boolean).void }
167+
def handle_method_hover(message, inherited_only: false)
168+
type = @type_inferrer.infer_receiver_type(@node_context)
169+
return unless type
170+
171+
methods = @index.resolve_method(message, type, inherited_only: inherited_only)
172+
return unless methods
173+
174+
title = "#{message}#{T.must(methods.first).decorated_parameters}"
175+
176+
categorized_markdown_from_index_entries(title, methods).each do |category, content|
177+
@response_builder.push(content, category: category)
178+
end
179+
end
180+
154181
sig { params(name: String).void }
155182
def handle_instance_variable_hover(name)
156183
type = @type_inferrer.infer_receiver_type(@node_context)

lib/ruby_lsp/requests/definition.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def initialize(document, global_state, position, dispatcher, typechecker_enabled
6262
Prism::InstanceVariableWriteNode,
6363
Prism::SymbolNode,
6464
Prism::StringNode,
65+
Prism::SuperNode,
66+
Prism::ForwardingSuperNode,
6567
],
6668
)
6769

lib/ruby_lsp/type_inferrer.rb

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,9 @@ def infer_receiver_type(node_context)
2020
when Prism::CallNode
2121
infer_receiver_for_call_node(node, node_context)
2222
when Prism::InstanceVariableReadNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableWriteNode,
23-
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode
24-
nesting = node_context.nesting
25-
# If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
26-
# inherits from Object
27-
return "Object" if nesting.empty?
28-
29-
fully_qualified_name = node_context.fully_qualified_name
30-
return fully_qualified_name if node_context.surrounding_method
31-
32-
"#{fully_qualified_name}::<Class:#{nesting.last}>"
23+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode,
24+
Prism::SuperNode, Prism::ForwardingSuperNode
25+
self_receiver_handling(node_context)
3326
end
3427
end
3528

@@ -41,15 +34,7 @@ def infer_receiver_for_call_node(node, node_context)
4134

4235
case receiver
4336
when Prism::SelfNode, nil
44-
nesting = node_context.nesting
45-
# If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
46-
# inherits from Object
47-
return "Object" if nesting.empty?
48-
return node_context.fully_qualified_name if node_context.surrounding_method
49-
50-
# If we're not inside a method, then we're inside the body of a class or module, which is a singleton
51-
# context
52-
"#{nesting.join("::")}::<Class:#{nesting.last}>"
37+
self_receiver_handling(node_context)
5338
when Prism::ConstantPathNode, Prism::ConstantReadNode
5439
# When the receiver is a constant reference, we have to try to resolve it to figure out the right
5540
# receiver. But since the invocation is directly on the constant, that's the singleton context of that
@@ -68,6 +53,19 @@ def infer_receiver_for_call_node(node, node_context)
6853
end
6954
end
7055

56+
sig { params(node_context: NodeContext).returns(String) }
57+
def self_receiver_handling(node_context)
58+
nesting = node_context.nesting
59+
# If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
60+
# inherits from Object
61+
return "Object" if nesting.empty?
62+
return node_context.fully_qualified_name if node_context.surrounding_method
63+
64+
# If we're not inside a method, then we're inside the body of a class or module, which is a singleton
65+
# context
66+
"#{nesting.join("::")}::<Class:#{nesting.last}>"
67+
end
68+
7169
sig do
7270
params(
7371
node: T.any(

test/requests/definition_expectations_test.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,45 @@ def do_something
741741
end
742742
end
743743

744+
def test_definition_for_super_calls
745+
source = <<~RUBY
746+
class Parent
747+
def foo; end
748+
def bar; end
749+
end
750+
751+
class Child < Parent
752+
def foo(a)
753+
super()
754+
end
755+
756+
def bar
757+
super
758+
end
759+
end
760+
RUBY
761+
762+
with_server(source) do |server, uri|
763+
server.process_message(
764+
id: 1,
765+
method: "textDocument/definition",
766+
params: { textDocument: { uri: uri }, position: { character: 4, line: 7 } },
767+
)
768+
769+
response = server.pop_response.response
770+
assert_equal(1, response[0].target_range.start.line)
771+
772+
server.process_message(
773+
id: 1,
774+
method: "textDocument/definition",
775+
params: { textDocument: { uri: uri }, position: { character: 4, line: 11 } },
776+
)
777+
778+
response = server.pop_response.response
779+
assert_equal(2, response[0].target_range.start.line)
780+
end
781+
end
782+
744783
private
745784

746785
def create_definition_addon

test/requests/hover_expectations_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,47 @@ def do_something
574574
end
575575
end
576576

577+
def test_hover_for_super_calls
578+
source = <<~RUBY
579+
class Parent
580+
# Foo
581+
def foo; end
582+
# Bar
583+
def bar; end
584+
end
585+
586+
class Child < Parent
587+
def foo(a)
588+
super()
589+
end
590+
591+
def bar
592+
super
593+
end
594+
end
595+
RUBY
596+
597+
with_server(source) do |server, uri|
598+
server.process_message(
599+
id: 1,
600+
method: "textDocument/hover",
601+
params: { textDocument: { uri: uri }, position: { character: 4, line: 9 } },
602+
)
603+
604+
contents = server.pop_response.response.contents.value
605+
assert_match("Foo", contents)
606+
607+
server.process_message(
608+
id: 1,
609+
method: "textDocument/hover",
610+
params: { textDocument: { uri: uri }, position: { character: 4, line: 13 } },
611+
)
612+
613+
contents = server.pop_response.response.contents.value
614+
assert_match("Bar", contents)
615+
end
616+
end
617+
577618
private
578619

579620
def create_hover_addon

test/type_inferrer_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,30 @@ def test_infer_top_level_instance_variables
172172
assert_equal("Object", @type_inferrer.infer_receiver_type(node_context))
173173
end
174174

175+
def test_infer_forwading_super_receiver
176+
node_context = index_and_locate(<<~RUBY, { line: 2, character: 4 })
177+
class Foo < Bar
178+
def initialize
179+
super
180+
end
181+
end
182+
RUBY
183+
184+
assert_equal("Foo", @type_inferrer.infer_receiver_type(node_context))
185+
end
186+
187+
def test_infer_super_receiver
188+
node_context = index_and_locate(<<~RUBY, { line: 2, character: 4 })
189+
class Foo < Bar
190+
def initialize(a, b, c)
191+
super()
192+
end
193+
end
194+
RUBY
195+
196+
assert_equal("Foo", @type_inferrer.infer_receiver_type(node_context))
197+
end
198+
175199
private
176200

177201
def index_and_locate(source, position)

0 commit comments

Comments
 (0)