ruby-lsp 0.17.2 → 0.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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