ruby-lsp 0.17.17 → 0.18.3

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -110
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +5 -11
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +14 -6
  6. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +162 -27
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +110 -8
  8. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +24 -10
  10. data/lib/ruby_indexer/test/constant_test.rb +4 -4
  11. data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
  12. data/lib/ruby_indexer/test/index_test.rb +68 -0
  13. data/lib/ruby_indexer/test/method_test.rb +257 -2
  14. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  15. data/lib/ruby_lsp/base_server.rb +21 -1
  16. data/lib/ruby_lsp/document.rb +5 -3
  17. data/lib/ruby_lsp/erb_document.rb +29 -10
  18. data/lib/ruby_lsp/global_state.rb +4 -3
  19. data/lib/ruby_lsp/internal.rb +40 -2
  20. data/lib/ruby_lsp/listeners/code_lens.rb +34 -5
  21. data/lib/ruby_lsp/listeners/completion.rb +20 -6
  22. data/lib/ruby_lsp/listeners/inlay_hints.rb +1 -16
  23. data/lib/ruby_lsp/listeners/signature_help.rb +55 -24
  24. data/lib/ruby_lsp/rbs_document.rb +5 -4
  25. data/lib/ruby_lsp/requests/code_action_resolve.rb +0 -15
  26. data/lib/ruby_lsp/requests/code_actions.rb +0 -10
  27. data/lib/ruby_lsp/requests/code_lens.rb +1 -11
  28. data/lib/ruby_lsp/requests/completion.rb +3 -20
  29. data/lib/ruby_lsp/requests/completion_resolve.rb +2 -10
  30. data/lib/ruby_lsp/requests/definition.rb +6 -20
  31. data/lib/ruby_lsp/requests/diagnostics.rb +0 -10
  32. data/lib/ruby_lsp/requests/document_highlight.rb +7 -14
  33. data/lib/ruby_lsp/requests/document_link.rb +0 -10
  34. data/lib/ruby_lsp/requests/document_symbol.rb +0 -17
  35. data/lib/ruby_lsp/requests/folding_ranges.rb +0 -10
  36. data/lib/ruby_lsp/requests/formatting.rb +0 -16
  37. data/lib/ruby_lsp/requests/hover.rb +9 -9
  38. data/lib/ruby_lsp/requests/inlay_hints.rb +2 -35
  39. data/lib/ruby_lsp/requests/on_type_formatting.rb +0 -10
  40. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +0 -11
  41. data/lib/ruby_lsp/requests/request.rb +17 -1
  42. data/lib/ruby_lsp/requests/selection_ranges.rb +0 -10
  43. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -23
  44. data/lib/ruby_lsp/requests/show_syntax_tree.rb +0 -11
  45. data/lib/ruby_lsp/requests/signature_help.rb +5 -20
  46. data/lib/ruby_lsp/requests/support/common.rb +1 -1
  47. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -0
  48. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +2 -0
  49. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +0 -11
  50. data/lib/ruby_lsp/requests/workspace_symbol.rb +0 -12
  51. data/lib/ruby_lsp/ruby_document.rb +4 -3
  52. data/lib/ruby_lsp/server.rb +45 -11
  53. data/lib/ruby_lsp/setup_bundler.rb +33 -15
  54. data/lib/ruby_lsp/type_inferrer.rb +8 -10
  55. data/lib/ruby_lsp/utils.rb +11 -1
  56. metadata +3 -6
  57. data/lib/ruby_lsp/check_docs.rb +0 -130
  58. data/lib/ruby_lsp/requests.rb +0 -64
  59. data/lib/ruby_lsp/response_builders.rb +0 -13
@@ -242,6 +242,64 @@ module RubyIndexer
242
242
  completion_items.values.map!(&:first)
243
243
  end
244
244
 
