ruby-lsp 0.17.2 → 0.17.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +280 -74
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +102 -102
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +234 -56
  7. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +147 -0
  8. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +49 -2
  10. data/lib/ruby_indexer/test/configuration_test.rb +1 -1
  11. data/lib/ruby_indexer/test/constant_test.rb +1 -1
  12. data/lib/ruby_indexer/test/index_test.rb +702 -71
  13. data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
  14. data/lib/ruby_indexer/test/method_test.rb +74 -24
  15. data/lib/ruby_indexer/test/rbs_indexer_test.rb +67 -0
  16. data/lib/ruby_indexer/test/test_case.rb +7 -0
  17. data/lib/ruby_lsp/document.rb +37 -8
  18. data/lib/ruby_lsp/global_state.rb +43 -18
  19. data/lib/ruby_lsp/internal.rb +2 -0
  20. data/lib/ruby_lsp/listeners/code_lens.rb +2 -2
  21. data/lib/ruby_lsp/listeners/completion.rb +53 -14
  22. data/lib/ruby_lsp/listeners/definition.rb +11 -7
  23. data/lib/ruby_lsp/listeners/hover.rb +14 -7
  24. data/lib/ruby_lsp/listeners/signature_help.rb +5 -2
  25. data/lib/ruby_lsp/node_context.rb +6 -1
  26. data/lib/ruby_lsp/requests/completion.rb +5 -4
  27. data/lib/ruby_lsp/requests/completion_resolve.rb +8 -0
  28. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +88 -0
  29. data/lib/ruby_lsp/requests/support/common.rb +19 -1
  30. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +12 -4
  31. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +91 -0
  32. data/lib/ruby_lsp/requests/workspace_symbol.rb +1 -21
  33. data/lib/ruby_lsp/requests.rb +2 -0
  34. data/lib/ruby_lsp/server.rb +54 -4
  35. data/lib/ruby_lsp/test_helper.rb +1 -1
  36. data/lib/ruby_lsp/type_inferrer.rb +86 -0
  37. metadata +29 -4
@@ -20,7 +20,9 @@ module RubyIndexer
20
20
  assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
21
21
 
22
22
  entry = T.must(@index["@a"]&.first)
23
- assert_equal("Foo::Bar", T.must(entry.owner).name)
23
+ owner = T.must(entry.owner)
24
+ assert_instance_of(Entry::Class, owner)
25
+ assert_equal("Foo::Bar", owner.name)
24
26
  end
25
27
 
26
28
  def test_instance_variable_and_write
@@ -38,7 +40,9 @@ module RubyIndexer
38
40
  assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
39
41
 
40
42
  entry = T.must(@index["@a"]&.first)
41
- assert_equal("Foo::Bar", T.must(entry.owner).name)
43
+ owner = T.must(entry.owner)
44
+ assert_instance_of(Entry::Class, owner)
45
+ assert_equal("Foo::Bar", owner.name)
42
46
  end
43
47
 
44
48
  def test_instance_variable_operator_write
@@ -56,7 +60,9 @@ module RubyIndexer
56
60
  assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
57
61
 
58
62
  entry = T.must(@index["@a"]&.first)
59
- assert_equal("Foo::Bar", T.must(entry.owner).name)
63
+ owner = T.must(entry.owner)
64
+ assert_instance_of(Entry::Class, owner)
65
+ assert_equal("Foo::Bar", owner.name)
60
66
  end
61
67
 
62
68
  def test_instance_variable_or_write
@@ -74,7 +80,9 @@ module RubyIndexer
74
80
  assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
75
81
 
76
82
  entry = T.must(@index["@a"]&.first)
77
- assert_equal("Foo::Bar", T.must(entry.owner).name)
83
+ owner = T.must(entry.owner)
84
+ assert_instance_of(Entry::Class, owner)
85
+ assert_equal("Foo::Bar", owner.name)
78
86
  end
79
87
 
80
88
  def test_instance_variable_target
@@ -93,10 +101,14 @@ module RubyIndexer
93
101
  assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:4-10:4-12")
94
102
 
95
103
  entry = T.must(@index["@a"]&.first)
96
- assert_equal("Foo::Bar", T.must(entry.owner).name)
104
+ owner = T.must(entry.owner)
105
+ assert_instance_of(Entry::Class, owner)
106
+ assert_equal("Foo::Bar", owner.name)
97
107
 
98
108
  entry = T.must(@index["@b"]&.first)
