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.
- checksums.yaml +4 -4
- data/README.md +4 -110
- data/VERSION +1 -1
- data/exe/ruby-lsp +5 -11
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +14 -6
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +162 -27
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +110 -8
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +24 -10
- data/lib/ruby_indexer/test/constant_test.rb +4 -4
- data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
- data/lib/ruby_indexer/test/index_test.rb +68 -0
- data/lib/ruby_indexer/test/method_test.rb +257 -2
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
- data/lib/ruby_lsp/base_server.rb +21 -1
- data/lib/ruby_lsp/document.rb +5 -3
- data/lib/ruby_lsp/erb_document.rb +29 -10
- data/lib/ruby_lsp/global_state.rb +4 -3
- data/lib/ruby_lsp/internal.rb +40 -2
- data/lib/ruby_lsp/listeners/code_lens.rb +34 -5
- data/lib/ruby_lsp/listeners/completion.rb +20 -6
- data/lib/ruby_lsp/listeners/inlay_hints.rb +1 -16
- data/lib/ruby_lsp/listeners/signature_help.rb +55 -24
- data/lib/ruby_lsp/rbs_document.rb +5 -4
- data/lib/ruby_lsp/requests/code_action_resolve.rb +0 -15
- data/lib/ruby_lsp/requests/code_actions.rb +0 -10
- data/lib/ruby_lsp/requests/code_lens.rb +1 -11
- data/lib/ruby_lsp/requests/completion.rb +3 -20
- data/lib/ruby_lsp/requests/completion_resolve.rb +2 -10
- data/lib/ruby_lsp/requests/definition.rb +6 -20
- data/lib/ruby_lsp/requests/diagnostics.rb +0 -10
- data/lib/ruby_lsp/requests/document_highlight.rb +7 -14
- data/lib/ruby_lsp/requests/document_link.rb +0 -10
- data/lib/ruby_lsp/requests/document_symbol.rb +0 -17
- data/lib/ruby_lsp/requests/folding_ranges.rb +0 -10
- data/lib/ruby_lsp/requests/formatting.rb +0 -16
- data/lib/ruby_lsp/requests/hover.rb +9 -9
- data/lib/ruby_lsp/requests/inlay_hints.rb +2 -35
- data/lib/ruby_lsp/requests/on_type_formatting.rb +0 -10
- data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +0 -11
- data/lib/ruby_lsp/requests/request.rb +17 -1
- data/lib/ruby_lsp/requests/selection_ranges.rb +0 -10
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -23
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +0 -11
- data/lib/ruby_lsp/requests/signature_help.rb +5 -20
- data/lib/ruby_lsp/requests/support/common.rb +1 -1
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -0
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +2 -0
- data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +0 -11
- data/lib/ruby_lsp/requests/workspace_symbol.rb +0 -12
- data/lib/ruby_lsp/ruby_document.rb +4 -3
- data/lib/ruby_lsp/server.rb +45 -11
- data/lib/ruby_lsp/setup_bundler.rb +33 -15
- data/lib/ruby_lsp/type_inferrer.rb +8 -10
- data/lib/ruby_lsp/utils.rb +11 -1
- metadata +3 -6
- data/lib/ruby_lsp/check_docs.rb +0 -130
- data/lib/ruby_lsp/requests.rb +0 -64
- 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)
|
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)
|
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
|
275
|
+
)).returns(T.nilable(String))
|
276
276
|
end
|
277
277
|
def comments_to_string(declaration)
|
278
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
348
|
+
assert_includes(entry.comments, "Returns `true` if any element of `self` meets a given criterion.")
|
349
349
|
end
|
350
350
|
|
351
351
|
private
|