245
+ sig do
246
+ params(
247
+ name: String,
248
+ nesting: T::Array[String],
249
+ ).returns(T::Array[T::Array[T.any(
250
+ Entry::Constant,
251
+ Entry::ConstantAlias,
252
+ Entry::Namespace,
253
+ Entry::UnresolvedConstantAlias,
254
+ )]])
255
+ end
256
+ def constant_completion_candidates(name, nesting)
257
+ # If we have a top level reference, then we don't need to include completions inside the current nesting
258
+ if name.start_with?("::")
259
+ return T.cast(
260
+ @entries_tree.search(name.delete_prefix("::")),
261
+ T::Array[T::Array[T.any(
262
+ Entry::Constant,
263
+ Entry::ConstantAlias,
264
+ Entry::Namespace,
265
+ Entry::UnresolvedConstantAlias,
266
+ )]],
267
+ )
268
+ end
269
+
270
+ # Otherwise, we have to include every possible constant the user might be referring to. This is essentially the
271
+ # same algorithm as resolve, but instead of returning early we concatenate all unique results
272
+
273
+ # Direct constants inside this namespace
274
+ entries = @entries_tree.search(nesting.any? ? "#{nesting.join("::")}::#{name}" : name)
275
+
276
+ # Constants defined in enclosing scopes
277
+ nesting.length.downto(1) do |i|
278
+ namespace = T.must(nesting[0...i]).join("::")
279
+ entries.concat(@entries_tree.search("#{namespace}::#{name}"))
280
+ end
281
+
282
+ # Inherited constants
283
+ if name.end_with?("::")
284
+ entries.concat(inherited_constant_completion_candidates(nil, nesting + [name]))
285
+ else
286
+ entries.concat(inherited_constant_completion_candidates(name, nesting))
287
+ end
288
+
289
+ # Top level constants
290
+ entries.concat(@entries_tree.search(name))
291
+ entries.uniq!
292
+ T.cast(
293
+ entries,
294
+ T::Array[T::Array[T.any(
295
+ Entry::Constant,
296
+ Entry::ConstantAlias,
297
+ Entry::Namespace,
298
+ Entry::UnresolvedConstantAlias,
299
+ )]],
300
+ )
301
+ end
302
+
245
303
  # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
246
304
  # documentation:
247
305
  #
@@ -312,12 +370,12 @@ module RubyIndexer
312
370
  break unless block.call(progress)
313
371
  end
314
372
 
315
- index_single(path)
373
+ index_single(path, collect_comments: false)
316
374
  end
317
375
  end
318
376
 
319
- sig { params(indexable_path: IndexablePath, source: T.nilable(String)).void }
320
- def index_single(indexable_path, source = nil)
377
+ sig { params(indexable_path: IndexablePath, source: T.nilable(String), collect_comments: T::Boolean).void }
378
+ def index_single(indexable_path, source = nil, collect_comments: true)
321
379
  content = source || File.read(indexable_path.full_path)
322
380
  dispatcher = Prism::Dispatcher.new
323
381
 
@@ -327,6 +385,7 @@ module RubyIndexer
327
385
  dispatcher,
328
386
  result,
329
387
  indexable_path.full_path,
388
+ collect_comments: collect_comments,
330
389
  enhancements: @enhancements,
331
390
  )
332
391
  dispatcher.dispatch(result.value)
@@ -364,12 +423,10 @@ module RubyIndexer
364
423
  # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
365
424
  sig { params(name: String, seen_names: T::Array[String]).returns(String) }
366
425
  def follow_aliased_namespace(name, seen_names = [])
367
- return name if @entries[name]
368
-
369
426
  parts = name.split("::")
370
427
  real_parts = []
371
428
 
372
- (parts.length - 1).downto(0).each do |i|
429
+ (parts.length - 1).downto(0) do |i|
373
430
  current_name = T.must(parts[0..i]).join("::")
374
431
  entry = @entries[current_name]&.first
375
432
 
@@ -607,7 +664,7 @@ module RubyIndexer
607
664
  attached_ancestor.file_path,
608
665
  attached_ancestor.location,
609
666
  attached_ancestor.name_location,
610
- [],
667
+ nil,
611
668
  nil,
612
669
  )