99
- assert_equal("Foo::Bar", T.must(entry.owner).name)
109
+ owner = T.must(entry.owner)
110
+ assert_instance_of(Entry::Class, owner)
111
+ assert_equal("Foo::Bar", owner.name)
100
112
  end
101
113
 
102
114
  def test_empty_name_instance_variables
@@ -118,6 +130,14 @@ module RubyIndexer
118
130
  module Foo
119
131
  class Bar
120
132
  @a = 123
133
+
134
+ class << self
135
+ def hello
136
+ @b = 123
137
+ end
138
+
139
+ @c = 123
140
+ end
121
141
  end
122
142
  end
123
143
  RUBY
@@ -125,7 +145,64 @@ module RubyIndexer
125
145
  assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6")
126
146
 
127
147
  entry = T.must(@index["@a"]&.first)
128
- assert_equal("Foo::Bar", T.must(entry.owner).name)
148
+ owner = T.must(entry.owner)
149
+ assert_instance_of(Entry::SingletonClass, owner)
150
+ assert_equal("Foo::Bar::<Class:Bar>", owner.name)
151
+
152
+ assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:6-8:6-10")
153
+
154
+ entry = T.must(@index["@b"]&.first)
155
+ owner = T.must(entry.owner)
156
+ assert_instance_of(Entry::SingletonClass, owner)
157
+ assert_equal("Foo::Bar::<Class:Bar>", owner.name)
158
+
159
+ assert_entry("@c", Entry::InstanceVariable, "/fake/path/foo.rb:9-6:9-8")
160
+
161
+ entry = T.must(@index["@c"]&.first)
162
+ owner = T.must(entry.owner)
163
+ assert_instance_of(Entry::SingletonClass, owner)
164
+ assert_equal("Foo::Bar::<Class:Bar>::<Class:<Class:Bar>>", owner.name)
165
+ end
166
+
167
+ def test_top_level_instance_variables
168
+ index(<<~RUBY)
169
+ @a = 123
170
+ RUBY
171
+
172
+ entry = T.must(@index["@a"]&.first)
173
+ assert_nil(entry.owner)
174
+ end
175
+
176
+ def test_class_instance_variables_inside_self_method
177
+ index(<<~RUBY)
178
+ class Foo
179
+ def self.bar
180
+ @a = 123
181
+ end
182
+ end
183
+ RUBY
184
+
185
+ entry = T.must(@index["@a"]&.first)
186
+ owner = T.must(entry.owner)
187
+ assert_instance_of(Entry::SingletonClass, owner)
188
+ assert_equal("Foo::<Class:Foo>", owner.name)
189
+ end
190
+
191
+ def test_instance_variable_inside_dynamic_method_declaration
192
+ index(<<~RUBY)
193
+ class Foo
194
+ def something.bar
195
+ @a = 123
196
+ end
197
+ end
198
+ RUBY
199
+
200
+ # If the surrounding method is beind defined on any dynamic value that isn't `self`, then we attribute the
201
+ # instance variable to the wrong owner since there's no way to understand that statically
202
+ entry = T.must(@index["@a"]&.first)
203
+ owner = T.must(entry.owner)
204
+ assert_instance_of(Entry::Class, owner)
205
+ assert_equal("Foo", owner.name)
129
206
  end
130
207
  end
131
208
  end
@@ -13,7 +13,7 @@ module RubyIndexer
13
13
  end
14
14
  RUBY
15
15
 
16
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
16
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
17
17
  end
18
18
 
19
19
  def test_conditional_method
@@ -24,7 +24,7 @@ module RubyIndexer
24
24
  end
25
25
  RUBY
26
26
 
27
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
27
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
28
28
  end
29
29
 
30
30
  def test_singleton_method_using_self_receiver
@@ -35,7 +35,12 @@ module RubyIndexer
35
35
  end
36
36
  RUBY
37
37
 
38
- assert_entry("bar", Entry::SingletonMethod, "/fake/path/foo.rb:1-2:2-5")
38
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
39
+
40
+ entry = T.must(@index["bar"].first)
41
+ owner = T.must(entry.owner)
42
+ assert_equal("Foo::<Class:Foo>", owner.name)
43
+ assert_instance_of(Entry::SingletonClass, owner)
39
44
  end
40
45
 
41
46
  def test_singleton_method_using_other_receiver_is_not_indexed
@@ -83,9 +88,9 @@ module RubyIndexer
83
88
  def baz; end
