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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +26 -1
  5. data/exe/ruby-lsp-check +1 -1
  6. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +74 -43
  7. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +26 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +147 -29
  9. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +383 -79
  10. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +195 -61
  11. data/lib/ruby_indexer/ruby_indexer.rb +1 -8
  12. data/lib/ruby_indexer/test/classes_and_modules_test.rb +71 -3
  13. data/lib/ruby_indexer/test/configuration_test.rb +1 -1
  14. data/lib/ruby_indexer/test/constant_test.rb +17 -17
  15. data/lib/ruby_indexer/test/enhancements_test.rb +197 -0
  16. data/lib/ruby_indexer/test/index_test.rb +367 -17
  17. data/lib/ruby_indexer/test/method_test.rb +58 -25
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +297 -0
  19. data/lib/ruby_indexer/test/test_case.rb +1 -5
  20. data/lib/ruby_lsp/addon.rb +22 -5
  21. data/lib/ruby_lsp/base_server.rb +8 -3
  22. data/lib/ruby_lsp/document.rb +27 -46
  23. data/lib/ruby_lsp/erb_document.rb +125 -0
  24. data/lib/ruby_lsp/global_state.rb +47 -19
  25. data/lib/ruby_lsp/internal.rb +2 -0
  26. data/lib/ruby_lsp/listeners/completion.rb +161 -57
  27. data/lib/ruby_lsp/listeners/definition.rb +91 -27
  28. data/lib/ruby_lsp/listeners/document_highlight.rb +5 -1
  29. data/lib/ruby_lsp/listeners/hover.rb +61 -19
  30. data/lib/ruby_lsp/listeners/signature_help.rb +13 -6
  31. data/lib/ruby_lsp/node_context.rb +65 -5
  32. data/lib/ruby_lsp/requests/code_action_resolve.rb +107 -9
  33. data/lib/ruby_lsp/requests/code_actions.rb +11 -2
  34. data/lib/ruby_lsp/requests/completion.rb +4 -4
  35. data/lib/ruby_lsp/requests/completion_resolve.rb +14 -9
  36. data/lib/ruby_lsp/requests/definition.rb +18 -8
  37. data/lib/ruby_lsp/requests/diagnostics.rb +6 -5
  38. data/lib/ruby_lsp/requests/document_symbol.rb +2 -7
  39. data/lib/ruby_lsp/requests/folding_ranges.rb +6 -2
  40. data/lib/ruby_lsp/requests/formatting.rb +15 -0
  41. data/lib/ruby_lsp/requests/hover.rb +5 -5
  42. data/lib/ruby_lsp/requests/on_type_formatting.rb +6 -4
  43. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  44. data/lib/ruby_lsp/requests/show_syntax_tree.rb +3 -2
  45. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  46. data/lib/ruby_lsp/requests/support/common.rb +11 -2
  47. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +2 -6
  48. data/lib/ruby_lsp/ruby_document.rb +74 -0
  49. data/lib/ruby_lsp/server.rb +129 -54
  50. data/lib/ruby_lsp/store.rb +33 -9
  51. data/lib/ruby_lsp/test_helper.rb +3 -1
  52. data/lib/ruby_lsp/type_inferrer.rb +61 -25
  53. data/lib/ruby_lsp/utils.rb +13 -0
  54. metadata +9 -8
  55. 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.filter_map do |entry|
154
- case entry
155
- when Entry::Member, Entry::MethodAlias
156
- entry if ancestors.any?(entry.owner&.name)
157
- when Entry::UnresolvedMethodAlias
158
- if ancestors.any?(entry.owner&.name)
159
- resolved_alias = resolve_method_alias(entry, receiver_name)
160
- resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
161
- end
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[Entry]))
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
- search_top_level(name, seen_names)
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: RubyIndexer.configuration.indexables, &block)
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(self, dispatcher, result, indexable_path.full_path)
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::Alias
377
+ when Entry::ConstantAlias
280
378
  target = entry.target
281
379
  return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
282
- when Entry::UnresolvedAlias
380
+ when Entry::UnresolvedConstantAlias
283
381
  resolved = resolve_alias(entry, seen_names)
284
382
 
285
- if resolved.is_a?(Entry::UnresolvedAlias)
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::Alias
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
- main_namespace_index += linearized_prepends.length
404
- when Entry::Include
405
- # When including a module, Ruby will always prevent duplicate entries in case the module has already been
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
- # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
413
- # from two diffent classes in different files, we simply ignore it
414
- superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
415
-
416
- if superclass
417
- # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
418
- # error. We need to ensure that this isn't the case
419
- parent_class = T.must(superclass.parent_class)
420
-
421
- resolved_parent_class = resolve(parent_class, nesting)
422
- parent_class_name = resolved_parent_class&.first&.name
423
-
424
- if parent_class_name && fully_qualified_name != parent_class_name
425
- ancestors.concat(linearized_ancestors_of(parent_class_name))
426
- end
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::UnresolvedAlias,
775
+ entry: Entry::UnresolvedConstantAlias,
494
776
  seen_names: T::Array[String],
495
- ).returns(T.any(Entry::Alias, Entry::UnresolvedAlias))
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::Alias.new(target_name, 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[Entry]))
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[Entry]))
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 { params(full_name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
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
- sig { params(name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
608
- def search_top_level(name, seen_names)
609
- @entries[name]&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
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