613
670
  add(singleton, skip_prefix_tree: true)
@@ -823,7 +880,7 @@ module RubyIndexer
823
880
  )]))
824
881
  end
825
882
  def lookup_enclosing_scopes(name, nesting, seen_names)
826
- nesting.length.downto(1).each do |i|
883
+ nesting.length.downto(1) do |i|
827
884
  namespace = T.must(nesting[0...i]).join("::")
828
885
 
829
886
  # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
@@ -870,6 +927,51 @@ module RubyIndexer
870
927
  nil
871
928
  end
872
929
 
930
+ sig do
931
+ params(
932
+ name: T.nilable(String),
933
+ nesting: T::Array[String],
934
+ ).returns(T::Array[T::Array[T.any(
935
+ Entry::Namespace,
936
+ Entry::ConstantAlias,
937
+ Entry::UnresolvedConstantAlias,
938
+ Entry::Constant,
939
+ )]])
940
+ end
941
+ def inherited_constant_completion_candidates(name, nesting)
942
+ namespace_entries = if name
943
+ *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
944
+ return [] if nesting_parts.empty?
945
+
946
+ resolve(nesting_parts.join("::"), [])
947
+ else
948
+ resolve(nesting.join("::"), [])
949
+ end
950
+ return [] unless namespace_entries
951
+
952
+ ancestors = linearized_ancestors_of(T.must(namespace_entries.first).name)
953
+ candidates = ancestors.flat_map do |ancestor_name|
954
+ @entries_tree.search("#{ancestor_name}::#{constant_name}")
955
+ end
956
+
957
+ # For candidates with the same name, we must only show the first entry in the inheritance chain, since that's the
958
+ # one the user will be referring to in completion
959
+ completion_items = candidates.each_with_object({}) do |entries, hash|
960
+ *parts, short_name = T.must(entries.first).name.split("::")
961
+ namespace_name = parts.join("::")
962
+ ancestor_index = ancestors.index(namespace_name)
963
+ existing_entry, existing_entry_index = hash[short_name]
964
+
965
+ next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
966
+
967
+ hash[short_name] = [entries, ancestor_index]
968
+ end
969
+
970
+ completion_items.values.map!(&:first)
971
+ rescue NonExistingNamespaceError
972
+ []
973
+ end
974
+
873
975
  # Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
874
976
  # of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
875
977
  # `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
@@ -272,10 +272,10 @@ module RubyIndexer
272
272
  RBS::AST::Declarations::Constant,
273
273
  RBS::AST::Members::MethodDefinition,
274
274
  RBS::AST::Members::Alias,
275
- )).returns(T::Array[String])
275
+ )).returns(T.nilable(String))
276
276
  end
277
277
  def comments_to_string(declaration)
278
- Array(declaration.comment&.string)
278
+ declaration.comment&.string
279
279
  end
280
280
  end
281
281
  end
@@ -213,10 +213,10 @@ module RubyIndexer
213
213
  RUBY
214
214
 
215
215
  foo_entry = @index["Foo"].first
216
- assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
216
+ assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
217
217
 
218
218
  bar_entry = @index["Bar"].first
219
- assert_equal("This Bar comment has 1 line padding", bar_entry.comments.join("\n"))
219
+ assert_equal("This Bar comment has 1 line padding", bar_entry.comments)
220
220
  end
221
221
 
222
222
  def test_skips_comments_containing_invalid_encodings
@@ -239,10 +239,10 @@ module RubyIndexer
239
239
  RUBY
240
240
 
241
241
  foo_entry = @index["Foo"].first
242
- assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
242
+ assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
243
243
 
244
244
  bar_entry = @index["Foo::Bar"].first
245
- assert_equal("This is a Bar comment", bar_entry.comments.join("\n"))
245
+ assert_equal("This is a Bar comment", bar_entry.comments)
246
246
  end
247
247
 
248
248
  def test_comments_can_be_attached_to_a_reopened_class
@@ -255,10 +255,10 @@ module RubyIndexer
255
255
  RUBY