84
89
  RUBY
85
90
 
86
- assert_entry("foo", Entry::InstanceMethod, "/fake/path/foo.rb:0-8:1-3", visibility: Entry::Visibility::PRIVATE)
87
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:3-0:3-12", visibility: Entry::Visibility::PUBLIC)
88
- assert_entry("baz", Entry::InstanceMethod, "/fake/path/foo.rb:7-0:7-12", visibility: Entry::Visibility::PROTECTED)
91
+ assert_entry("foo", Entry::Method, "/fake/path/foo.rb:0-8:1-3", visibility: Entry::Visibility::PRIVATE)
92
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:3-0:3-12", visibility: Entry::Visibility::PUBLIC)
93
+ assert_entry("baz", Entry::Method, "/fake/path/foo.rb:7-0:7-12", visibility: Entry::Visibility::PROTECTED)
89
94
  end
90
95
 
91
96
  def test_visibility_tracking_with_nested_class_or_modules
@@ -103,9 +108,9 @@ module RubyIndexer
103
108
  end
104
109
  RUBY
105
110
 
106
- assert_entry("foo", Entry::InstanceMethod, "/fake/path/foo.rb:3-2:3-14", visibility: Entry::Visibility::PRIVATE)
107
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:6-4:6-16", visibility: Entry::Visibility::PUBLIC)
108
- assert_entry("baz", Entry::InstanceMethod, "/fake/path/foo.rb:9-2:9-14", visibility: Entry::Visibility::PRIVATE)
111
+ assert_entry("foo", Entry::Method, "/fake/path/foo.rb:3-2:3-14", visibility: Entry::Visibility::PRIVATE)
112
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:6-4:6-16", visibility: Entry::Visibility::PUBLIC)
113
+ assert_entry("baz", Entry::Method, "/fake/path/foo.rb:9-2:9-14", visibility: Entry::Visibility::PRIVATE)
109
114
  end
110
115
 
111
116
  def test_method_with_parameters
@@ -116,7 +121,7 @@ module RubyIndexer
116
121
  end
117
122
  RUBY
118
123
 
119
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
124
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
120
125
  entry = T.must(@index["bar"].first)
121
126
  assert_equal(1, entry.parameters.length)
122
127
  parameter = entry.parameters.first
@@ -132,7 +137,7 @@ module RubyIndexer
132
137
  end
133
138
  RUBY
134
139
 
135
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
140
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
136
141
  entry = T.must(@index["bar"].first)
137
142
  assert_equal(1, entry.parameters.length)
138
143
  parameter = entry.parameters.first
@@ -148,7 +153,7 @@ module RubyIndexer
148
153
  end
149
154
  RUBY
150
155
 
151
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
156
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
152
157
  entry = T.must(@index["bar"].first)
153
158
  assert_equal(1, entry.parameters.length)
154
159
  parameter = entry.parameters.first
@@ -164,7 +169,7 @@ module RubyIndexer
164
169
  end
165
170
  RUBY
166
171
 
167
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
172
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
168
173
  entry = T.must(@index["bar"].first)
169
174
  assert_equal(2, entry.parameters.length)
170
175
  a, b = entry.parameters
@@ -184,7 +189,7 @@ module RubyIndexer
184
189
  end
185
190
  RUBY
186
191
 
187
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
192
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
188
193
  entry = T.must(@index["bar"].first)
189
194
  assert_equal(2, entry.parameters.length)
190
195
  a, b = entry.parameters
@@ -209,7 +214,7 @@ module RubyIndexer
209
214
  end
210
215
  RUBY
211
216
 
212
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
217
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
213
218
  entry = T.must(@index["bar"].first)
214
219
  assert_equal(2, entry.parameters.length)
215
220
  a, b = entry.parameters
@@ -246,7 +251,7 @@ module RubyIndexer
246
251
  end
247
252
  RUBY
248
253
 
249
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
254
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
250
255
  entry = T.must(@index["bar"].first)
251
256
  assert_equal(1, entry.parameters.length)
252
257
  param = entry.parameters.first
@@ -287,7 +292,7 @@ module RubyIndexer
287
292
  end
288
293
  RUBY
289
294
 
290
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
295
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
291
296
  entry = T.must(@index["bar"].first)
292
297
  assert_equal(2, entry.parameters.length)
293
298
  first, second = entry.parameters
@@ -307,7 +312,7 @@ module RubyIndexer
307
312
  end
