ruby-lsp 0.16.7 → 0.17.1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +139 -18
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +101 -12
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +183 -10
  7. data/lib/ruby_indexer/test/classes_and_modules_test.rb +55 -9
  8. data/lib/ruby_indexer/test/configuration_test.rb +4 -5
  9. data/lib/ruby_indexer/test/constant_test.rb +8 -8
  10. data/lib/ruby_indexer/test/index_test.rb +528 -0
  11. data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
  12. data/lib/ruby_indexer/test/method_test.rb +37 -0
  13. data/lib/ruby_indexer/test/test_case.rb +3 -1
  14. data/lib/ruby_lsp/addon.rb +8 -8
  15. data/lib/ruby_lsp/document.rb +3 -3
  16. data/lib/ruby_lsp/internal.rb +1 -0
  17. data/lib/ruby_lsp/listeners/completion.rb +74 -17
  18. data/lib/ruby_lsp/listeners/definition.rb +62 -6
  19. data/lib/ruby_lsp/listeners/hover.rb +60 -6
  20. data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
  21. data/lib/ruby_lsp/node_context.rb +28 -0
  22. data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
  23. data/lib/ruby_lsp/requests/code_actions.rb +16 -15
  24. data/lib/ruby_lsp/requests/completion.rb +21 -13
  25. data/lib/ruby_lsp/requests/completion_resolve.rb +9 -0
  26. data/lib/ruby_lsp/requests/definition.rb +20 -5
  27. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  28. data/lib/ruby_lsp/requests/hover.rb +5 -6
  29. data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
  30. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  31. data/lib/ruby_lsp/requests/support/common.rb +4 -3
  32. data/lib/ruby_lsp/requests/workspace_symbol.rb +3 -1
  33. data/lib/ruby_lsp/server.rb +10 -4
  34. data/lib/ruby_lsp/test_helper.rb +1 -0
  35. metadata +4 -2
@@ -6,6 +6,7 @@ module RubyIndexer
6
6
  extend T::Sig
7
7
 
8
8
  class UnresolvableAliasError < StandardError; end
9
+ class NonExistingNamespaceError < StandardError; end
9
10
 
10
11
  # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
11
12
  ENTRY_SIMILARITY_THRESHOLD = 0.7
@@ -31,6 +32,9 @@ module RubyIndexer
31
32
 
32
33
  # Holds all require paths for every indexed item so that we can provide autocomplete for requires
33
34
  @require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
35
+
36
+ # Holds the linearized ancestors list for every namespace
37
+ @ancestors = T.let({}, T::Hash[String, T::Array[String]])
34
38
  end
35
39
 
36
40
  sig { params(indexable: IndexablePath).void }
@@ -126,6 +130,16 @@ module RubyIndexer
126
130
  results.flat_map(&:first)
127
131
  end
128
132
 
133
+ sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) }
134
+ def method_completion_candidates(name, receiver_name)
135
+ ancestors = linearized_ancestors_of(receiver_name)
136
+ candidates = prefix_search(name).flatten
137
+ candidates.select! do |entry|
138
+ entry.is_a?(RubyIndexer::Entry::Member) && ancestors.any?(entry.owner&.name)
139
+ end
140
+ candidates
141
+ end
142
+
129
143
  # Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
130
144
  # the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
131
145
  # 1. Foo::Bar::Baz
@@ -245,16 +259,175 @@ module RubyIndexer
245
259
  sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
246
260
  def resolve_method(method_name, receiver_name)
247
261
  method_entries = self[method_name]
