Skip to content

Commit 81deb14

Browse files
authored
Discard method call target if position doesn't cover identifier (#1981)
1 parent 1c7199a commit 81deb14

File tree

5 files changed

+128
-19
lines changed

5 files changed

+128
-19
lines changed

lib/ruby_lsp/requests/definition.rb

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,40 +43,49 @@ def initialize(document, global_state, position, dispatcher, typechecker_enabled
4343
ResponseBuilders::CollectionResponseBuilder[Interface::Location].new,
4444
ResponseBuilders::CollectionResponseBuilder[Interface::Location],
4545
)
46+
@dispatcher = dispatcher
4647

4748
target, parent, nesting = document.locate_node(
4849
position,
4950
node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
5051
)
5152

5253
if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
54+
# If the target is part of a constant path node, we need to find the exact portion of the constant that the
55+
# user is requesting to go to definition for
5356
target = determine_target(
5457
target,
5558
parent,
5659
position,
5760
)
61+
elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
62+
!covers_position?(target.message_loc, position)
63+
# If the target is a method call, we need to ensure that the requested position is exactly on top of the
64+
# method identifier. Otherwise, we risk showing definitions for unrelated things
65+
target = nil
5866
end
5967

60-
Listeners::Definition.new(
61-
@response_builder,
62-
global_state,
63-
document.uri,
64-
nesting,
65-
dispatcher,
66-
typechecker_enabled,
67-
)
68+
if target
69+
Listeners::Definition.new(
70+
@response_builder,
71+
global_state,
72+
document.uri,
73+
nesting,
74+
dispatcher,
75+
typechecker_enabled,
76+
)
6877

69-
Addon.addons.each do |addon|
70-
addon.create_definition_listener(@response_builder, document.uri, nesting, dispatcher)
78+
Addon.addons.each do |addon|
79+
addon.create_definition_listener(@response_builder, document.uri, nesting, dispatcher)
80+
end
7181
end
7282

7383
@target = T.let(target, T.nilable(Prism::Node))
74-
@dispatcher = dispatcher
7584
end
7685

7786
sig { override.returns(T::Array[Interface::Location]) }
7887
def perform
79-
@dispatcher.dispatch_once(@target)
88+
@dispatcher.dispatch_once(@target) if @target
8089
@response_builder.response
8190
end
8291
end

lib/ruby_lsp/requests/hover.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,29 @@ def provider
4141
end
4242
def initialize(document, global_state, position, dispatcher, typechecker_enabled)
4343
super()
44-
@target = T.let(nil, T.nilable(Prism::Node))
45-
@target, parent, nesting = document.locate_node(
44+
target, parent, nesting = document.locate_node(
4645
position,
4746
node_types: Listeners::Hover::ALLOWED_TARGETS,
4847
)
4948

5049
if (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
51-
!Listeners::Hover::ALLOWED_TARGETS.include?(@target.class)) ||
52-
(parent.is_a?(Prism::ConstantPathNode) && @target.is_a?(Prism::ConstantReadNode))
53-
@target = determine_target(
54-
T.must(@target),
50+
!Listeners::Hover::ALLOWED_TARGETS.include?(target.class)) ||
51+
(parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode))
52+
target = determine_target(
53+
T.must(target),
5554
T.must(parent),
5655
position,
5756
)
57+
elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
58+
!covers_position?(target.message_loc, position)
59+
60+
target = nil
5861
end
5962

6063
# Don't need to instantiate any listeners if there's no target
61-
return unless @target
64+
return unless target
6265

66+
@target = T.let(target, T.nilable(Prism::Node))
6367
uri = document.uri
6468
@response_builder = T.let(ResponseBuilders::Hover.new, ResponseBuilders::Hover)
6569
Listeners::Hover.new(@response_builder, global_state, uri, nesting, dispatcher, typechecker_enabled)

lib/ruby_lsp/requests/request.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ def determine_target(target, parent, position)
6565

6666
target
6767
end
68+
69+
# Checks if a given location covers the position requested
70+
sig { params(location: T.nilable(Prism::Location), position: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
71+
def covers_position?(location, position)
72+
return false unless location
73+
74+
start_line = location.start_line - 1
75+
end_line = location.end_line - 1
76+
line = position[:line]
77+
character = position[:character]
78+
79+
(start_line < line || (start_line == line && location.start_column <= character)) &&
80+
(end_line > line || (end_line == line && location.end_column >= character))
81+
end
6882
end
6983
end
7084
end

test/requests/definition_expectations_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,58 @@ def foo; end
403403
end
404404
end
405405

406+
def test_definition_precision_for_methods_with_block_arguments
407+
source = <<~RUBY
408+
class Foo
409+
def foo(&block); end
410+
end
411+
412+
bar.foo(&:argument)
413+
RUBY
414+
415+
# Going to definition on `argument` should not take you to the `foo` method definition
416+
with_server(source) do |server, uri|
417+
server.process_message(
418+
id: 1,
419+
method: "textDocument/definition",
420+
params: { textDocument: { uri: uri }, position: { character: 12, line: 4 } },
421+
)
422+
assert_empty(server.pop_response.response)
423+
424+
server.process_message(
425+
id: 1,
426+
method: "textDocument/definition",
427+
params: { textDocument: { uri: uri }, position: { character: 4, line: 4 } },
428+
)
429+
refute_empty(server.pop_response.response)
430+
end
431+
end
432+
433+
def test_definition_for_method_call_inside_arguments
434+
source = <<~RUBY
435+
class Foo
436+
def foo; end
437+
438+
def bar(a:, b:); end
439+
440+
def baz
441+
bar(a: foo, b: 42)
442+
end
443+
end
444+
RUBY
445+
446+
with_server(source) do |server, uri|
447+
server.process_message(
448+
id: 1,
449+
method: "textDocument/definition",
450+
params: { textDocument: { uri: uri }, position: { character: 11, line: 6 } },
451+
)
452+
response = server.pop_response.response.first
453+
assert_equal(1, response.range.start.line)
454+
assert_equal(1, response.range.end.line)
455+
end
456+
end
457+
406458
private
407459

408460
def create_definition_addon

test/requests/hover_expectations_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,36 @@ class Post
281281
end
282282
end
283283

284+
def test_hover_precision_for_methods_with_block_arguments
285+
source = <<~RUBY
286+
class Foo
287+
# Hello
288+
def foo(&block); end
289+
290+
def bar
291+
foo(&:argument)
292+
end
293+
end
294+
RUBY
295+
296+
# Going to definition on `argument` should not take you to the `foo` method definition
297+
with_server(source) do |server, uri|
298+
server.process_message(
299+
id: 1,
300+
method: "textDocument/hover",
301+
params: { textDocument: { uri: uri }, position: { character: 12, line: 5 } },
302+
)
303+
assert_nil(server.pop_response.response)
304+
305+
server.process_message(
306+
id: 1,
307+
method: "textDocument/hover",
308+
params: { textDocument: { uri: uri }, position: { character: 4, line: 5 } },
309+
)
310+
assert_match("Hello", server.pop_response.response.contents.value)
311+
end
312+
end
313+
284314
private
285315

286316
def create_hover_addon

0 commit comments

Comments
 (0)