ruby-lsp 0.17.17 → 0.18.3

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