ruby-lsp 0.16.7 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) 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. 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