308
313
  RUBY
309
314
 
310
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
315
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
311
316
  entry = T.must(@index["bar"].first)
312
317
  assert_empty(entry.parameters)
313
318
  end
@@ -359,24 +364,69 @@ module RubyIndexer
359
364
  def test_properly_tracks_multiple_levels_of_nesting
360
365
  index(<<~RUBY)
361
366
  module Foo
362
- def first; end
367
+ def first_method; end
363
368
 
364
369
  module Bar
365
- def second; end
370
+ def second_method; end
366
371
  end
367
372
 
368
- def third; end
373
+ def third_method; end
369
374
  end
370
375
  RUBY
371
376
 
372
- entry = T.must(@index["first"]&.first)
377
+ entry = T.cast(@index["first_method"]&.first, Entry::Method)
373
378
  assert_equal("Foo", T.must(entry.owner).name)
374
379
 
375
- entry = T.must(@index["second"]&.first)
380
+ entry = T.cast(@index["second_method"]&.first, Entry::Method)
376
381
  assert_equal("Foo::Bar", T.must(entry.owner).name)
377
382
 
378
- entry = T.must(@index["third"]&.first)
383
+ entry = T.cast(@index["third_method"]&.first, Entry::Method)
379
384
  assert_equal("Foo", T.must(entry.owner).name)
380
385
  end
