ruby-lsp 0.16.7 → 0.17.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 +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
|