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.
- 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
|