ruby-lsp 0.16.7 → 0.17.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +139 -18
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +101 -12
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +183 -10
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +55 -9
- data/lib/ruby_indexer/test/configuration_test.rb +4 -5
- data/lib/ruby_indexer/test/constant_test.rb +8 -8
- data/lib/ruby_indexer/test/index_test.rb +528 -0
- data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
- data/lib/ruby_indexer/test/method_test.rb +37 -0
- data/lib/ruby_indexer/test/test_case.rb +3 -1
- data/lib/ruby_lsp/addon.rb +8 -8
- data/lib/ruby_lsp/document.rb +3 -3
- data/lib/ruby_lsp/internal.rb +1 -0
- data/lib/ruby_lsp/listeners/completion.rb +74 -17
- data/lib/ruby_lsp/listeners/definition.rb +62 -6
- data/lib/ruby_lsp/listeners/hover.rb +60 -6
- data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
- data/lib/ruby_lsp/node_context.rb +28 -0
- data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
- data/lib/ruby_lsp/requests/code_actions.rb +16 -15
- data/lib/ruby_lsp/requests/completion.rb +21 -13
- data/lib/ruby_lsp/requests/completion_resolve.rb +9 -0
- data/lib/ruby_lsp/requests/definition.rb +20 -5
- data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
- data/lib/ruby_lsp/requests/hover.rb +5 -6
- data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
- data/lib/ruby_lsp/requests/signature_help.rb +3 -3
- data/lib/ruby_lsp/requests/support/common.rb +4 -3
- data/lib/ruby_lsp/requests/workspace_symbol.rb +3 -1
- data/lib/ruby_lsp/server.rb +10 -4
- data/lib/ruby_lsp/test_helper.rb +1 -0
- metadata +4 -2
@@ -6,6 +6,7 @@ module RubyIndexer
|
|
6
6
|
extend T::Sig
|
7
7
|
|
8
8
|
class UnresolvableAliasError < StandardError; end
|
9
|
+
class NonExistingNamespaceError < StandardError; end
|
9
10
|
|
10
11
|
# The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
|
11
12
|
ENTRY_SIMILARITY_THRESHOLD = 0.7
|
@@ -31,6 +32,9 @@ module RubyIndexer
|
|
31
32
|
|
32
33
|
# Holds all require paths for every indexed item so that we can provide autocomplete for requires
|
33
34
|
@require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
|
35
|
+
|
36
|
+
# Holds the linearized ancestors list for every namespace
|
37
|
+
@ancestors = T.let({}, T::Hash[String, T::Array[String]])
|
34
38
|
end
|
35
39
|
|
36
40
|
sig { params(indexable: IndexablePath).void }
|
@@ -126,6 +130,16 @@ module RubyIndexer
|
|
126
130
|
results.flat_map(&:first)
|
127
131
|
end
|
128
132
|
|
133
|
+
sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) }
|
134
|
+
def method_completion_candidates(name, receiver_name)
|
135
|
+
ancestors = linearized_ancestors_of(receiver_name)
|
136
|
+
candidates = prefix_search(name).flatten
|
137
|
+
candidates.select! do |entry|
|
138
|
+
entry.is_a?(RubyIndexer::Entry::Member) && ancestors.any?(entry.owner&.name)
|
139
|
+
end
|
140
|
+
candidates
|
141
|
+
end
|
142
|
+
|
129
143
|
# Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
|
130
144
|
# the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
|
131
145
|
# 1. Foo::Bar::Baz
|
@@ -245,16 +259,175 @@ module RubyIndexer
|
|
245
259
|
sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
|
246
260
|
def resolve_method(method_name, receiver_name)
|
247
261
|
method_entries = self[method_name]
|
248
|
-
|
249
|
-
return unless
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
262
|
+
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
|
263
|
+
return unless method_entries
|
264
|
+
|
265
|
+
ancestors.each do |ancestor|
|
266
|
+
found = method_entries.select do |entry|
|
267
|
+
next unless entry.is_a?(Entry::Member)
|
268
|
+
|
269
|
+
entry.owner&.name == ancestor
|
270
|
+
end
|
271
|
+
|
272
|
+
return T.cast(found, T::Array[Entry::Member]) if found.any?
|
273
|
+
end
|
274
|
+
|
275
|
+
nil
|
276
|
+
rescue NonExistingNamespaceError
|
277
|
+
nil
|
278
|
+
end
|
279
|
+
|
280
|
+
# Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
|
281
|
+
# or constant declarations.
|
282
|
+
#
|
283
|
+
# When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
|
284
|
+
# everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
|
285
|
+
# module that prepends another module, then the prepend module appears before the included module.
|
286
|
+
#
|
287
|
+
# The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
|
288
|
+
sig { params(fully_qualified_name: String).returns(T::Array[String]) }
|
289
|
+
def linearized_ancestors_of(fully_qualified_name)
|
290
|
+
# If we already computed the ancestors for this namespace, return it straight away
|
291
|
+
cached_ancestors = @ancestors[fully_qualified_name]
|
292
|
+
return cached_ancestors if cached_ancestors
|
293
|
+
|
294
|
+
ancestors = [fully_qualified_name]
|
295
|
+
|
296
|
+
# Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
|
297
|
+
# this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
|
298
|
+
# the cache will reflect the final result
|
299
|
+
@ancestors[fully_qualified_name] = ancestors
|
300
|
+
|
301
|
+
# If we don't have an entry for `name`, raise
|
302
|
+
entries = resolve(fully_qualified_name, [])
|
303
|
+
raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
|
304
|
+
|
305
|
+
# If none of the entries for `name` are namespaces, raise
|
306
|
+
namespaces = entries.filter_map do |entry|
|
307
|
+
case entry
|
308
|
+
when Entry::Namespace
|
309
|
+
entry
|
310
|
+
when Entry::Alias
|
311
|
+
self[entry.target]&.grep(Entry::Namespace)
|
312
|
+
end
|
313
|
+
end.flatten
|
314
|
+
|
315
|
+
raise NonExistingNamespaceError,
|
316
|
+
"None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
|
317
|
+
|
318
|
+
mixin_operations = namespaces.flat_map(&:mixin_operations)
|
319
|
+
main_namespace_index = 0
|
320
|
+
|
321
|
+
# The original nesting where we discovered this namespace, so that we resolve the correct names of the
|
322
|
+
# included/prepended/extended modules and parent classes
|
323
|
+
nesting = T.must(namespaces.first).nesting
|
324
|
+
|
325
|
+
mixin_operations.each do |operation|
|
326
|
+
resolved_module = resolve(operation.module_name, nesting)
|
327
|
+
next unless resolved_module
|
328
|
+
|
329
|
+
module_fully_qualified_name = T.must(resolved_module.first).name
|
330
|
+
|
331
|
+
case operation
|
332
|
+
when Entry::Prepend
|
333
|
+
# When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
|
334
|
+
# the actual namespace twice. However, it does not check if it has been included because you are allowed to
|
335
|
+
# prepend the same module after it has already been included
|
336
|
+
linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
|
337
|
+
|
338
|
+
# When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
|
339
|
+
# example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
|
340
|
+
# be inserted after `"A`
|
341
|
+
uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
|
342
|
+
insert_position = linearized_prepends.length - uniq_prepends.length
|
343
|
+
|
344
|
+
T.unsafe(ancestors).insert(
|
345
|
+
insert_position,
|
346
|
+
*(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
|
347
|
+
)
|
348
|
+
|
349
|
+
main_namespace_index += linearized_prepends.length
|
350
|
+
when Entry::Include
|
351
|
+
# When including a module, Ruby will always prevent duplicate entries in case the module has already been
|
352
|
+
# prepended or included
|
353
|
+
linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
|
354
|
+
T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
|
359
|
+
# from two diffent classes in different files, we simply ignore it
|
360
|
+
superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
|
361
|
+
|
362
|
+
if superclass
|
363
|
+
# If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
|
364
|
+
# error. We need to ensure that this isn't the case
|
365
|
+
parent_class = T.must(superclass.parent_class)
|
366
|
+
|
367
|
+
resolved_parent_class = resolve(parent_class, nesting)
|
368
|
+
parent_class_name = resolved_parent_class&.first&.name
|
369
|
+
|
370
|
+
if parent_class_name && fully_qualified_name != parent_class_name
|
371
|
+
ancestors.concat(linearized_ancestors_of(parent_class_name))
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
ancestors
|
376
|
+
end
|
377
|
+
|
378
|
+
# Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
|
379
|
+
# and find inherited instance variables as well
|
380
|
+
sig { params(variable_name: String, owner_name: String).returns(T.nilable(T::Array[Entry::InstanceVariable])) }
|
381
|
+
def resolve_instance_variable(variable_name, owner_name)
|
382
|
+
entries = T.cast(self[variable_name], T.nilable(T::Array[Entry::InstanceVariable]))
|
383
|
+
return unless entries
|
384
|
+
|
385
|
+
ancestors = linearized_ancestors_of(owner_name)
|
386
|
+
return if ancestors.empty?
|
387
|
+
|
388
|
+
entries.select { |e| ancestors.include?(e.owner&.name) }
|
389
|
+
end
|
390
|
+
|
391
|
+
# Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
|
392
|
+
# include the `@` prefix
|
393
|
+
sig { params(name: String, owner_name: String).returns(T::Array[Entry::InstanceVariable]) }
|
394
|
+
def instance_variable_completion_candidates(name, owner_name)
|
395
|
+
entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
|
396
|
+
ancestors = linearized_ancestors_of(owner_name)
|
397
|
+
|
398
|
+
variables = entries.uniq(&:name)
|
399
|
+
variables.select! { |e| ancestors.any?(e.owner&.name) }
|
400
|
+
variables
|
401
|
+
end
|
402
|
+
|
403
|
+
# Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed,
|
404
|
+
# removed declarations removed and that the ancestor linearization cache is cleared if necessary
|
405
|
+
sig { params(indexable: IndexablePath).void }
|
406
|
+
def handle_change(indexable)
|
407
|
+
original_entries = @files_to_entries[indexable.full_path]
|
408
|
+
|
409
|
+
delete(indexable)
|
410
|
+
index_single(indexable)
|
411
|
+
|
412
|
+
updated_entries = @files_to_entries[indexable.full_path]
|
413
|
+
|
414
|
+
return unless original_entries && updated_entries
|
415
|
+
|
416
|
+
# A change in one ancestor may impact several different others, which could be including that ancestor through
|
417
|
+
# indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
|
418
|
+
# ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
|
419
|
+
# ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
|
420
|
+
original_map = T.cast(
|
421
|
+
original_entries.select { |e| e.is_a?(Entry::Namespace) },
|
422
|
+
T::Array[Entry::Namespace],
|
423
|
+
).to_h { |e| [e.name, e.ancestor_hash] }
|
424
|
+
|
425
|
+
updated_map = T.cast(
|
426
|
+
updated_entries.select { |e| e.is_a?(Entry::Namespace) },
|
427
|
+
T::Array[Entry::Namespace],
|
428
|
+
).to_h { |e| [e.name, e.ancestor_hash] }
|
429
|
+
|
430
|
+
@ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
|
258
431
|
end
|
259
432
|
|
260
433
|
private
|
@@ -290,13 +290,13 @@ module RubyIndexer
|
|
290
290
|
RUBY
|
291
291
|
|
292
292
|
b_const = @index["A::B"].first
|
293
|
-
assert_equal(
|
293
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
294
294
|
|
295
295
|
c_const = @index["A::C"].first
|
296
|
-
assert_equal(
|
296
|
+
assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
|
297
297
|
|
298
298
|
d_const = @index["A::D"].first
|
299
|
-
assert_equal(
|
299
|
+
assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
|
300
300
|
end
|
301
301
|
|
302
302
|
def test_keeping_track_of_super_classes
|
@@ -369,13 +369,13 @@ module RubyIndexer
|
|
369
369
|
RUBY
|
370
370
|
|
371
371
|
foo = T.must(@index["Foo"][0])
|
372
|
-
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.
|
372
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
373
373
|
|
374
374
|
qux = T.must(@index["Foo::Qux"][0])
|
375
|
-
assert_equal(["Corge", "Corge", "Baz"], qux.
|
375
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
376
376
|
|
377
377
|
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
378
|
-
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.
|
378
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
379
379
|
end
|
380
380
|
|
381
381
|
def test_keeping_track_of_prepended_modules
|
@@ -415,13 +415,59 @@ module RubyIndexer
|
|
415
415
|
RUBY
|
416
416
|
|
417
417
|
foo = T.must(@index["Foo"][0])
|
418
|
-
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.
|
418
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
419
419
|
|
420
420
|
qux = T.must(@index["Foo::Qux"][0])
|
421
|
-
assert_equal(["Corge", "Corge", "Baz"], qux.
|
421
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
422
422
|
|
423
423
|
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
424
|
-
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.
|
424
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
425
|
+
end
|
426
|
+
|
427
|
+
def test_keeping_track_of_extended_modules
|
428
|
+
index(<<~RUBY)
|
429
|
+
class Foo
|
430
|
+
# valid syntaxes that we can index
|
431
|
+
extend A1
|
432
|
+
self.extend A2
|
433
|
+
extend A3, A4
|
434
|
+
self.extend A5, A6
|
435
|
+
|
436
|
+
# valid syntaxes that we cannot index because of their dynamic nature
|
437
|
+
extend some_variable_or_method_call
|
438
|
+
self.extend some_variable_or_method_call
|
439
|
+
|
440
|
+
def something
|
441
|
+
extend A7 # We should not index this because of this dynamic nature
|
442
|
+
end
|
443
|
+
|
444
|
+
# Valid inner class syntax definition with its own modules prepended
|
445
|
+
class Qux
|
446
|
+
extend Corge
|
447
|
+
self.extend Corge
|
448
|
+
extend Baz
|
449
|
+
|
450
|
+
extend some_variable_or_method_call
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
class ConstantPathReferences
|
455
|
+
extend Foo::Bar
|
456
|
+
self.extend Foo::Bar2
|
457
|
+
|
458
|
+
extend dynamic::Bar
|
459
|
+
extend Foo::
|
460
|
+
end
|
461
|
+
RUBY
|
462
|
+
|
463
|
+
foo = T.must(@index["Foo"][0])
|
464
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
465
|
+
|
466
|
+
qux = T.must(@index["Foo::Qux"][0])
|
467
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
468
|
+
|
469
|
+
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
470
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
425
471
|
end
|
426
472
|
end
|
427
473
|
end
|
@@ -68,12 +68,11 @@ module RubyIndexer
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project
|
71
|
-
Bundler.settings.
|
72
|
-
|
71
|
+
Bundler.settings.temporary(path: "vendor/bundle") do
|
72
|
+
config = Configuration.new
|
73
73
|
|
74
|
-
|
75
|
-
|
76
|
-
Bundler.settings.set_global("path", nil)
|
74
|
+
assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
|
75
|
+
end
|
77
76
|
end
|
78
77
|
|
79
78
|
def test_indexables_does_not_include_gems_own_installed_files
|
@@ -122,13 +122,13 @@ module RubyIndexer
|
|
122
122
|
RUBY
|
123
123
|
|
124
124
|
b_const = @index["A::B"].first
|
125
|
-
assert_equal(
|
125
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
126
126
|
|
127
127
|
c_const = @index["A::C"].first
|
128
|
-
assert_equal(
|
128
|
+
assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
|
129
129
|
|
130
130
|
d_const = @index["A::D"].first
|
131
|
-
assert_equal(
|
131
|
+
assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
|
132
132
|
end
|
133
133
|
|
134
134
|
def test_marking_constants_as_private_reopening_namespaces
|
@@ -155,13 +155,13 @@ module RubyIndexer
|
|
155
155
|
RUBY
|
156
156
|
|
157
157
|
a_const = @index["A::B::CONST_A"].first
|
158
|
-
assert_equal(
|
158
|
+
assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
|
159
159
|
|
160
160
|
b_const = @index["A::B::CONST_B"].first
|
161
|
-
assert_equal(
|
161
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
162
162
|
|
163
163
|
c_const = @index["A::B::CONST_C"].first
|
164
|
-
assert_equal(
|
164
|
+
assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
|
165
165
|
end
|
166
166
|
|
167
167
|
def test_marking_constants_as_private_with_receiver
|
@@ -179,10 +179,10 @@ module RubyIndexer
|
|
179
179
|
RUBY
|
180
180
|
|
181
181
|
a_const = @index["A::B::CONST_A"].first
|
182
|
-
assert_equal(
|
182
|
+
assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
|
183
183
|
|
184
184
|
b_const = @index["A::B::CONST_B"].first
|
185
|
-
assert_equal(
|
185
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
186
186
|
end
|
187
187
|
|
188
188
|
def test_indexing_constant_aliases
|