386
+
387
+ def test_keeps_track_of_aliases
388
+ index(<<~RUBY)
389
+ class Foo
390
+ alias whatever to_s
391
+ alias_method :foo, :to_a
392
+ alias_method "bar", "to_a"
393
+
394
+ # These two are not indexed because they are dynamic or incomplete
395
+ alias_method baz, :to_a
396
+ alias_method :baz
397
+ end
398
+ RUBY
399
+
400
+ assert_entry("whatever", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:1-8:1-16")
401
+ assert_entry("foo", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:2-15:2-19")
402
+ assert_entry("bar", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:3-15:3-20")
403
+ # Foo plus 3 valid aliases
404
+ assert_equal(4, @index.instance_variable_get(:@entries).length - @default_indexed_entries.length)
405
+ end
406
+
407
+ def test_singleton_methods
408
+ index(<<~RUBY)
409
+ class Foo
410
+ def self.bar; end
411
+
412
+ class << self
413
+ def baz; end
414
+ end
415
+ end
416
+ RUBY
417
+
418
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:1-19")
419
+ assert_entry("baz", Entry::Method, "/fake/path/foo.rb:4-4:4-16")
420
+
421
+ bar_owner = T.must(T.must(@index["bar"].first).owner)
422
+ baz_owner = T.must(T.must(@index["baz"].first).owner)
423
+
424
+ assert_instance_of(Entry::SingletonClass, bar_owner)
425
+ assert_instance_of(Entry::SingletonClass, baz_owner)
426
+
427
+ # Regardless of whether the method was added through `self.something` or `class << self`, the owner object must be
428
+ # the exact same
429
+ assert_same(bar_owner, baz_owner)
430
+ end
381
431
  end
382
432
  end
@@ -0,0 +1,67 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test_case"
5
+
6
+ module RubyIndexer
7
+ class RBSIndexerTest < TestCase
8
+ def test_index_core_classes
9
+ entries = @index["Array"]
10
+ refute_nil(entries)
11
+ # Array is a class but also an instance method on Kernel
12
+ assert_equal(2, entries.length)
13
+ entry = entries.find { |entry| entry.is_a?(RubyIndexer::Entry::Class) }
14
+ assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path)
15
+ assert_equal("array.rbs", entry.file_name)
16
+ assert_equal("Object", entry.parent_class)
17
+ assert_equal(1, entry.mixin_operations.length)
18
+ enumerable_include = entry.mixin_operations.first
19
+ assert_equal("Enumerable", enumerable_include.module_name)
20
+
21
+ # Using fixed positions would be fragile, so let's just check some basics.
22
+ assert_operator(entry.location.start_line, :>, 0)
23
+ assert_operator(entry.location.end_line, :>, entry.location.start_line)
24
+ assert_equal(0, entry.location.start_column)
25
+ assert_operator(entry.location.end_column, :>, 0)
26
+ end
27
+
28
+ def test_index_core_modules
29
+ entries = @index["Kernel"]
30
+ refute_nil(entries)
31
+ assert_equal(1, entries.length)
32
+ entry = entries.first
33
+ assert_match(%r{/gems/rbs-.*/core/kernel.rbs}, entry.file_path)
34
+ assert_equal("kernel.rbs", entry.file_name)
35
+
36
+ # Using fixed positions would be fragile, so let's just check some basics.
37
+ assert_operator(entry.location.start_line, :>, 0)
38
+ assert_operator(entry.location.end_line, :>, entry.location.start_line)
39
+ assert_equal(0, entry.location.start_column)
40
+ assert_operator(entry.location.end_column, :>, 0)
41
+ end
42
+
43
+ def test_index_methods
44
+ entries = @index["initialize"]
45
+ refute_nil(entries)
46
+ entry = entries.find { |entry| entry.owner.name == "Array" }
47
+ assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path)
48
+ assert_equal("array.rbs", entry.file_name)
49
+ assert_equal(Entry::Visibility::PUBLIC, entry.visibility)
50
+
51
+ # Using fixed positions would be fragile, so let's just check some basics.
52
+ assert_operator(entry.location.start_line, :>, 0)
53
+ assert_operator(entry.location.end_line, :>, entry.location.start_line)
54
+ assert_equal(2, entry.location.start_column)
55
+ assert_operator(entry.location.end_column, :>, 0)
56
+ end
57
+
58
+ def test_attaches_correct_owner_to_singleton_methods
59
+ entries = @index["basename"]
60
+ refute_nil(entries)
61
+
62
+ owner = entries.first.owner
63
+ assert_instance_of(Entry::SingletonClass, owner)
64
+ assert_equal("File::<Class:File>", owner.name)
65
+ end
66
+ end
67
+ end
@@ -7,6 +7,8 @@ module RubyIndexer
7
7
  class TestCase < Minitest::Test
8
8
  def setup
9
9
  @index = Index.new
10
+ RBSIndexer.new(@index).index_ruby_core
11
+ @default_indexed_entries = @index.instance_variable_get(:@entries).dup
10
12
  end
11
13
 
12
14
  private
@@ -17,6 +19,7 @@ module RubyIndexer
17
19
 
18
20
  def assert_entry(expected_name, type, expected_location, visibility: nil)
19
21
  entries = @index[expected_name]
22
+ refute_nil(entries, "Expected #{expected_name} to be indexed")
20
23
  refute_empty(entries, "Expected #{expected_name} to be indexed")
21
24
 
22
25
  entry = entries.first
@@ -41,6 +44,10 @@ module RubyIndexer
41
44
  assert_empty(@index.instance_variable_get(:@entries), "Expected nothing to be indexed")
42
45
  end
43
46
 
47
+ def assert_no_indexed_entries
48
+ assert_equal(@default_indexed_entries, @index.instance_variable_get(:@entries))
49
+ end
50
+
44
51
  def assert_no_entry(entry)
45
52
  refute(@index.instance_variable_get(:@entries).key?(entry), "Expected '#{entry}' to not be indexed")
46
53
  end
@@ -127,7 +127,10 @@ module RubyLsp
127
127
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
128
128
  closest = node
129
129
  parent = T.let(nil, T.nilable(Prism::Node))
130
- nesting = T.let([], T::Array[T.any(Prism::ClassNode, Prism::ModuleNode)])
130
+ nesting_nodes = T.let(
131
+ [],
132
+ T::Array[T.any(Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode)],
133
+ )
131
134
  call_node = T.let(nil, T.nilable(Prism::CallNode))
132
135
 
133
136
  until queue.empty?
@@ -151,13 +154,18 @@ module RubyLsp
151
154
 
152
155
  # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
153
156
  # need to pop the stack
154
- previous_level = nesting.last
155
- nesting.pop if previous_level && loc.start_offset > previous_level.location.end_offset
157
+ previous_level = nesting_nodes.last
158
+ nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
156
159
 
157
160
  # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
158
161
  # target when it is a constant
159
- if candidate.is_a?(Prism::ClassNode) || candidate.is_a?(Prism::ModuleNode)
160
- nesting << candidate
162
+ case candidate
163
+ when Prism::ClassNode, Prism::ModuleNode
164
+ nesting_nodes << candidate
165
+ when Prism::SingletonClassNode
166
+ nesting_nodes << candidate
167
+ when Prism::DefNode
168
+ nesting_nodes << candidate
161
169
  end
162
170
 
163
171
  if candidate.is_a?(Prism::CallNode)
@@ -189,11 +197,32 @@ module RubyLsp
189
197
  # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
190
198
  # though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
191
199
  if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
192
- last_level = nesting.last
193
- nesting.pop if last_level && last_level.constant_path == closest
200
+ last_level = nesting_nodes.last
201
+
202
+ if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) &&
203
+ last_level.constant_path == closest
204
+ nesting_nodes.pop
205
+ end
206
+ end
207
+
208
+ nesting = []
209
+ surrounding_method = T.let(nil, T.nilable(String))
210
+
211
+ nesting_nodes.each do |node|
212
+ case node
213
+ when Prism::ClassNode, Prism::ModuleNode
214
+ nesting << node.constant_path.slice
215
+ when Prism::SingletonClassNode
216
+ nesting << "<Class:#{nesting.last}>"
217
+ when Prism::DefNode
218
+ surrounding_method = node.name.to_s
219
+ next unless node.receiver.is_a?(Prism::SelfNode)
220
+
221
+ nesting << "<Class:#{nesting.last}>"
222
+ end
194
223
  end