256
256
 
257
257
  first_foo_entry = @index["Foo"][0]
258
- assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
258
+ assert_equal("This is a Foo comment", first_foo_entry.comments)
259
259
 
260
260
  second_foo_entry = @index["Foo"][1]
261
- assert_equal("This is another Foo comment", second_foo_entry.comments.join("\n"))
261
+ assert_equal("This is another Foo comment", second_foo_entry.comments)
262
262
  end
263
263
 
264
264
  def test_comments_removes_the_leading_pound_and_space
@@ -271,10 +271,10 @@ module RubyIndexer
271
271
  RUBY
272
272
 
273
273
  first_foo_entry = @index["Foo"][0]
274
- assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
274
+ assert_equal("This is a Foo comment", first_foo_entry.comments)
275
275
 
276
276
  second_foo_entry = @index["Bar"][0]
277
- assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n"))
277
+ assert_equal("This is a Bar comment", second_foo_entry.comments)
278
278
  end
279
279
 
280
280
  def test_private_class_and_module_indexing
@@ -483,7 +483,7 @@ module RubyIndexer
483
483
 
484
484
  foo = T.must(@index["Foo::<Class:Foo>"].first)
485
485
  assert_equal(4, foo.location.start_line)
486
- assert_equal("Some extra comments", foo.comments.join("\n"))
486
+ assert_equal("Some extra comments", foo.comments)
487
487
  end
488
488
 
489
489
  def test_dynamic_singleton_class_blocks
@@ -501,7 +501,7 @@ module RubyIndexer
501
501
  # That pattern cannot be properly analyzed statically and assuming that it's always a regular singleton simplifies
502
502
  # the implementation considerably.
503
503
  assert_equal(3, singleton.location.start_line)
504
- assert_equal("Some extra comments", singleton.comments.join("\n"))
504
+ assert_equal("Some extra comments", singleton.comments)
505
505
  end
506
506
 
507
507
  def test_namespaces_inside_singleton_blocks
@@ -605,5 +605,19 @@ module RubyIndexer
605
605
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7")
606
606
  assert_entry("Qux", Entry::Class, "/fake/path/foo.rb:5-4:6-7")
607
607
  end
608
+
609
+ def test_lazy_comment_fetching_uses_correct_line_breaks_for_rendering
610
+ path = "lib/ruby_lsp/node_context.rb"
611
+ indexable = IndexablePath.new("#{Dir.pwd}/lib", path)
612
+
613
+ @index.index_single(indexable, collect_comments: false)
614
+
615
+ entry = @index["RubyLsp::NodeContext"].first
616
+
617
+ assert_equal(<<~COMMENTS.chomp, entry.comments)
618
+ This class allows listeners to access contextual information about a node in the AST, such as its parent,
619
+ its namespace nesting, and the surrounding CallNode (e.g. a method call).
620
+ COMMENTS
621
+ end
608
622
  end
609
623
  end
@@ -86,16 +86,16 @@ module RubyIndexer
86
86
  A::BAZ = 1
87
87
  RUBY
88
88
 
89
- foo_comment = @index["FOO"].first.comments.join("\n")
89
+ foo_comment = @index["FOO"].first.comments
90
90
  assert_equal("FOO comment", foo_comment)
91
91
 
92
- a_foo_comment = @index["A::FOO"].first.comments.join("\n")
92
+ a_foo_comment = @index["A::FOO"].first.comments
93
93
  assert_equal("A::FOO comment", a_foo_comment)
94
94
 
95
- bar_comment = @index["BAR"].first.comments.join("\n")
95
+ bar_comment = @index["BAR"].first.comments
96
96
  assert_equal("::BAR comment", bar_comment)
97
97
 
98
- a_baz_comment = @index["A::BAZ"].first.comments.join("\n")
98
+ a_baz_comment = @index["A::BAZ"].first.comments
99
99
  assert_equal("A::BAZ comment", a_baz_comment)
100
100
  end
101
101
 