248
- owner_entries = self[receiver_name]
249
- return unless owner_entries && method_entries
250
-
251
- owner_name = T.must(owner_entries.first).name
252
- T.cast(
253
- method_entries.grep(Entry::Member).select do |entry|
254
- T.cast(entry, Entry::Member).owner&.name == owner_name
255
- end,
256
- T::Array[Entry::Member],
257
- )
262
+ ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
263
+ return unless method_entries
264
+
265
+ ancestors.each do |ancestor|
266
+ found = method_entries.select do |entry|
267
+ next unless entry.is_a?(Entry::Member)
268
+
269
+ entry.owner&.name == ancestor
270
+ end
271
+
272
+ return T.cast(found, T::Array[Entry::Member]) if found.any?
273
+ end
274
+
275
+ nil
276
+ rescue NonExistingNamespaceError
277
+ nil
278
+ end
279
+
280
+ # Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
281
+ # or constant declarations.
282
+ #
283
+ # When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
284
+ # everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
285
+ # module that prepends another module, then the prepend module appears before the included module.
286
+ #
287
+ # The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
288
+ sig { params(fully_qualified_name: String).returns(T::Array[String]) }
289
+ def linearized_ancestors_of(fully_qualified_name)
290
+ # If we already computed the ancestors for this namespace, return it straight away
291
+ cached_ancestors = @ancestors[fully_qualified_name]
292
+ return cached_ancestors if cached_ancestors
293
+
294
+ ancestors = [fully_qualified_name]
295
+
296
+ # Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
297
+ # this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
298
+ # the cache will reflect the final result
299
+ @ancestors[fully_qualified_name] = ancestors
300
+
301
+ # If we don't have an entry for `name`, raise
302
+ entries = resolve(fully_qualified_name, [])
303
+ raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
304
+
305
+ # If none of the entries for `name` are namespaces, raise
306
+ namespaces = entries.filter_map do |entry|
307
+ case entry
308
+ when Entry::Namespace
309
+ entry
310
+ when Entry::Alias
311
+ self[entry.target]&.grep(Entry::Namespace)
312
+ end
313
+ end.flatten
314
+
315
+ raise NonExistingNamespaceError,
316
+ "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
317
+
318
+ mixin_operations = namespaces.flat_map(&:mixin_operations)
319
+ main_namespace_index = 0
320
+
321
+ # The original nesting where we discovered this namespace, so that we resolve the correct names of the
322
+ # included/prepended/extended modules and parent classes
323
+ nesting = T.must(namespaces.first).nesting
324
+
325
+ mixin_operations.each do |operation|
326
+ resolved_module = resolve(operation.module_name, nesting)
327
+ next unless resolved_module
328
+
329
+ module_fully_qualified_name = T.must(resolved_module.first).name
330
+
331
+ case operation
332
+ when Entry::Prepend
333
+ # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
334
+ # the actual namespace twice. However, it does not check if it has been included because you are allowed to
335
+ # prepend the same module after it has already been included
336
+ linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
337
+
338
+ # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
339
+ # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
340
+ # be inserted after `"A`
341
+ uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
342
+ insert_position = linearized_prepends.length - uniq_prepends.length
343
+
344
+ T.unsafe(ancestors).insert(
345
+ insert_position,
346
+ *(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
347
+ )
348
+
349
+ main_namespace_index += linearized_prepends.length
350
+ when Entry::Include
351
+ # When including a module, Ruby will always prevent duplicate entries in case the module has already been
352
+ # prepended or included
353
+ linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
354
+ T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
355
+ end
356
+ end
357
+
358
+ # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
359
+ # from two diffent classes in different files, we simply ignore it
360
+ superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
361
+
362
+ if superclass
363
+ # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
364
+ # error. We need to ensure that this isn't the case
365
+ parent_class = T.must(superclass.parent_class)
366
+
367
+ resolved_parent_class = resolve(parent_class, nesting)
368
+ parent_class_name = resolved_parent_class&.first&.name
369
+
370
+ if parent_class_name && fully_qualified_name != parent_class_name
371
+ ancestors.concat(linearized_ancestors_of(parent_class_name))
372
+ end
373
+ end
374
+
375
+ ancestors
376
+ end
377
+
378
+ # Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
379
+ # and find inherited instance variables as well
380
+ sig { params(variable_name: String, owner_name: String).returns(T.nilable(T::Array[Entry::InstanceVariable])) }
381
+ def resolve_instance_variable(variable_name, owner_name)
382
+ entries = T.cast(self[variable_name], T.nilable(T::Array[Entry::InstanceVariable]))
383
+ return unless entries
384
+
385
+ ancestors = linearized_ancestors_of(owner_name)
386
+ return if ancestors.empty?
387
+
388
+ entries.select { |e| ancestors.include?(e.owner&.name) }
389
+ end
390
+
391
+ # Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
392
+ # include the `@` prefix
393
+ sig { params(name: String, owner_name: String).returns(T::Array[Entry::InstanceVariable]) }
394
+ def instance_variable_completion_candidates(name, owner_name)
395
+ entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
396
+ ancestors = linearized_ancestors_of(owner_name)
397
+
398
+ variables = entries.uniq(&:name)
399
+ variables.select! { |e| ancestors.any?(e.owner&.name) }
400
+ variables
401
+ end
402
+
403
+ # Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed,
404
+ # removed declarations removed and that the ancestor linearization cache is cleared if necessary
405
+ sig { params(indexable: IndexablePath).void }
406
+ def handle_change(indexable)
407
+ original_entries = @files_to_entries[indexable.full_path]
408
+
409
+ delete(indexable)
410
+ index_single(indexable)
411
+
412
+ updated_entries = @files_to_entries[indexable.full_path]
413
+
414
+ return unless original_entries && updated_entries
415
+
416
+ # A change in one ancestor may impact several different others, which could be including that ancestor through
417
+ # indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
418
+ # ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
419
+ # ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
420
+ original_map = T.cast(
421
+ original_entries.select { |e| e.is_a?(Entry::Namespace) },
422
+ T::Array[Entry::Namespace],
423
+ ).to_h { |e| [e.name, e.ancestor_hash] }
424
+
425
+ updated_map = T.cast(
426
+ updated_entries.select { |e| e.is_a?(Entry::Namespace) },
427
+ T::Array[Entry::Namespace],
428
+ ).to_h { |e| [e.name, e.ancestor_hash] }
429
+
430
+ @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
258
431
  end
259
432
 
260
433
  private
@@ -290,13 +290,13 @@ module RubyIndexer
290
290
  RUBY
291
291
 
292
292
  b_const = @index["A::B"].first
293
- assert_equal(:private, b_const.visibility)
293
+ assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
294
294
 
295
295
  c_const = @index["A::C"].first
296
- assert_equal(:private, c_const.visibility)
296
+ assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
297
297
 
298
298
  d_const = @index["A::D"].first
299
- assert_equal(:public, d_const.visibility)
299
+ assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
300
300
  end
301
301
 
302
302
  def test_keeping_track_of_super_classes
@@ -369,13 +369,13 @@ module RubyIndexer
369
369
  RUBY
370
370
 
371
371
  foo = T.must(@index["Foo"][0])
372
- assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)
372
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
373
373
 