195
224
 
196
- NodeContext.new(closest, parent, nesting.map { |n| n.constant_path.location.slice }, call_node)
225
+ NodeContext.new(closest, parent, nesting, call_node, surrounding_method)
197
226
  end
198
227
 
199
228
  sig { returns(T::Boolean) }
@@ -12,7 +12,7 @@ module RubyLsp
12
12
  attr_accessor :formatter
13
13
 
14
14
  sig { returns(T::Boolean) }
15
- attr_reader :typechecker
15
+ attr_reader :has_type_checker
16
16
 
17
17
  sig { returns(RubyIndexer::Index) }
18
18
  attr_reader :index
@@ -23,6 +23,9 @@ module RubyLsp
23
23
  sig { returns(T::Boolean) }
24
24
  attr_reader :supports_watching_files
25
25
 
26
+ sig { returns(TypeInferrer) }
27
+ attr_reader :type_inferrer
28
+
26
29
  sig { void }
27
30
  def initialize
28
31
  @workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
@@ -31,8 +34,9 @@ module RubyLsp
31
34
  @formatter = T.let("auto", String)
32
35
  @linters = T.let([], T::Array[String])
33
36
  @test_library = T.let("minitest", String)
34
- @typechecker = T.let(true, T::Boolean)
37
+ @has_type_checker = T.let(true, T::Boolean)
35
38
  @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
39
+ @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
36
40
  @supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
37
41
  @supports_watching_files = T.let(false, T::Boolean)
38
42
  end
@@ -54,18 +58,19 @@ module RubyLsp
54
58
 
55
59
  sig { params(options: T::Hash[Symbol, T.untyped]).void }
56
60
  def apply_options(options)
57
- dependencies = gather_dependencies
61
+ direct_dependencies = gather_direct_dependencies
62
+ all_dependencies = gather_direct_and_indirect_dependencies
58
63
  workspace_uri = options.dig(:workspaceFolders, 0, :uri)
59
64
  @workspace_uri = URI(workspace_uri) if workspace_uri
60
65
 
61
66
  specified_formatter = options.dig(:initializationOptions, :formatter)
62
67
  @formatter = specified_formatter if specified_formatter
63
- @formatter = detect_formatter(dependencies) if @formatter == "auto"
68
+ @formatter = detect_formatter(direct_dependencies, all_dependencies) if @formatter == "auto"
64
69
 
65
70
  specified_linters = options.dig(:initializationOptions, :linters)
66
- @linters = specified_linters || detect_linters(dependencies)
67
- @test_library = detect_test_library(dependencies)
68
- @typechecker = detect_typechecker(dependencies)
71
+ @linters = specified_linters || detect_linters(direct_dependencies)
72
+ @test_library = detect_test_library(direct_dependencies)
73
+ @has_type_checker = detect_typechecker(direct_dependencies)
69
74
 
70
75
  encodings = options.dig(:capabilities, :general, :positionEncodings)
71
76
  @encoding = if !encodings || encodings.empty?
@@ -103,16 +108,18 @@ module RubyLsp
103
108
 
104
109
  private
105
110
 
