ruby-lsp 0.17.4 → 0.17.13
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 +11 -2
- data/VERSION +1 -1
- data/exe/ruby-lsp +26 -1
- data/exe/ruby-lsp-check +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +74 -43
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +26 -0
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +147 -29
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +383 -79
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +195 -61
- data/lib/ruby_indexer/ruby_indexer.rb +1 -8
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +71 -3
- data/lib/ruby_indexer/test/configuration_test.rb +1 -1
- data/lib/ruby_indexer/test/constant_test.rb +17 -17
- data/lib/ruby_indexer/test/enhancements_test.rb +197 -0
- data/lib/ruby_indexer/test/index_test.rb +367 -17
- data/lib/ruby_indexer/test/method_test.rb +58 -25
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +297 -0
- data/lib/ruby_indexer/test/test_case.rb +1 -5
- data/lib/ruby_lsp/addon.rb +22 -5
- data/lib/ruby_lsp/base_server.rb +8 -3
- data/lib/ruby_lsp/document.rb +27 -46
- data/lib/ruby_lsp/erb_document.rb +125 -0
- data/lib/ruby_lsp/global_state.rb +47 -19
- data/lib/ruby_lsp/internal.rb +2 -0
- data/lib/ruby_lsp/listeners/completion.rb +161 -57
- data/lib/ruby_lsp/listeners/definition.rb +91 -27
- data/lib/ruby_lsp/listeners/document_highlight.rb +5 -1
- data/lib/ruby_lsp/listeners/hover.rb +61 -19
- data/lib/ruby_lsp/listeners/signature_help.rb +13 -6
- data/lib/ruby_lsp/node_context.rb +65 -5
- data/lib/ruby_lsp/requests/code_action_resolve.rb +107 -9
- data/lib/ruby_lsp/requests/code_actions.rb +11 -2
- data/lib/ruby_lsp/requests/completion.rb +4 -4
- data/lib/ruby_lsp/requests/completion_resolve.rb +14 -9
- data/lib/ruby_lsp/requests/definition.rb +18 -8
- data/lib/ruby_lsp/requests/diagnostics.rb +6 -5
- data/lib/ruby_lsp/requests/document_symbol.rb +2 -7
- data/lib/ruby_lsp/requests/folding_ranges.rb +6 -2
- data/lib/ruby_lsp/requests/formatting.rb +15 -0
- data/lib/ruby_lsp/requests/hover.rb +5 -5
- data/lib/ruby_lsp/requests/on_type_formatting.rb +6 -4
- data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +3 -2
- data/lib/ruby_lsp/requests/signature_help.rb +3 -3
- data/lib/ruby_lsp/requests/support/common.rb +11 -2
- data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +2 -6
- data/lib/ruby_lsp/ruby_document.rb +74 -0
- data/lib/ruby_lsp/server.rb +129 -54
- data/lib/ruby_lsp/store.rb +33 -9
- data/lib/ruby_lsp/test_helper.rb +3 -1
- data/lib/ruby_lsp/type_inferrer.rb +61 -25
- data/lib/ruby_lsp/utils.rb +13 -0
- metadata +9 -8
- data/exe/ruby-lsp-doctor +0 -23
@@ -11,6 +11,9 @@ module RubyIndexer
|
|
11
11
|
# The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
|
12
12
|
ENTRY_SIMILARITY_THRESHOLD = 0.7
|
13
13
|
|
14
|
+
sig { returns(Configuration) }
|
15
|
+
attr_reader :configuration
|
16
|
+
|
14
17
|
sig { void }
|
15
18
|
def initialize
|
16
19
|
# Holds all entries in the index using the following format:
|
@@ -35,6 +38,29 @@ module RubyIndexer
|
|
35
38
|
|
36
39
|
# Holds the linearized ancestors list for every namespace
|
37
40
|
@ancestors = T.let({}, T::Hash[String, T::Array[String]])
|
41
|
+
|
42
|
+
# List of classes that are enhancing the index
|
43
|
+
@enhancements = T.let([], T::Array[Enhancement])
|
44
|
+
|
45
|
+
# Map of module name to included hooks that have to be executed when we include the given module
|
46
|
+
@included_hooks = T.let(
|
47
|
+
{},
|
48
|
+
T::Hash[String, T::Array[T.proc.params(index: Index, base: Entry::Namespace).void]],
|
49
|
+
)
|
50
|
+
|
51
|
+
@configuration = T.let(RubyIndexer::Configuration.new, Configuration)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Register an enhancement to the index. Enhancements must conform to the `Enhancement` interface
|
55
|
+
sig { params(enhancement: Enhancement).void }
|
56
|
+
def register_enhancement(enhancement)
|
57
|
+
@enhancements << enhancement
|
58
|
+
end
|
59
|
+
|
60
|
+
# Register an included `hook` that will be executed when `module_name` is included into any namespace
|
61
|
+
sig { params(module_name: String, hook: T.proc.params(index: Index, base: Entry::Namespace).void).void }
|
62
|
+
def register_included_hook(module_name, &hook)
|
63
|
+
(@included_hooks[module_name] ||= []) << hook
|
38
64
|
end
|
39
65
|
|
40
66
|
sig { params(indexable: IndexablePath).void }
|
@@ -84,6 +110,34 @@ module RubyIndexer
|
|
84
110
|
@require_paths_tree.search(query)
|
85
111
|
end
|
86
112
|
|
113
|
+
# Searches for a constant based on an unqualified name and returns the first possible match regardless of whether
|
114
|
+
# there are more possible matching entries
|
115
|
+
sig do
|
116
|
+
params(
|
117
|
+
name: String,
|
118
|
+
).returns(T.nilable(T::Array[T.any(
|
119
|
+
Entry::Namespace,
|
120
|
+
Entry::ConstantAlias,
|
121
|
+
Entry::UnresolvedConstantAlias,
|
122
|
+
Entry::Constant,
|
123
|
+
)]))
|
124
|
+
end
|
125
|
+
def first_unqualified_const(name)
|
126
|
+
_name, entries = @entries.find do |const_name, _entries|
|
127
|
+
const_name.end_with?(name)
|
128
|
+
end
|
129
|
+
|
130
|
+
T.cast(
|
131
|
+
entries,
|
132
|
+
T.nilable(T::Array[T.any(
|
133
|
+
Entry::Namespace,
|
134
|
+
Entry::ConstantAlias,
|
135
|
+
Entry::UnresolvedConstantAlias,
|
136
|
+
Entry::Constant,
|
137
|
+
)]),
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
87
141
|
# Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches
|
88
142
|
# to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given
|
89
143
|
# name match. For example:
|
@@ -150,17 +204,42 @@ module RubyIndexer
|
|
150
204
|
ancestors = linearized_ancestors_of(receiver_name)
|
151
205
|
|
152
206
|
candidates = name ? prefix_search(name).flatten : @entries.values.flatten
|
153
|
-
candidates.
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
207
|
+
completion_items = candidates.each_with_object({}) do |entry, hash|
|
208
|
+
unless entry.is_a?(Entry::Member) || entry.is_a?(Entry::MethodAlias) ||
|
209
|
+
entry.is_a?(Entry::UnresolvedMethodAlias)
|
210
|
+
next
|
211
|
+
end
|
212
|
+
|
213
|
+
entry_name = entry.name
|
214
|
+
ancestor_index = ancestors.index(entry.owner&.name)
|
215
|
+
existing_entry, existing_entry_index = hash[entry_name]
|
216
|
+
|
217
|
+
# Conditions for matching a method completion candidate:
|
218
|
+
# 1. If an ancestor_index was found, it means that this method is owned by the receiver. The exact index is
|
219
|
+
# where in the ancestor chain the method was found. For example, if the ancestors are ["A", "B", "C"] and we
|
220
|
+
# found the method declared in `B`, then the ancestors index is 1
|
221
|
+
#
|
222
|
+
# 2. We already established that this method is owned by the receiver. Now, check if we already added a
|
223
|
+
# completion candidate for this method name. If not, then we just go and add it (the left hand side of the or)
|
224
|
+
#
|
225
|
+
# 3. If we had already found a method entry for the same name, then we need to check if the current entry that
|
226
|
+
# we are comparing appears first in the hierarchy or not. For example, imagine we have the method `open` defined
|
227
|
+
# in both `File` and its parent `IO`. If we first find the method `open` in `IO`, it will be inserted into the
|
228
|
+
# hash. Then, when we find the entry for `open` owned by `File`, we need to replace `IO.open` by `File.open`,
|
229
|
+
# since `File.open` appears first in the hierarchy chain and is therefore the correct method being invoked. The
|
230
|
+
# last part of the conditional checks if the current entry was found earlier in the hierarchy chain, in which
|
231
|
+
# case we must update the existing entry to avoid showing the wrong method declaration for overridden methods
|
232
|
+
next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
|
233
|
+
|
234
|
+
if entry.is_a?(Entry::UnresolvedMethodAlias)
|
235
|
+
resolved_alias = resolve_method_alias(entry, receiver_name)
|
236
|
+
hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
|
237
|
+
else
|
238
|
+
hash[entry_name] = [entry, ancestor_index]
|
162
239
|
end
|
163
240
|
end
|
241
|
+
|
242
|
+
completion_items.values.map!(&:first)
|
164
243
|
end
|
165
244
|
|
166
245
|
# Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
|
@@ -175,7 +254,11 @@ module RubyIndexer
|
|
175
254
|
name: String,
|
176
255
|
nesting: T::Array[String],
|
177
256
|
seen_names: T::Array[String],
|
178
|
-
).returns(T.nilable(T::Array[
|
257
|
+
).returns(T.nilable(T::Array[T.any(
|
258
|
+
Entry::Namespace,
|
259
|
+
Entry::ConstantAlias,
|
260
|
+
Entry::UnresolvedConstantAlias,
|
261
|
+
)]))
|
179
262
|
end
|
180
263
|
def resolve(name, nesting, seen_names = [])
|
181
264
|
# If we have a top level reference, then we just search for it straight away ignoring the nesting
|
@@ -204,7 +287,7 @@ module RubyIndexer
|
|
204
287
|
return entries if entries
|
205
288
|
|
206
289
|
# Finally, as a fallback, Ruby will search for the constant in the top level namespace
|
207
|
-
|
290
|
+
direct_or_aliased_constant(name, seen_names)
|
208
291
|
rescue UnresolvableAliasError
|
209
292
|
nil
|
210
293
|
end
|
@@ -218,7 +301,8 @@ module RubyIndexer
|
|
218
301
|
block: T.nilable(T.proc.params(progress: Integer).returns(T::Boolean)),
|
219
302
|
).void
|
220
303
|
end
|
221
|
-
def index_all(indexable_paths:
|
304
|
+
def index_all(indexable_paths: @configuration.indexables, &block)
|
305
|
+
RBSIndexer.new(self).index_ruby_core
|
222
306
|
# Calculate how many paths are worth 1% of progress
|
223
307
|
progress_step = (indexable_paths.length / 100.0).ceil
|
224
308
|
|
@@ -238,11 +322,25 @@ module RubyIndexer
|
|
238
322
|
dispatcher = Prism::Dispatcher.new
|
239
323
|
|
240
324
|
result = Prism.parse(content)
|
241
|
-
DeclarationListener.new(
|
325
|
+
listener = DeclarationListener.new(
|
326
|
+
self,
|
327
|
+
dispatcher,
|
328
|
+
result,
|
329
|
+
indexable_path.full_path,
|
330
|
+
enhancements: @enhancements,
|
331
|
+
)
|
242
332
|
dispatcher.dispatch(result.value)
|
243
333
|
|
334
|
+
indexing_errors = listener.indexing_errors.uniq
|
335
|
+
|
244
336
|
require_path = indexable_path.require_path
|
245
337
|
@require_paths_tree.insert(require_path, indexable_path) if require_path
|
338
|
+
|
339
|
+
if indexing_errors.any?
|
340
|
+
indexing_errors.each do |error|
|
341
|
+
$stderr.puts error
|
342
|
+
end
|
343
|
+
end
|
246
344
|
rescue Errno::EISDIR, Errno::ENOENT
|
247
345
|
# If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
|
248
346
|
# it
|
@@ -276,13 +374,13 @@ module RubyIndexer
|
|
276
374
|
entry = @entries[current_name]&.first
|
277
375
|
|
278
376
|
case entry
|
279
|
-
when Entry::
|
377
|
+
when Entry::ConstantAlias
|
280
378
|
target = entry.target
|
281
379
|
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
|
282
|
-
when Entry::
|
380
|
+
when Entry::UnresolvedConstantAlias
|
283
381
|
resolved = resolve_alias(entry, seen_names)
|
284
382
|
|
285
|
-
if resolved.is_a?(Entry::
|
383
|
+
if resolved.is_a?(Entry::UnresolvedConstantAlias)
|
286
384
|
raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
|
287
385
|
end
|
288
386
|
|
@@ -302,14 +400,17 @@ module RubyIndexer
|
|
302
400
|
params(
|
303
401
|
method_name: String,
|
304
402
|
receiver_name: String,
|
403
|
+
inherited_only: T::Boolean,
|
305
404
|
).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)]))
|
306
405
|
end
|
307
|
-
def resolve_method(method_name, receiver_name)
|
406
|
+
def resolve_method(method_name, receiver_name, inherited_only: false)
|
308
407
|
method_entries = self[method_name]
|
309
408
|
return unless method_entries
|
310
409
|
|
311
410
|
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
|
312
411
|
ancestors.each do |ancestor|
|
412
|
+
next if inherited_only && ancestor == receiver_name
|
413
|
+
|
313
414
|
found = method_entries.filter_map do |entry|
|
314
415
|
case entry
|
315
416
|
when Entry::Member, Entry::MethodAlias
|
@@ -345,8 +446,25 @@ module RubyIndexer
|
|
345
446
|
cached_ancestors = @ancestors[fully_qualified_name]
|
346
447
|
return cached_ancestors if cached_ancestors
|
347
448
|
|
449
|
+
parts = fully_qualified_name.split("::")
|
450
|
+
singleton_levels = 0
|
451
|
+
|
452
|
+
parts.reverse_each do |part|
|
453
|
+
break unless part.include?("<Class:")
|
454
|
+
|
455
|
+
singleton_levels += 1
|
456
|
+
parts.pop
|
457
|
+
end
|
458
|
+
|
459
|
+
attached_class_name = parts.join("::")
|
460
|
+
|
348
461
|
# If we don't have an entry for `name`, raise
|
349
462
|
entries = self[fully_qualified_name]
|
463
|
+
|
464
|
+
if singleton_levels > 0 && !entries && indexed?(attached_class_name)
|
465
|
+
entries = [existing_or_new_singleton_class(attached_class_name)]
|
466
|
+
end
|
467
|
+
|
350
468
|
raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
|
351
469
|
|
352
470
|
ancestors = [fully_qualified_name]
|
@@ -361,7 +479,7 @@ module RubyIndexer
|
|
361
479
|
case entry
|
362
480
|
when Entry::Namespace
|
363
481
|
entry
|
364
|
-
when Entry::
|
482
|
+
when Entry::ConstantAlias
|
365
483
|
self[entry.target]&.grep(Entry::Namespace)
|
366
484
|
end
|
367
485
|
end.flatten
|
@@ -369,62 +487,31 @@ module RubyIndexer
|
|
369
487
|
raise NonExistingNamespaceError,
|
370
488
|
"None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
|
371
489
|
|
372
|
-
mixin_operations = namespaces.flat_map(&:mixin_operations)
|
373
|
-
main_namespace_index = 0
|
374
|
-
|
375
490
|
# The original nesting where we discovered this namespace, so that we resolve the correct names of the
|
376
491
|
# included/prepended/extended modules and parent classes
|
377
|
-
nesting = T.must(namespaces.first).nesting
|
378
|
-
|
379
|
-
mixin_operations.each do |operation|
|
380
|
-
resolved_module = resolve(operation.module_name, nesting)
|
381
|
-
next unless resolved_module
|
382
|
-
|
383
|
-
module_fully_qualified_name = T.must(resolved_module.first).name
|
384
|
-
|
385
|
-
case operation
|
386
|
-
when Entry::Prepend
|
387
|
-
# When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
|
388
|
-
# the actual namespace twice. However, it does not check if it has been included because you are allowed to
|
389
|
-
# prepend the same module after it has already been included
|
390
|
-
linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
|
391
|
-
|
392
|
-
# When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
|
393
|
-
# example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
|
394
|
-
# be inserted after `"A`
|
395
|
-
uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
|
396
|
-
insert_position = linearized_prepends.length - uniq_prepends.length
|
397
|
-
|
398
|
-
T.unsafe(ancestors).insert(
|
399
|
-
insert_position,
|
400
|
-
*(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
|
401
|
-
)
|
492
|
+
nesting = T.must(namespaces.first).nesting.flat_map { |n| n.split("::") }
|
402
493
|
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
# prepended or included
|
407
|
-
linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
|
408
|
-
T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
|
494
|
+
if nesting.any?
|
495
|
+
singleton_levels.times do
|
496
|
+
nesting << "<Class:#{T.must(nesting.last)}>"
|
409
497
|
end
|
410
498
|
end
|
411
499
|
|
412
|
-
#
|
413
|
-
#
|
414
|
-
|
415
|
-
|
416
|
-
if
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
end
|
500
|
+
# We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
|
501
|
+
# new singleton methods or to extend a module through an include. There's no need to support instance methods, the
|
502
|
+
# inclusion of another module or the prepending of another module, because those features are already a part of
|
503
|
+
# Ruby and can be used directly without any metaprogramming
|
504
|
+
run_included_hooks(attached_class_name, nesting) if singleton_levels > 0
|
505
|
+
|
506
|
+
linearize_mixins(ancestors, namespaces, nesting)
|
507
|
+
linearize_superclass(
|
508
|
+
ancestors,
|
509
|
+
attached_class_name,
|
510
|
+
fully_qualified_name,
|
511
|
+
namespaces,
|
512
|
+
nesting,
|
513
|
+
singleton_levels,
|
514
|
+
)
|
428
515
|
|
429
516
|
ancestors
|
430
517
|
end
|
@@ -484,15 +571,210 @@ module RubyIndexer
|
|
484
571
|
@ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
|
485
572
|
end
|
486
573
|
|
574
|
+
sig { returns(T::Boolean) }
|
575
|
+
def empty?
|
576
|
+
@entries.empty?
|
577
|
+
end
|
578
|
+
|
579
|
+
sig { returns(T::Array[String]) }
|
580
|
+
def names
|
581
|
+
@entries.keys
|
582
|
+
end
|
583
|
+
|
584
|
+
sig { params(name: String).returns(T::Boolean) }
|
585
|
+
def indexed?(name)
|
586
|
+
@entries.key?(name)
|
587
|
+
end
|
588
|
+
|
589
|
+
sig { returns(Integer) }
|
590
|
+
def length
|
591
|
+
@entries.count
|
592
|
+
end
|
593
|
+
|
594
|
+
sig { params(name: String).returns(Entry::SingletonClass) }
|
595
|
+
def existing_or_new_singleton_class(name)
|
596
|
+
*_namespace, unqualified_name = name.split("::")
|
597
|
+
full_singleton_name = "#{name}::<Class:#{unqualified_name}>"
|
598
|
+
singleton = T.cast(self[full_singleton_name]&.first, T.nilable(Entry::SingletonClass))
|
599
|
+
|
600
|
+
unless singleton
|
601
|
+
attached_ancestor = T.must(self[name]&.first)
|
602
|
+
|
603
|
+
singleton = Entry::SingletonClass.new(
|
604
|
+
[full_singleton_name],
|
605
|
+
attached_ancestor.file_path,
|
606
|
+
attached_ancestor.location,
|
607
|
+
attached_ancestor.name_location,
|
608
|
+
[],
|
609
|
+
nil,
|
610
|
+
)
|
611
|
+
add(singleton, skip_prefix_tree: true)
|
612
|
+
end
|
613
|
+
|
614
|
+
singleton
|
615
|
+
end
|
616
|
+
|
487
617
|
private
|
488
618
|
|
619
|
+
# Runs the registered included hooks
|
620
|
+
sig { params(fully_qualified_name: String, nesting: T::Array[String]).void }
|
621
|
+
def run_included_hooks(fully_qualified_name, nesting)
|
622
|
+
return if @included_hooks.empty?
|
623
|
+
|
624
|
+
namespaces = self[fully_qualified_name]&.grep(Entry::Namespace)
|
625
|
+
return unless namespaces
|
626
|
+
|
627
|
+
namespaces.each do |namespace|
|
628
|
+
namespace.mixin_operations.each do |operation|
|
629
|
+
next unless operation.is_a?(Entry::Include)
|
630
|
+
|
631
|
+
# First we resolve the include name, so that we know the actual module being referred to in the include
|
632
|
+
resolved_modules = resolve(operation.module_name, nesting)
|
633
|
+
next unless resolved_modules
|
634
|
+
|
635
|
+
module_name = T.must(resolved_modules.first).name
|
636
|
+
|
637
|
+
# Then we grab any hooks registered for that module
|
638
|
+
hooks = @included_hooks[module_name]
|
639
|
+
next unless hooks
|
640
|
+
|
641
|
+
# We invoke the hooks with the index and the namespace that included the module
|
642
|
+
hooks.each { |hook| hook.call(self, namespace) }
|
643
|
+
end
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
# Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
|
648
|
+
# linearized ancestors of the mixins
|
649
|
+
sig do
|
650
|
+
params(
|
651
|
+
ancestors: T::Array[String],
|
652
|
+
namespace_entries: T::Array[Entry::Namespace],
|
653
|
+
nesting: T::Array[String],
|
654
|
+
).void
|
655
|
+
end
|
656
|
+
def linearize_mixins(ancestors, namespace_entries, nesting)
|
657
|
+
mixin_operations = namespace_entries.flat_map(&:mixin_operations)
|
658
|
+
main_namespace_index = 0
|
659
|
+
|
660
|
+
mixin_operations.each do |operation|
|
661
|
+
resolved_module = resolve(operation.module_name, nesting)
|
662
|
+
next unless resolved_module
|
663
|
+
|
664
|
+
module_fully_qualified_name = T.must(resolved_module.first).name
|
665
|
+
|
666
|
+
case operation
|
667
|
+
when Entry::Prepend
|
668
|
+
# When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
|
669
|
+
# the actual namespace twice. However, it does not check if it has been included because you are allowed to
|
670
|
+
# prepend the same module after it has already been included
|
671
|
+
linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
|
672
|
+
|
673
|
+
# When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
|
674
|
+
# example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
|
675
|
+
# be inserted after `"A`
|
676
|
+
uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
|
677
|
+
insert_position = linearized_prepends.length - uniq_prepends.length
|
678
|
+
|
679
|
+
T.unsafe(ancestors).insert(
|
680
|
+
insert_position,
|
681
|
+
*(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
|
682
|
+
)
|
683
|
+
|
684
|
+
main_namespace_index += linearized_prepends.length
|
685
|
+
when Entry::Include
|
686
|
+
# When including a module, Ruby will always prevent duplicate entries in case the module has already been
|
687
|
+
# prepended or included
|
688
|
+
linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
|
689
|
+
T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
|
690
|
+
end
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
# Linearize the superclass of a given namespace (including modules with the implicit `Module` superclass). This
|
695
|
+
# method will mutate the `ancestors` array with the linearized ancestors of the superclass
|
696
|
+
sig do
|
697
|
+
params(
|
698
|
+
ancestors: T::Array[String],
|
699
|
+
attached_class_name: String,
|
700
|
+
fully_qualified_name: String,
|
701
|
+
namespace_entries: T::Array[Entry::Namespace],
|
702
|
+
nesting: T::Array[String],
|
703
|
+
singleton_levels: Integer,
|
704
|
+
).void
|
705
|
+
end
|
706
|
+
def linearize_superclass( # rubocop:disable Metrics/ParameterLists
|
707
|
+
ancestors,
|
708
|
+
attached_class_name,
|
709
|
+
fully_qualified_name,
|
710
|
+
namespace_entries,
|
711
|
+
nesting,
|
712
|
+
singleton_levels
|
713
|
+
)
|
714
|
+
# Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
|
715
|
+
# from two diffent classes in different files, we simply ignore it
|
716
|
+
superclass = T.cast(
|
717
|
+
if singleton_levels > 0
|
718
|
+
self[attached_class_name]&.find { |n| n.is_a?(Entry::Class) && n.parent_class }
|
719
|
+
else
|
720
|
+
namespace_entries.find { |n| n.is_a?(Entry::Class) && n.parent_class }
|
721
|
+
end,
|
722
|
+
T.nilable(Entry::Class),
|
723
|
+
)
|
724
|
+
|
725
|
+
if superclass
|
726
|
+
# If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
|
727
|
+
# error. We need to ensure that this isn't the case
|
728
|
+
parent_class = T.must(superclass.parent_class)
|
729
|
+
|
730
|
+
resolved_parent_class = resolve(parent_class, nesting)
|
731
|
+
parent_class_name = resolved_parent_class&.first&.name
|
732
|
+
|
733
|
+
if parent_class_name && fully_qualified_name != parent_class_name
|
734
|
+
|
735
|
+
parent_name_parts = parent_class_name.split("::")
|
736
|
+
singleton_levels.times do
|
737
|
+
parent_name_parts << "<Class:#{parent_name_parts.last}>"
|
738
|
+
end
|
739
|
+
|
740
|
+
ancestors.concat(linearized_ancestors_of(parent_name_parts.join("::")))
|
741
|
+
end
|
742
|
+
|
743
|
+
# When computing the linearization for a class's singleton class, it inherits from the linearized ancestors of
|
744
|
+
# the `Class` class
|
745
|
+
if parent_class_name&.start_with?("BasicObject") && singleton_levels > 0
|
746
|
+
class_class_name_parts = ["Class"]
|
747
|
+
|
748
|
+
(singleton_levels - 1).times do
|
749
|
+
class_class_name_parts << "<Class:#{class_class_name_parts.last}>"
|
750
|
+
end
|
751
|
+
|
752
|
+
ancestors.concat(linearized_ancestors_of(class_class_name_parts.join("::")))
|
753
|
+
end
|
754
|
+
elsif singleton_levels > 0
|
755
|
+
# When computing the linearization for a module's singleton class, it inherits from the linearized ancestors of
|
756
|
+
# the `Module` class
|
757
|
+
mod = T.cast(self[attached_class_name]&.find { |n| n.is_a?(Entry::Module) }, T.nilable(Entry::Module))
|
758
|
+
|
759
|
+
if mod
|
760
|
+
module_class_name_parts = ["Module"]
|
761
|
+
|
762
|
+
(singleton_levels - 1).times do
|
763
|
+
module_class_name_parts << "<Class:#{module_class_name_parts.last}>"
|
764
|
+
end
|
765
|
+
|
766
|
+
ancestors.concat(linearized_ancestors_of(module_class_name_parts.join("::")))
|
767
|
+
end
|
768
|
+
end
|
769
|
+
end
|
770
|
+
|
489
771
|
# Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
|
490
772
|
# that doesn't exist, then we return the same UnresolvedAlias
|
491
773
|
sig do
|
492
774
|
params(
|
493
|
-
entry: Entry::
|
775
|
+
entry: Entry::UnresolvedConstantAlias,
|
494
776
|
seen_names: T::Array[String],
|
495
|
-
).returns(T.any(Entry::
|
777
|
+
).returns(T.any(Entry::ConstantAlias, Entry::UnresolvedConstantAlias))
|
496
778
|
end
|
497
779
|
def resolve_alias(entry, seen_names)
|
498
780
|
alias_name = entry.name
|
@@ -504,7 +786,7 @@ module RubyIndexer
|
|
504
786
|
return entry unless target
|
505
787
|
|
506
788
|
target_name = T.must(target.first).name
|
507
|
-
resolved_alias = Entry::
|
789
|
+
resolved_alias = Entry::ConstantAlias.new(target_name, entry)
|
508
790
|
|
509
791
|
# Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
|
510
792
|
original_entries = T.must(@entries[alias_name])
|
@@ -521,7 +803,11 @@ module RubyIndexer
|
|
521
803
|
name: String,
|
522
804
|
nesting: T::Array[String],
|
523
805
|
seen_names: T::Array[String],
|
524
|
-
).returns(T.nilable(T::Array[
|
806
|
+
).returns(T.nilable(T::Array[T.any(
|
807
|
+
Entry::Namespace,
|
808
|
+
Entry::ConstantAlias,
|
809
|
+
Entry::UnresolvedConstantAlias,
|
810
|
+
)]))
|
525
811
|
end
|
526
812
|
def lookup_enclosing_scopes(name, nesting, seen_names)
|
527
813
|
nesting.length.downto(1).each do |i|
|
@@ -546,7 +832,11 @@ module RubyIndexer
|
|
546
832
|
name: String,
|
547
833
|
nesting: T::Array[String],
|
548
834
|
seen_names: T::Array[String],
|
549
|
-
).returns(T.nilable(T::Array[
|
835
|
+
).returns(T.nilable(T::Array[T.any(
|
836
|
+
Entry::Namespace,
|
837
|
+
Entry::ConstantAlias,
|
838
|
+
Entry::UnresolvedConstantAlias,
|
839
|
+
)]))
|
550
840
|
end
|
551
841
|
def lookup_ancestor_chain(name, nesting, seen_names)
|
552
842
|
*nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
|
@@ -598,15 +888,29 @@ module RubyIndexer
|
|
598
888
|
end
|
599
889
|
end
|
600
890
|
|
601
|
-
sig
|
891
|
+
sig do
|
892
|
+
params(
|
893
|
+
full_name: String,
|
894
|
+
seen_names: T::Array[String],
|
895
|
+
).returns(
|
896
|
+
T.nilable(T::Array[T.any(
|
897
|
+
Entry::Namespace,
|
898
|
+
Entry::ConstantAlias,
|
899
|
+
Entry::UnresolvedConstantAlias,
|
900
|
+
)]),
|
901
|
+
)
|
902
|
+
end
|
602
903
|
def direct_or_aliased_constant(full_name, seen_names)
|
603
904
|
entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
|
604
|
-
entries&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
|
605
|
-
end
|
606
905
|
|
607
|
-
|
608
|
-
|
609
|
-
|
906
|
+
T.cast(
|
907
|
+
entries&.map { |e| e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e },
|
908
|
+
T.nilable(T::Array[T.any(
|
909
|
+
Entry::Namespace,
|
910
|
+
Entry::ConstantAlias,
|
911
|
+
Entry::UnresolvedConstantAlias,
|
912
|
+
)]),
|
913
|
+
)
|
610
914
|
end
|
611
915
|
|
612
916
|
# Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to
|