@@ -38,7 +38,7 @@ module RubyIndexer
38
38
  file_path,
39
39
  location,
40
40
  location,
41
- [],
41
+ nil,
42
42
  [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
43
43
  Entry::Visibility::PUBLIC,
44
44
  owner,
@@ -120,7 +120,7 @@ module RubyIndexer
120
120
  file_path,
121
121
  location,
122
122
  location,
123
- [],
123
+ nil,
124
124
  [],
125
125
  Entry::Visibility::PUBLIC,
126
126
  owner,
@@ -1863,5 +1863,73 @@ module RubyIndexer
1863
1863
  def test_entries_for_returns_nil_if_no_matches
1864
1864
  assert_nil(@index.entries_for("non_existing_file.rb", Entry::Namespace))
1865
1865
  end
1866
+
1867
+ def test_constant_completion_candidates_all_possible_constants
1868
+ index(<<~RUBY)
1869
+ XQRK = 3
1870
+
1871
+ module Bar
1872
+ XQRK = 2
1873
+ end
1874
+
1875
+ module Foo
1876
+ XQRK = 1
1877
+ end
1878
+
1879
+ module Namespace
1880
+ XQRK = 0
1881
+
1882
+ class Baz
1883
+ include Foo
1884
+ include Bar
1885
+ end
1886
+ end
1887
+ RUBY
1888
+
1889
+ result = @index.constant_completion_candidates("X", ["Namespace", "Baz"])
1890
+
1891
+ result.each do |entries|
1892
+ name = entries.first.name
1893
+ assert(entries.all? { |e| e.name == name })
1894
+ end
1895
+
1896
+ assert_equal(["Namespace::XQRK", "Bar::XQRK", "XQRK"], result.map { |entries| entries.first.name })
1897
+
1898
+ result = @index.constant_completion_candidates("::X", ["Namespace", "Baz"])
1899
+ assert_equal(["XQRK"], result.map { |entries| entries.first.name })
1900
+ end
1901
+
1902
+ def test_constant_completion_candidates_for_empty_name
1903
+ index(<<~RUBY)
1904
+ module Foo
1905
+ Bar = 1
1906
+ end
1907
+
1908
+ class Baz
1909
+ include Foo
1910
+ end
1911
+ RUBY
1912
+
1913
+ result = @index.constant_completion_candidates("Baz::", [])
1914
+ assert_includes(result.map { |entries| entries.first.name }, "Foo::Bar")
1915
+ end
1916
+
1917
+ def test_follow_alias_namespace
1918
+ index(<<~RUBY)
1919
+ module First
1920
+ module Second
1921
+ class Foo
1922
+ end
1923
+ end
1924
+ end
1925
+
1926
+ module Namespace
1927
+ Second = First::Second
1928
+ end
1929
+ RUBY
1930
+
1931
+ real_namespace = @index.follow_aliased_namespace("Namespace::Second")
1932
+ assert_equal("First::Second", real_namespace)
1933
+ end
1866
1934
  end
1867
1935
  end
@@ -330,6 +330,33 @@ module RubyIndexer
330
330
  assert_empty(parameters)
331
331
  end
332
332
 
333
+ def test_methods_with_argument_forwarding
334
+ index(<<~RUBY)
335
+ class Foo
336
+ def bar(...)
337
+ end
338
+
339
+ def baz(a, ...)
340
+ end
341
+ end
342
+ RUBY
343
+
344
+ entry = T.must(@index["bar"].first)
345
+ assert_instance_of(Entry::Method, entry, "Expected `bar` to be indexed")
346
+
347
+ parameters = entry.signatures.first.parameters
348
+ assert_equal(1, parameters.length)
349
+ assert_instance_of(Entry::ForwardingParameter, parameters.first)
350
+
351
+ entry = T.must(@index["baz"].first)
352
+ assert_instance_of(Entry::Method, entry, "Expected `baz` to be indexed")
353
+
354
+ parameters = entry.signatures.first.parameters
355
+ assert_equal(2, parameters.length)
356
+ assert_instance_of(Entry::RequiredParameter, parameters[0])
357
+ assert_instance_of(Entry::ForwardingParameter, parameters[1])
358
+ end
359
+
333
360
  def test_keeps_track_of_method_owner
334
361
  index(<<~RUBY)
335
362
  class Foo
@@ -355,9 +382,9 @@ module RubyIndexer
355
382
  RUBY
356
383
 
357
384
  assert_entry("bar", Entry::Accessor, "/fake/path/foo.rb:2-15:2-18")
358
- assert_equal("Hello there", @index["bar"].first.comments.join("\n"))
385
+ assert_equal("Hello there", @index["bar"].first.comments)
359
386
  assert_entry("other", Entry::Accessor, "/fake/path/foo.rb:2-21:2-26")
360
- assert_equal("Hello there", @index["other"].first.comments.join("\n"))
387
+ assert_equal("Hello there", @index["other"].first.comments)
361
388
  assert_entry("baz=", Entry::Accessor, "/fake/path/foo.rb:3-15:3-18")
362
389
  assert_entry("qux", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20")
363
390
  assert_entry("qux=", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20")
@@ -461,5 +488,233 @@ module RubyIndexer
461
488
  assert_equal(6, name_location.start_column)
462
489
  assert_equal(9, name_location.end_column)
463
490
  end
491
+
492
+ def test_signature_matches_for_a_method_with_positional_params
493
+ index(<<~RUBY)
494
+ class Foo
495
+ def bar(a, b = 123)
496
+ end
497
+ end
498
+ RUBY
499
+
500
+ entry = T.must(@index["bar"].first)
501
+
502
+ # Matching calls
503
+ assert_signature_matches(entry, "bar()")
504
+ assert_signature_matches(entry, "bar(1)")
505
+ assert_signature_matches(entry, "bar(1, 2)")
506
+ assert_signature_matches(entry, "bar(...)")
507
+ assert_signature_matches(entry, "bar(1, ...)")
508
+ assert_signature_matches(entry, "bar(*a)")
509
+ assert_signature_matches(entry, "bar(1, *a)")
510
+ assert_signature_matches(entry, "bar(1, *a, 2)")
511
+ assert_signature_matches(entry, "bar(*a, 2)")
512
+ assert_signature_matches(entry, "bar(1, **a)")
513
+ assert_signature_matches(entry, "bar(1) {}")
514
+ # This call is impossible to analyze statically because it depends on whether there are elements inside `a` or
515
+ # not. If there's nothing, the call will fail. But if there's anything inside, the hash will become the first
516
+ # positional argument
517
+ assert_signature_matches(entry, "bar(**a)")
518
+
519
+ # Non matching calls
520
+
521
+ refute_signature_matches(entry, "bar(1, 2, 3)")
522
+ refute_signature_matches(entry, "bar(1, b: 2)")
523
+ refute_signature_matches(entry, "bar(1, 2, c: 3)")
524
+ end
525
+
526
+ def test_signature_matches_for_a_method_with_argument_forwarding
527
+ index(<<~RUBY)
528
+ class Foo
529
+ def bar(...)
530
+ end
531
+ end
532
+ RUBY
533
+
534
+ entry = T.must(@index["bar"].first)
535
+
536
+ # All calls match a forwarding parameter
537
+ assert_signature_matches(entry, "bar(1)")
538
+ assert_signature_matches(entry, "bar(1, 2)")
539
+ assert_signature_matches(entry, "bar(...)")
540
+ assert_signature_matches(entry, "bar(1, ...)")
541
+ assert_signature_matches(entry, "bar(*a)")
542
+ assert_signature_matches(entry, "bar(1, *a)")
543
+ assert_signature_matches(entry, "bar(1, *a, 2)")
544
+ assert_signature_matches(entry, "bar(*a, 2)")
545
+ assert_signature_matches(entry, "bar(1, **a)")
546
+ assert_signature_matches(entry, "bar(1) {}")
547
+ assert_signature_matches(entry, "bar()")
548
+ assert_signature_matches(entry, "bar(1, 2, 3)")
549
+ assert_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
550
+ end
551
+
552
+ def test_signature_matches_for_post_forwarding_parameter
553
+ index(<<~RUBY)
554
+ class Foo
555
+ def bar(a, ...)
556
+ end
557
+ end
558
+ RUBY
559
+
560
+ entry = T.must(@index["bar"].first)
561
+
562
+ # All calls with at least one positional argument match
563
+ assert_signature_matches(entry, "bar(1)")
564
+ assert_signature_matches(entry, "bar(1, 2)")
565
+ assert_signature_matches(entry, "bar(...)")
566
+ assert_signature_matches(entry, "bar(1, ...)")
567
+ assert_signature_matches(entry, "bar(*a)")
568
+ assert_signature_matches(entry, "bar(1, *a)")
569
+ assert_signature_matches(entry, "bar(1, *a, 2)")
570
+ assert_signature_matches(entry, "bar(*a, 2)")
571
+ assert_signature_matches(entry, "bar(1, **a)")
572
+ assert_signature_matches(entry, "bar(1) {}")
573
+ assert_signature_matches(entry, "bar(1, 2, 3)")
574
+ assert_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
575
+ assert_signature_matches(entry, "bar()")
576
+ end
577
+
578
+ def test_signature_matches_for_destructured_parameters
579
+ index(<<~RUBY)
580
+ class Foo
581
+ def bar(a, (b, c))
582
+ end
583
+ end
584
+ RUBY
585
+
586
+ entry = T.must(@index["bar"].first)
587
+
588
+ # All calls with at least one positional argument match
589
+ assert_signature_matches(entry, "bar()")
590
+ assert_signature_matches(entry, "bar(1)")
591
+ assert_signature_matches(entry, "bar(1, 2)")
592
+ assert_signature_matches(entry, "bar(...)")
593
+ assert_signature_matches(entry, "bar(1, ...)")
594
+ assert_signature_matches(entry, "bar(*a)")
595
+ assert_signature_matches(entry, "bar(1, *a)")
596
+ assert_signature_matches(entry, "bar(*a, 2)")
597
+ # This matches because `bar(1, *[], 2)` would result in `bar(1, 2)`, which is a valid call
598
+ assert_signature_matches(entry, "bar(1, *a, 2)")
599
+ assert_signature_matches(entry, "bar(1, **a)")
600
+ assert_signature_matches(entry, "bar(1) {}")
601
+
602
+ refute_signature_matches(entry, "bar(1, 2, 3)")
603
+ refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
604
+ end
605
+
606
+ def test_signature_matches_for_post_parameters
607
+ index(<<~RUBY)
608
+ class Foo
609
+ def bar(*splat, a)
610
+ end
611
+ end
612
+ RUBY
613
+
614
+ entry = T.must(@index["bar"].first)
615
+
616
+ # All calls with at least one positional argument match
617
+ assert_signature_matches(entry, "bar(1)")
618
+ assert_signature_matches(entry, "bar(1, 2)")
619
+ assert_signature_matches(entry, "bar(...)")
620
+ assert_signature_matches(entry, "bar(1, ...)")
621
+ assert_signature_matches(entry, "bar(*a)")
622
+ assert_signature_matches(entry, "bar(1, *a)")
623
+ assert_signature_matches(entry, "bar(*a, 2)")
624
+ assert_signature_matches(entry, "bar(1, *a, 2)")
625
+ assert_signature_matches(entry, "bar(1, **a)")
626
+ assert_signature_matches(entry, "bar(1, 2, 3)")
627
+ assert_signature_matches(entry, "bar(1) {}")
628
+ assert_signature_matches(entry, "bar()")
629
+
630
+ refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
631
+ end
632
+
633
+ def test_signature_matches_for_keyword_parameters
634
+ index(<<~RUBY)
635
+ class Foo
636
+ def bar(a:, b: 123)
637
+ end
638
+ end
639
+ RUBY
640
+
641
+ entry = T.must(@index["bar"].first)
642
+
643
+ assert_signature_matches(entry, "bar(...)")
644
+ assert_signature_matches(entry, "bar()")
645
+ assert_signature_matches(entry, "bar(a: 1)")
646
+ assert_signature_matches(entry, "bar(a: 1, b: 32)")
647
+
648
+ refute_signature_matches(entry, "bar(a: 1, c: 2)")
649
+ refute_signature_matches(entry, "bar(1, ...)")
650
+ refute_signature_matches(entry, "bar(1) {}")
651
+ refute_signature_matches(entry, "bar(1, *a)")
652
+ refute_signature_matches(entry, "bar(*a, 2)")
653
+ refute_signature_matches(entry, "bar(1, *a, 2)")
654
+ refute_signature_matches(entry, "bar(1, **a)")
655
+ refute_signature_matches(entry, "bar(*a)")
656
+ refute_signature_matches(entry, "bar(1)")
657
+ refute_signature_matches(entry, "bar(1, 2)")
658
+ refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
659
+ end
660
+
661
+ def test_signature_matches_for_keyword_splats
662
+ index(<<~RUBY)
663
+ class Foo
664
+ def bar(a, b:, **kwargs)
665
+ end
666
+ end
667
+ RUBY
668
+
669
+ entry = T.must(@index["bar"].first)
670
+
671
+ assert_signature_matches(entry, "bar(...)")
672
+ assert_signature_matches(entry, "bar()")
673
+ assert_signature_matches(entry, "bar(1)")
674
+ assert_signature_matches(entry, "bar(1, b: 2)")
675
+ assert_signature_matches(entry, "bar(1, b: 2, c: 3, d: 4)")
676
+
677
+ refute_signature_matches(entry, "bar(1, 2, b: 2)")
678
+ end
679
+
680
+ def test_partial_signature_matches
681
+ # It's important to match signatures partially, because we want to figure out which signature we should show while
682
+ # the user is in the middle of typing
683
+ index(<<~RUBY)
684
+ class Foo
685
+ def bar(a:, b:)
686
+ end
687
+
688
+ def baz(a, b)
689
+ end
690
+ end
691
+ RUBY
692
+
693
+ entry = T.must(@index["bar"].first)
694
+ assert_signature_matches(entry, "bar(a: 1)")
695
+
696
+ entry = T.must(@index["baz"].first)
697
+ assert_signature_matches(entry, "baz(1)")
698
+ end
699
+
700
+ private
701
+
702
+ sig { params(entry: Entry::Method, call_string: String).void }
703
+ def assert_signature_matches(entry, call_string)
704
+ sig = T.must(entry.signatures.first)
705
+ arguments = parse_prism_args(call_string)
706
+ assert(sig.matches?(arguments), "Expected #{call_string} to match #{entry.name}#{entry.decorated_parameters}")
707
+ end
708
+
709
+ sig { params(entry: Entry::Method, call_string: String).void }
710
+ def refute_signature_matches(entry, call_string)
711
+ sig = T.must(entry.signatures.first)
712
+ arguments = parse_prism_args(call_string)
713
+ refute(sig.matches?(arguments), "Expected #{call_string} to not match #{entry.name}#{entry.decorated_parameters}")
714
+ end
715
+
716
+ def parse_prism_args(s)
717
+ Array(Prism.parse(s).value.statements.body.first.arguments&.arguments)
718
+ end
464
719
  end
465
720
  end
@@ -345,7 +345,7 @@ module RubyIndexer
345
345
  assert_equal("all?", entry.old_name)
346
346
  assert_equal("Array", entry.owner.name)
347
347
  assert(entry.file_path.end_with?("core/array.rbs"))
348
- assert_includes(entry.comments[0], "Returns `true` if any element of `self` meets a given criterion.")
348
+ assert_includes(entry.comments, "Returns `true` if any element of `self` meets a given criterion.")
349
349
  end
350
350
 
351
351
  private