ruby-lsp 0.16.7 → 0.17.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 +3 -1
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +195 -18
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +129 -12
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +333 -44
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +99 -0
- data/lib/ruby_indexer/ruby_indexer.rb +1 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +58 -11
- data/lib/ruby_indexer/test/configuration_test.rb +5 -6
- data/lib/ruby_indexer/test/constant_test.rb +9 -9
- data/lib/ruby_indexer/test/index_test.rb +867 -7
- data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
- data/lib/ruby_indexer/test/method_test.rb +57 -0
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +42 -0
- data/lib/ruby_indexer/test/test_case.rb +10 -1
- data/lib/ruby_lsp/addon.rb +8 -8
- data/lib/ruby_lsp/document.rb +26 -3
- data/lib/ruby_lsp/global_state.rb +37 -16
- data/lib/ruby_lsp/internal.rb +2 -0
- data/lib/ruby_lsp/listeners/code_lens.rb +2 -2
- data/lib/ruby_lsp/listeners/completion.rb +74 -17
- data/lib/ruby_lsp/listeners/definition.rb +82 -24
- data/lib/ruby_lsp/listeners/hover.rb +62 -6
- data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
- data/lib/ruby_lsp/node_context.rb +39 -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 +25 -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/support/rubocop_diagnostic.rb +23 -6
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +5 -1
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
- data/lib/ruby_lsp/requests/workspace_symbol.rb +7 -4
- data/lib/ruby_lsp/server.rb +22 -5
- data/lib/ruby_lsp/test_helper.rb +1 -0
- metadata +29 -5
@@ -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,35 +130,58 @@ module RubyIndexer
|
|
126
130
|
results.flat_map(&:first)
|
127
131
|
end
|
128
132
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
+
|
143
|
+
# Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
|
144
|
+
# documentation:
|
145
|
+
#
|
146
|
+
# name: the name of the reference how it was found in the source code (qualified or not)
|
147
|
+
# nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"])
|
148
|
+
# seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when
|
149
|
+
# resolving circular references
|
150
|
+
sig do
|
151
|
+
params(
|
152
|
+
name: String,
|
153
|
+
nesting: T::Array[String],
|
154
|
+
seen_names: T::Array[String],
|
155
|
+
).returns(T.nilable(T::Array[Entry]))
|
156
|
+
end
|
157
|
+
def resolve(name, nesting, seen_names = [])
|
158
|
+
# If we have a top level reference, then we just search for it straight away ignoring the nesting
|
136
159
|
if name.start_with?("::")
|
137
|
-
|
138
|
-
|
139
|
-
return results&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e }
|
160
|
+
entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names)
|
161
|
+
return entries if entries
|
140
162
|
end
|
141
163
|
|
142
|
-
|
143
|
-
|
144
|
-
full_name = namespace.empty? ? name : "#{namespace}::#{name}"
|
164
|
+
# Non qualified reference path
|
165
|
+
full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name
|
145
166
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
# the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
|
151
|
-
# `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
|
152
|
-
# `RubyLsp::Interface` part is an alias, that has to be resolved
|
153
|
-
entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
|
154
|
-
return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries
|
155
|
-
end
|
167
|
+
# When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the
|
168
|
+
# constant. First, it will try to find the constant in the exact namespace where the reference was found
|
169
|
+
entries = direct_or_aliased_constant(full_name, seen_names)
|
170
|
+
return entries if entries
|
156
171
|
|
157
|
-
|
172
|
+
# If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes,
|
173
|
+
# unwrapping each level one by one. Important note: the top level is not included because that's the fallback of
|
174
|
+
# the algorithm after every other possibility has been exhausted
|
175
|
+
entries = lookup_enclosing_scopes(name, nesting, seen_names)
|
176
|
+
return entries if entries
|
177
|
+
|
178
|
+
# If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the
|
179
|
+
# specific namespace where the reference was found
|
180
|
+
entries = lookup_ancestor_chain(name, nesting, seen_names)
|
181
|
+
return entries if entries
|
182
|
+
|
183
|
+
# Finally, as a fallback, Ruby will search for the constant in the top level namespace
|
184
|
+
search_top_level(name, seen_names)
|
158
185
|
rescue UnresolvableAliasError
|
159
186
|
nil
|
160
187
|
end
|
@@ -208,8 +235,8 @@ module RubyIndexer
|
|
208
235
|
# If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
|
209
236
|
# `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
|
210
237
|
# aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
|
211
|
-
sig { params(name: String).returns(String) }
|
212
|
-
def follow_aliased_namespace(name)
|
238
|
+
sig { params(name: String, seen_names: T::Array[String]).returns(String) }
|
239
|
+
def follow_aliased_namespace(name, seen_names = [])
|
213
240
|
return name if @entries[name]
|
214
241
|
|
215
242
|
parts = name.split("::")
|
@@ -222,16 +249,16 @@ module RubyIndexer
|
|
222
249
|
case entry
|
223
250
|
when Entry::Alias
|
224
251
|
target = entry.target
|
225
|
-
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
|
252
|
+
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
|
226
253
|
when Entry::UnresolvedAlias
|
227
|
-
resolved = resolve_alias(entry)
|
254
|
+
resolved = resolve_alias(entry, seen_names)
|
228
255
|
|
229
256
|
if resolved.is_a?(Entry::UnresolvedAlias)
|
230
257
|
raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
|
231
258
|
end
|
232
259
|
|
233
260
|
target = resolved.target
|
234
|
-
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
|
261
|
+
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
|
235
262
|
else
|
236
263
|
real_parts.unshift(T.must(parts[i]))
|
237
264
|
end
|
@@ -245,38 +272,300 @@ module RubyIndexer
|
|
245
272
|
sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
|
246
273
|
def resolve_method(method_name, receiver_name)
|
247
274
|
method_entries = self[method_name]
|
248
|
-
|
249
|
-
return unless
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
275
|
+
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
|
276
|
+
return unless method_entries
|
277
|
+
|
278
|
+
ancestors.each do |ancestor|
|
279
|
+
found = method_entries.select do |entry|
|
280
|
+
next unless entry.is_a?(Entry::Member)
|
281
|
+
|
282
|
+
entry.owner&.name == ancestor
|
283
|
+
end
|
284
|
+
|
285
|
+
return T.cast(found, T::Array[Entry::Member]) if found.any?
|
286
|
+
end
|
287
|
+
|
288
|
+
nil
|
289
|
+
rescue NonExistingNamespaceError
|
290
|
+
nil
|
291
|
+
end
|
292
|
+
|
293
|
+
# Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
|
294
|
+
# or constant declarations.
|
295
|
+
#
|
296
|
+
# When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
|
297
|
+
# everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
|
298
|
+
# module that prepends another module, then the prepend module appears before the included module.
|
299
|
+
#
|
300
|
+
# The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
|
301
|
+
sig { params(fully_qualified_name: String).returns(T::Array[String]) }
|
302
|
+
def linearized_ancestors_of(fully_qualified_name)
|
303
|
+
# If we already computed the ancestors for this namespace, return it straight away
|
304
|
+
cached_ancestors = @ancestors[fully_qualified_name]
|
305
|
+
return cached_ancestors if cached_ancestors
|
306
|
+
|
307
|
+
# If we don't have an entry for `name`, raise
|
308
|
+
entries = self[fully_qualified_name]
|
309
|
+
raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
|
310
|
+
|
311
|
+
ancestors = [fully_qualified_name]
|
312
|
+
|
313
|
+
# Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
|
314
|
+
# this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
|
315
|
+
# the cache will reflect the final result
|
316
|
+
@ancestors[fully_qualified_name] = ancestors
|
317
|
+
|
318
|
+
# If none of the entries for `name` are namespaces, raise
|
319
|
+
namespaces = entries.filter_map do |entry|
|
320
|
+
case entry
|
321
|
+
when Entry::Namespace
|
322
|
+
entry
|
323
|
+
when Entry::Alias
|
324
|
+
self[entry.target]&.grep(Entry::Namespace)
|
325
|
+
end
|
326
|
+
end.flatten
|
327
|
+
|
328
|
+
raise NonExistingNamespaceError,
|
329
|
+
"None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
|
330
|
+
|
331
|
+
mixin_operations = namespaces.flat_map(&:mixin_operations)
|
332
|
+
main_namespace_index = 0
|
333
|
+
|
334
|
+
# The original nesting where we discovered this namespace, so that we resolve the correct names of the
|
335
|
+
# included/prepended/extended modules and parent classes
|
336
|
+
nesting = T.must(namespaces.first).nesting
|
337
|
+
|
338
|
+
mixin_operations.each do |operation|
|
339
|
+
resolved_module = resolve(operation.module_name, nesting)
|
340
|
+
next unless resolved_module
|
341
|
+
|
342
|
+
module_fully_qualified_name = T.must(resolved_module.first).name
|
343
|
+
|
344
|
+
case operation
|
345
|
+
when Entry::Prepend
|
346
|
+
# When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
|
347
|
+
# the actual namespace twice. However, it does not check if it has been included because you are allowed to
|
348
|
+
# prepend the same module after it has already been included
|
349
|
+
linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
|
350
|
+
|
351
|
+
# When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
|
352
|
+
# example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
|
353
|
+
# be inserted after `"A`
|
354
|
+
uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
|
355
|
+
insert_position = linearized_prepends.length - uniq_prepends.length
|
356
|
+
|
357
|
+
T.unsafe(ancestors).insert(
|
358
|
+
insert_position,
|
359
|
+
*(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
|
360
|
+
)
|
361
|
+
|
362
|
+
main_namespace_index += linearized_prepends.length
|
363
|
+
when Entry::Include
|
364
|
+
# When including a module, Ruby will always prevent duplicate entries in case the module has already been
|
365
|
+
# prepended or included
|
366
|
+
linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
|
367
|
+
T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
|
372
|
+
# from two diffent classes in different files, we simply ignore it
|
373
|
+
superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
|
374
|
+
|
375
|
+
if superclass
|
376
|
+
# If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
|
377
|
+
# error. We need to ensure that this isn't the case
|
378
|
+
parent_class = T.must(superclass.parent_class)
|
379
|
+
|
380
|
+
resolved_parent_class = resolve(parent_class, nesting)
|
381
|
+
parent_class_name = resolved_parent_class&.first&.name
|
382
|
+
|
383
|
+
if parent_class_name && fully_qualified_name != parent_class_name
|
384
|
+
ancestors.concat(linearized_ancestors_of(parent_class_name))
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
ancestors
|
389
|
+
end
|
390
|
+
|
391
|
+
# Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
|
392
|
+
# and find inherited instance variables as well
|
393
|
+
sig { params(variable_name: String, owner_name: String).returns(T.nilable(T::Array[Entry::InstanceVariable])) }
|
394
|
+
def resolve_instance_variable(variable_name, owner_name)
|
395
|
+
entries = T.cast(self[variable_name], T.nilable(T::Array[Entry::InstanceVariable]))
|
396
|
+
return unless entries
|
397
|
+
|
398
|
+
ancestors = linearized_ancestors_of(owner_name)
|
399
|
+
return if ancestors.empty?
|
400
|
+
|
401
|
+
entries.select { |e| ancestors.include?(e.owner&.name) }
|
402
|
+
end
|
403
|
+
|
404
|
+
# Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
|
405
|
+
# include the `@` prefix
|
406
|
+
sig { params(name: String, owner_name: String).returns(T::Array[Entry::InstanceVariable]) }
|
407
|
+
def instance_variable_completion_candidates(name, owner_name)
|
408
|
+
entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
|
409
|
+
ancestors = linearized_ancestors_of(owner_name)
|
410
|
+
|
411
|
+
variables = entries.select { |e| ancestors.any?(e.owner&.name) }
|
412
|
+
variables.uniq!(&:name)
|
413
|
+
variables
|
414
|
+
end
|
415
|
+
|
416
|
+
# Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed,
|
417
|
+
# removed declarations removed and that the ancestor linearization cache is cleared if necessary
|
418
|
+
sig { params(indexable: IndexablePath).void }
|
419
|
+
def handle_change(indexable)
|
420
|
+
original_entries = @files_to_entries[indexable.full_path]
|
421
|
+
|
422
|
+
delete(indexable)
|
423
|
+
index_single(indexable)
|
424
|
+
|
425
|
+
updated_entries = @files_to_entries[indexable.full_path]
|
426
|
+
|
427
|
+
return unless original_entries && updated_entries
|
428
|
+
|
429
|
+
# A change in one ancestor may impact several different others, which could be including that ancestor through
|
430
|
+
# indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
|
431
|
+
# ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
|
432
|
+
# ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
|
433
|
+
original_map = T.cast(
|
434
|
+
original_entries.select { |e| e.is_a?(Entry::Namespace) },
|
435
|
+
T::Array[Entry::Namespace],
|
436
|
+
).to_h { |e| [e.name, e.ancestor_hash] }
|
437
|
+
|
438
|
+
updated_map = T.cast(
|
439
|
+
updated_entries.select { |e| e.is_a?(Entry::Namespace) },
|
440
|
+
T::Array[Entry::Namespace],
|
441
|
+
).to_h { |e| [e.name, e.ancestor_hash] }
|
442
|
+
|
443
|
+
@ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
|
258
444
|
end
|
259
445
|
|
260
446
|
private
|
261
447
|
|
262
448
|
# Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
|
263
449
|
# that doesn't exist, then we return the same UnresolvedAlias
|
264
|
-
sig
|
265
|
-
|
266
|
-
|
450
|
+
sig do
|
451
|
+
params(
|
452
|
+
entry: Entry::UnresolvedAlias,
|
453
|
+
seen_names: T::Array[String],
|
454
|
+
).returns(T.any(Entry::Alias, Entry::UnresolvedAlias))
|
455
|
+
end
|
456
|
+
def resolve_alias(entry, seen_names)
|
457
|
+
alias_name = entry.name
|
458
|
+
return entry if seen_names.include?(alias_name)
|
459
|
+
|
460
|
+
seen_names << alias_name
|
461
|
+
|
462
|
+
target = resolve(entry.target, entry.nesting, seen_names)
|
267
463
|
return entry unless target
|
268
464
|
|
269
465
|
target_name = T.must(target.first).name
|
270
466
|
resolved_alias = Entry::Alias.new(target_name, entry)
|
271
467
|
|
272
468
|
# Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
|
273
|
-
original_entries = T.must(@entries[
|
469
|
+
original_entries = T.must(@entries[alias_name])
|
274
470
|
original_entries.delete(entry)
|
275
471
|
original_entries << resolved_alias
|
276
472
|
|
277
|
-
@entries_tree.insert(
|
473
|
+
@entries_tree.insert(alias_name, original_entries)
|
278
474
|
|
279
475
|
resolved_alias
|
280
476
|
end
|
477
|
+
|
478
|
+
sig do
|
479
|
+
params(
|
480
|
+
name: String,
|
481
|
+
nesting: T::Array[String],
|
482
|
+
seen_names: T::Array[String],
|
483
|
+
).returns(T.nilable(T::Array[Entry]))
|
484
|
+
end
|
485
|
+
def lookup_enclosing_scopes(name, nesting, seen_names)
|
486
|
+
nesting.length.downto(1).each do |i|
|
487
|
+
namespace = T.must(nesting[0...i]).join("::")
|
488
|
+
|
489
|
+
# If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
|
490
|
+
# because the user might be trying to jump to the alias definition.
|
491
|
+
#
|
492
|
+
# However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
|
493
|
+
# the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
|
494
|
+
# `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
|
495
|
+
# `RubyLsp::Interface` part is an alias, that has to be resolved
|
496
|
+
entries = direct_or_aliased_constant("#{namespace}::#{name}", seen_names)
|
497
|
+
return entries if entries
|
498
|
+
end
|
499
|
+
|
500
|
+
nil
|
501
|
+
end
|
502
|
+
|
503
|
+
sig do
|
504
|
+
params(
|
505
|
+
name: String,
|
506
|
+
nesting: T::Array[String],
|
507
|
+
seen_names: T::Array[String],
|
508
|
+
).returns(T.nilable(T::Array[Entry]))
|
509
|
+
end
|
510
|
+
def lookup_ancestor_chain(name, nesting, seen_names)
|
511
|
+
*nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
|
512
|
+
return if T.must(nesting_parts).empty?
|
513
|
+
|
514
|
+
namespace_entries = resolve(T.must(nesting_parts).join("::"), [], seen_names)
|
515
|
+
return unless namespace_entries
|
516
|
+
|
517
|
+
ancestors = T.must(nesting_parts).empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
|
518
|
+
|
519
|
+
ancestors.each do |ancestor_name|
|
520
|
+
entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
|
521
|
+
return entries if entries
|
522
|
+
end
|
523
|
+
|
524
|
+
nil
|
525
|
+
rescue NonExistingNamespaceError
|
526
|
+
nil
|
527
|
+
end
|
528
|
+
|
529
|
+
# Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
|
530
|
+
# of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
|
531
|
+
# `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
|
532
|
+
# nesting
|
533
|
+
sig { params(name: String, nesting: T::Array[String]).returns(String) }
|
534
|
+
def build_non_redundant_full_name(name, nesting)
|
535
|
+
return name if nesting.empty?
|
536
|
+
|
537
|
+
namespace = nesting.join("::")
|
538
|
+
|
539
|
+
# If the name is not qualified, we can just concatenate the nesting and the name
|
540
|
+
return "#{namespace}::#{name}" unless name.include?("::")
|
541
|
+
|
542
|
+
name_parts = name.split("::")
|
543
|
+
|
544
|
+
# Find the first part of the name that is not in the nesting
|
545
|
+
index = name_parts.index { |part| !nesting.include?(part) }
|
546
|
+
|
547
|
+
if index.nil?
|
548
|
+
# All parts of the nesting are redundant because they are already present in the name. We can return the name
|
549
|
+
# directly
|
550
|
+
name
|
551
|
+
elsif index == 0
|
552
|
+
# No parts of the nesting are in the name, we can concatenate the namespace and the name
|
553
|
+
"#{namespace}::#{name}"
|
554
|
+
else
|
555
|
+
# The name includes some parts of the nesting. We need to remove the redundant parts
|
556
|
+
"#{namespace}::#{T.must(name_parts[index..-1]).join("::")}"
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
sig { params(full_name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
|
561
|
+
def direct_or_aliased_constant(full_name, seen_names)
|
562
|
+
entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
|
563
|
+
entries&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
|
564
|
+
end
|
565
|
+
|
566
|
+
sig { params(name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
|
567
|
+
def search_top_level(name, seen_names)
|
568
|
+
@entries[name]&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
|
569
|
+
end
|
281
570
|
end
|
282
571
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyIndexer
|
5
|
+
class RBSIndexer
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { params(index: Index).void }
|
9
|
+
def initialize(index)
|
10
|
+
@index = index
|
11
|
+
end
|
12
|
+
|
13
|
+
sig { void }
|
14
|
+
def index_ruby_core
|
15
|
+
loader = RBS::EnvironmentLoader.new
|
16
|
+
RBS::Environment.from_loader(loader).resolve_type_names
|
17
|
+
|
18
|
+
loader.each_signature do |source, pathname, _buffer, declarations, _directives|
|
19
|
+
process_signature(source, pathname, declarations)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
sig { params(source: T.untyped, pathname: Pathname, declarations: T::Array[RBS::AST::Declarations::Base]).void }
|
26
|
+
def process_signature(source, pathname, declarations)
|
27
|
+
declarations.each do |declaration|
|
28
|
+
process_declaration(declaration, pathname)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { params(declaration: RBS::AST::Declarations::Base, pathname: Pathname).void }
|
33
|
+
def process_declaration(declaration, pathname)
|
34
|
+
case declaration
|
35
|
+
when RBS::AST::Declarations::Class
|
36
|
+
handle_class_declaration(declaration, pathname)
|
37
|
+
when RBS::AST::Declarations::Module
|
38
|
+
handle_module_declaration(declaration, pathname)
|
39
|
+
else # rubocop:disable Style/EmptyElse
|
40
|
+
# Other kinds not yet handled
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { params(declaration: RBS::AST::Declarations::Class, pathname: Pathname).void }
|
45
|
+
def handle_class_declaration(declaration, pathname)
|
46
|
+
nesting = [declaration.name.name.to_s]
|
47
|
+
file_path = pathname.to_s
|
48
|
+
location = to_ruby_indexer_location(declaration.location)
|
49
|
+
comments = Array(declaration.comment&.string)
|
50
|
+
parent_class = declaration.super_class&.name&.name&.to_s
|
51
|
+
class_entry = Entry::Class.new(nesting, file_path, location, comments, parent_class)
|
52
|
+
add_declaration_mixins_to_entry(declaration, class_entry)
|
53
|
+
@index << class_entry
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { params(declaration: RBS::AST::Declarations::Module, pathname: Pathname).void }
|
57
|
+
def handle_module_declaration(declaration, pathname)
|
58
|
+
nesting = [declaration.name.name.to_s]
|
59
|
+
file_path = pathname.to_s
|
60
|
+
location = to_ruby_indexer_location(declaration.location)
|
61
|
+
comments = Array(declaration.comment&.string)
|
62
|
+
module_entry = Entry::Module.new(nesting, file_path, location, comments)
|
63
|
+
add_declaration_mixins_to_entry(declaration, module_entry)
|
64
|
+
@index << module_entry
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { params(rbs_location: RBS::Location).returns(RubyIndexer::Location) }
|
68
|
+
def to_ruby_indexer_location(rbs_location)
|
69
|
+
RubyIndexer::Location.new(
|
70
|
+
rbs_location.start_line,
|
71
|
+
rbs_location.end_line,
|
72
|
+
rbs_location.start_column,
|
73
|
+
rbs_location.end_column,
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
sig do
|
78
|
+
params(
|
79
|
+
declaration: T.any(RBS::AST::Declarations::Class, RBS::AST::Declarations::Module),
|
80
|
+
entry: Entry::Namespace,
|
81
|
+
).void
|
82
|
+
end
|
83
|
+
def add_declaration_mixins_to_entry(declaration, entry)
|
84
|
+
declaration.each_mixin do |mixin|
|
85
|
+
name = mixin.name.name.to_s
|
86
|
+
mixin_operation =
|
87
|
+
case mixin
|
88
|
+
when RBS::AST::Members::Include
|
89
|
+
Entry::Include.new(name)
|
90
|
+
when RBS::AST::Members::Extend
|
91
|
+
Entry::Extend.new(name)
|
92
|
+
when RBS::AST::Members::Prepend
|
93
|
+
Entry::Prepend.new(name)
|
94
|
+
end
|
95
|
+
entry.mixin_operations << mixin_operation if mixin_operation
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -11,6 +11,7 @@ require "ruby_indexer/lib/ruby_indexer/entry"
|
|
11
11
|
require "ruby_indexer/lib/ruby_indexer/configuration"
|
12
12
|
require "ruby_indexer/lib/ruby_indexer/prefix_tree"
|
13
13
|
require "ruby_indexer/lib/ruby_indexer/location"
|
14
|
+
require "ruby_indexer/lib/ruby_indexer/rbs_indexer"
|
14
15
|
|
15
16
|
module RubyIndexer
|
16
17
|
@configuration = T.let(Configuration.new, Configuration)
|
@@ -191,7 +191,8 @@ module RubyIndexer
|
|
191
191
|
|
192
192
|
@index.delete(IndexablePath.new(nil, "/fake/path/foo.rb"))
|
193
193
|
refute_entry("Foo")
|
194
|
-
|
194
|
+
|
195
|
+
assert_no_indexed_entries
|
195
196
|
end
|
196
197
|
|
197
198
|
def test_comments_can_be_attached_to_a_class
|
@@ -290,13 +291,13 @@ module RubyIndexer
|
|
290
291
|
RUBY
|
291
292
|
|
292
293
|
b_const = @index["A::B"].first
|
293
|
-
assert_equal(
|
294
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
294
295
|
|
295
296
|
c_const = @index["A::C"].first
|
296
|
-
assert_equal(
|
297
|
+
assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
|
297
298
|
|
298
299
|
d_const = @index["A::D"].first
|
299
|
-
assert_equal(
|
300
|
+
assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
|
300
301
|
end
|
301
302
|
|
302
303
|
def test_keeping_track_of_super_classes
|
@@ -323,7 +324,7 @@ module RubyIndexer
|
|
323
324
|
assert_equal("Bar", foo.parent_class)
|
324
325
|
|
325
326
|
baz = T.must(@index["Baz"].first)
|
326
|
-
|
327
|
+
assert_equal("::Object", baz.parent_class)
|
327
328
|
|
328
329
|
qux = T.must(@index["Something::Qux"].first)
|
329
330
|
assert_equal("::Baz", qux.parent_class)
|
@@ -369,13 +370,13 @@ module RubyIndexer
|
|
369
370
|
RUBY
|
370
371
|
|
371
372
|
foo = T.must(@index["Foo"][0])
|
372
|
-
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.
|
373
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
373
374
|
|
374
375
|
qux = T.must(@index["Foo::Qux"][0])
|
375
|
-
assert_equal(["Corge", "Corge", "Baz"], qux.
|
376
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
376
377
|
|
377
378
|
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
378
|
-
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.
|
379
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
379
380
|
end
|
380
381
|
|
381
382
|
def test_keeping_track_of_prepended_modules
|
@@ -415,13 +416,59 @@ module RubyIndexer
|
|
415
416
|
RUBY
|
416
417
|
|
417
418
|
foo = T.must(@index["Foo"][0])
|
418
|
-
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.
|
419
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
420
|
+
|
421
|
+
qux = T.must(@index["Foo::Qux"][0])
|
422
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
423
|
+
|
424
|
+
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
425
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
426
|
+
end
|
427
|
+
|
428
|
+
def test_keeping_track_of_extended_modules
|
429
|
+
index(<<~RUBY)
|
430
|
+
class Foo
|
431
|
+
# valid syntaxes that we can index
|
432
|
+
extend A1
|
433
|
+
self.extend A2
|
434
|
+
extend A3, A4
|
435
|
+
self.extend A5, A6
|
436
|
+
|
437
|
+
# valid syntaxes that we cannot index because of their dynamic nature
|
438
|
+
extend some_variable_or_method_call
|
439
|
+
self.extend some_variable_or_method_call
|
440
|
+
|
441
|
+
def something
|
442
|
+
extend A7 # We should not index this because of this dynamic nature
|
443
|
+
end
|
444
|
+
|
445
|
+
# Valid inner class syntax definition with its own modules prepended
|
446
|
+
class Qux
|
447
|
+
extend Corge
|
448
|
+
self.extend Corge
|
449
|
+
extend Baz
|
450
|
+
|
451
|
+
extend some_variable_or_method_call
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
class ConstantPathReferences
|
456
|
+
extend Foo::Bar
|
457
|
+
self.extend Foo::Bar2
|
458
|
+
|
459
|
+
extend dynamic::Bar
|
460
|
+
extend Foo::
|
461
|
+
end
|
462
|
+
RUBY
|
463
|
+
|
464
|
+
foo = T.must(@index["Foo"][0])
|
465
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
419
466
|
|
420
467
|
qux = T.must(@index["Foo::Qux"][0])
|
421
|
-
assert_equal(["Corge", "Corge", "Baz"], qux.
|
468
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
422
469
|
|
423
470
|
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
424
|
-
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.
|
471
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
425
472
|
end
|
426
473
|
end
|
427
474
|
end
|