374
374
  qux = T.must(@index["Foo::Qux"][0])
375
- assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)
375
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
376
376
 
377
377
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
378
- assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
378
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
379
379
  end
380
380
 
381
381
  def test_keeping_track_of_prepended_modules
@@ -415,13 +415,59 @@ module RubyIndexer
415
415
  RUBY
416
416
 
417
417
  foo = T.must(@index["Foo"][0])
418
- assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.prepended_modules)
418
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
419
419
 
420
420
  qux = T.must(@index["Foo::Qux"][0])
421
- assert_equal(["Corge", "Corge", "Baz"], qux.prepended_modules)
421
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
422
422
 
423
423
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
424
- assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.prepended_modules)
424
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
425
+ end
426
+
427
+ def test_keeping_track_of_extended_modules
428
+ index(<<~RUBY)
429
+ class Foo
430
+ # valid syntaxes that we can index
431
+ extend A1
432
+ self.extend A2
433
+ extend A3, A4
434
+ self.extend A5, A6
435
+
436
+ # valid syntaxes that we cannot index because of their dynamic nature
437
+ extend some_variable_or_method_call
438
+ self.extend some_variable_or_method_call
439
+
440
+ def something
441
+ extend A7 # We should not index this because of this dynamic nature
442
+ end
443
+
444
+ # Valid inner class syntax definition with its own modules prepended
445
+ class Qux
446
+ extend Corge
447
+ self.extend Corge
448
+ extend Baz
449
+
450
+ extend some_variable_or_method_call
451
+ end
452
+ end
453
+
454
+ class ConstantPathReferences
455
+ extend Foo::Bar
456
+ self.extend Foo::Bar2
457
+
458
+ extend dynamic::Bar
459
+ extend Foo::
460
+ end
461
+ RUBY
462
+
463
+ foo = T.must(@index["Foo"][0])
464
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
465
+
466
+ qux = T.must(@index["Foo::Qux"][0])
467
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
468
+
469
+ constant_path_references = T.must(@index["ConstantPathReferences"][0])
470
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
425
471
  end
426
472
  end
427
473
  end
@@ -68,12 +68,11 @@ module RubyIndexer
68
68
  end
69
69
 
70
70
  def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project
71
- Bundler.settings.set_global("path", "vendor/bundle")
72
- config = Configuration.new
71
+ Bundler.settings.temporary(path: "vendor/bundle") do
72
+ config = Configuration.new
73
73
 
74
- assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
75
- ensure
76
- Bundler.settings.set_global("path", nil)
74
+ assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
75
+ end
77
76
  end
78
77
 
79
78
  def test_indexables_does_not_include_gems_own_installed_files
@@ -122,13 +122,13 @@ module RubyIndexer
122
122
  RUBY
123
123
 
124
124
  b_const = @index["A::B"].first
125
- assert_equal(:private, b_const.visibility)
125
+ assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
126
126
 
127
127
  c_const = @index["A::C"].first
128
- assert_equal(:private, c_const.visibility)
128
+ assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
129
129
 
130
130
  d_const = @index["A::D"].first
131
- assert_equal(:public, d_const.visibility)
131
+ assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
132
132
  end
133
133
 
134
134
  def test_marking_constants_as_private_reopening_namespaces
@@ -155,13 +155,13 @@ module RubyIndexer
155
155
  RUBY
156
156
 
157
157
  a_const = @index["A::B::CONST_A"].first
158
- assert_equal(:private, a_const.visibility)
158
+ assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
159
159
 
160
160
  b_const = @index["A::B::CONST_B"].first
161
- assert_equal(:private, b_const.visibility)
161
+ assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
162
162
 
163
163
  c_const = @index["A::B::CONST_C"].first
164
- assert_equal(:private, c_const.visibility)
164
+ assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
165
165
  end
166
166
 
167
167
  def test_marking_constants_as_private_with_receiver
@@ -179,10 +179,10 @@ module RubyIndexer
179
179
  RUBY
180
180
 
181
181
  a_const = @index["A::B::CONST_A"].first
182
- assert_equal(:private, a_const.visibility)
182
+ assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
183
183
 
184
184
  b_const = @index["A::B::CONST_B"].first
185
- assert_equal(:private, b_const.visibility)
185
+ assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
186
186
  end
187
187
 
188
188
  def test_indexing_constant_aliases