106
- sig { params(dependencies: T::Array[String]).returns(String) }
107
- def detect_formatter(dependencies)
111
+ sig { params(direct_dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(String) }
112
+ def detect_formatter(direct_dependencies, all_dependencies)
108
113
  # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
109
- if dependencies.any?(/^rubocop/)
110
- "rubocop"
111
- elsif dependencies.any?(/^syntax_tree$/)
112
- "syntax_tree"
113
- else
114
- "none"
115
- end
114
+ return "rubocop" if direct_dependencies.any?(/^rubocop/)
115
+
116
+ syntax_tree_is_direct_dependency = direct_dependencies.include?("syntax_tree")
117
+ return "syntax_tree" if syntax_tree_is_direct_dependency
118
+
119
+ rubocop_is_transitive_dependency = all_dependencies.include?("rubocop")
120
+ return "rubocop" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
121
+
122
+ "none"
116
123
  end
117
124
 
118
125
  # Try to detect if there are linters in the project's dependencies. For auto-detection, we always only consider a
@@ -132,7 +139,7 @@ module RubyLsp
132
139
  # by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
133
140
  # activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
134
141
  # of bin/rails to support these cases.
135
- elsif File.exist?(File.join(workspace_path, "bin/rails"))
142
+ elsif bin_rails_present
136
143
  "rails"
137
144
  # NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
138
145
  elsif dependencies.any?(/^minitest$/)
@@ -162,8 +169,18 @@ module RubyLsp
162
169
  false
163
170
  end
164
171
 
172
+ sig { returns(T::Boolean) }
173
+ def bin_rails_present
174
+ File.exist?(File.join(workspace_path, "bin/rails"))
175
+ end
176
+
177
+ sig { returns(T::Boolean) }
178
+ def dot_rubocop_yml_present
179
+ File.exist?(File.join(workspace_path, ".rubocop.yml"))
180
+ end
181
+
165
182
  sig { returns(T::Array[String]) }
166
- def gather_dependencies
183
+ def gather_direct_dependencies
167
184
  Bundler.with_original_env { Bundler.default_gemfile }
168
185
  Bundler.locked_gems.dependencies.keys + gemspec_dependencies
169
186
  rescue Bundler::GemfileNotFound
@@ -176,5 +193,13 @@ module RubyLsp
176
193
  .grep(Bundler::Source::Gemspec)
177
194
  .flat_map { _1.gemspec&.dependencies&.map(&:name) }
178
195
  end
196
+
197
+ sig { returns(T::Array[String]) }
198
+ def gather_direct_and_indirect_dependencies
199
+ Bundler.with_original_env { Bundler.default_gemfile }
200
+ Bundler.locked_gems.specs.map(&:name)
201
+ rescue Bundler::GemfileNotFound
202
+ []
203
+ end
179
204
  end
180
205
  end
@@ -18,6 +18,7 @@ require "set"
18
18
  require "prism"
19
19
  require "prism/visitor"
20
20
  require "language_server-protocol"
21
+ require "rbs"
21
22
 
22
23
  require "ruby-lsp"
23
24
  require "ruby_lsp/base_server"
@@ -27,6 +28,7 @@ require "ruby_lsp/utils"
27
28
  require "ruby_lsp/parameter_scope"
28
29
  require "ruby_lsp/global_state"
29
30
  require "ruby_lsp/server"
31
+ require "ruby_lsp/type_inferrer"
30
32
  require "ruby_lsp/requests"
31
33
  require "ruby_lsp/response_builders"
32
34
  require "ruby_lsp/node_context"
@@ -236,7 +236,7 @@ module RubyLsp
236
236
  # so there must be something to the left of the available path.
237
237
  group_stack = T.must(group_stack[last_dynamic_reference_index + 1..])
238
238
  if method_name
239
- " --name " + "/::#{Shellwords.escape(group_stack.join("::") + "#" + method_name)}$/"
239
+ " --name " + "/::#{Shellwords.escape(group_stack.join("::")) + "#" + Shellwords.escape(method_name)}$/"
240
240
  else
241
241
  # When clicking on a CodeLens for `Test`, `(#|::)` will match all tests
242
242
  # that are registered on the class itself (matches after `#`) and all tests
@@ -245,7 +245,7 @@ module RubyLsp
245
245
  end
246
246
  elsif method_name
247
247
  # We know the entire path, do an exact match
248
- " --name " + Shellwords.escape(group_stack.join("::") + "#" + method_name)
248
+ " --name " + Shellwords.escape(group_stack.join("::")) + "#" + Shellwords.escape(method_name)
249
249
  elsif spec_name
250
250
  " --name " + "/#{Shellwords.escape(spec_name)}/"
251
251
  else