ruby-lsp 0.17.3 → 0.17.5

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +251 -100
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +173 -114
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +337 -77
  7. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +43 -14
  8. data/lib/ruby_indexer/test/classes_and_modules_test.rb +79 -3
  9. data/lib/ruby_indexer/test/index_test.rb +563 -29
  10. data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
  11. data/lib/ruby_indexer/test/method_test.rb +75 -25
  12. data/lib/ruby_indexer/test/rbs_indexer_test.rb +38 -2
  13. data/lib/ruby_indexer/test/test_case.rb +1 -5
  14. data/lib/ruby_lsp/addon.rb +13 -1
  15. data/lib/ruby_lsp/document.rb +50 -23
  16. data/lib/ruby_lsp/erb_document.rb +125 -0
  17. data/lib/ruby_lsp/global_state.rb +11 -4
  18. data/lib/ruby_lsp/internal.rb +3 -0
  19. data/lib/ruby_lsp/listeners/completion.rb +69 -34
  20. data/lib/ruby_lsp/listeners/definition.rb +34 -23
  21. data/lib/ruby_lsp/listeners/hover.rb +14 -7
  22. data/lib/ruby_lsp/listeners/signature_help.rb +5 -2
  23. data/lib/ruby_lsp/node_context.rb +6 -1
  24. data/lib/ruby_lsp/requests/code_action_resolve.rb +2 -2
  25. data/lib/ruby_lsp/requests/completion.rb +6 -5
  26. data/lib/ruby_lsp/requests/completion_resolve.rb +7 -4
  27. data/lib/ruby_lsp/requests/definition.rb +4 -3
  28. data/lib/ruby_lsp/requests/formatting.rb +2 -0
  29. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +88 -0
  30. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  31. data/lib/ruby_lsp/requests/show_syntax_tree.rb +3 -2
  32. data/lib/ruby_lsp/requests/support/common.rb +19 -1
  33. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +12 -4
  34. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +87 -0
  35. data/lib/ruby_lsp/requests/workspace_symbol.rb +1 -21
  36. data/lib/ruby_lsp/requests.rb +2 -0
  37. data/lib/ruby_lsp/ruby_document.rb +10 -0
  38. data/lib/ruby_lsp/server.rb +95 -26
  39. data/lib/ruby_lsp/store.rb +23 -8
  40. data/lib/ruby_lsp/test_helper.rb +3 -1
  41. data/lib/ruby_lsp/type_inferrer.rb +86 -0
  42. metadata +10 -6
@@ -65,13 +65,13 @@ module RubyIndexer
65
65
  @require_paths_tree.delete(require_path) if require_path
66
66
  end
67
67
 
68
- sig { params(entry: Entry).void }
69
- def <<(entry)
68
+ sig { params(entry: Entry, skip_prefix_tree: T::Boolean).void }
69
+ def add(entry, skip_prefix_tree: false)
70
70
  name = entry.name
71
71
 
72
72
  (@entries[name] ||= []) << entry
73
73
  (@files_to_entries[entry.file_path] ||= []) << entry
74
- @entries_tree.insert(name, T.must(@entries[name]))
74
+ @entries_tree.insert(name, T.must(@entries[name])) unless skip_prefix_tree
75
75
  end
76
76
 
77
77
  sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) }
@@ -118,11 +118,21 @@ module RubyIndexer
118
118
  # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
119
119
  sig { params(query: T.nilable(String)).returns(T::Array[Entry]) }
120
120
  def fuzzy_search(query)
121
- return @entries.flat_map { |_name, entries| entries } unless query
121
+ unless query
122
+ entries = @entries.filter_map do |_name, entries|
123
+ next if entries.first.is_a?(Entry::SingletonClass)
124
+
125
+ entries
126
+ end
127
+
128
+ return entries.flatten
129
+ end
122
130
 
123
131
  normalized_query = query.gsub("::", "").downcase
124
132
 
125
133
  results = @entries.filter_map do |name, entries|
134
+ next if entries.first.is_a?(Entry::SingletonClass)
135
+
126
136
  similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
127
137
  [entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
128
138
  end
@@ -130,14 +140,52 @@ module RubyIndexer
130
140
  results.flat_map(&:first)
131
141
  end
132
142
 
133
- sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) }
143
+ sig do
144
+ params(
145
+ name: T.nilable(String),
146
+ receiver_name: String,
147
+ ).returns(T::Array[T.any(Entry::Member, Entry::MethodAlias)])
148
+ end
134
149
  def method_completion_candidates(name, receiver_name)
135
150
  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)
151
+
152
+ candidates = name ? prefix_search(name).flatten : @entries.values.flatten
153
+ completion_items = candidates.each_with_object({}) do |entry, hash|
154
+ unless entry.is_a?(Entry::Member) || entry.is_a?(Entry::MethodAlias) ||
155
+ entry.is_a?(Entry::UnresolvedMethodAlias)
156
+ next
157
+ end
158
+
159
+ entry_name = entry.name
160
+ ancestor_index = ancestors.index(entry.owner&.name)
161
+ existing_entry, existing_entry_index = hash[entry_name]
162
+
163
+ # Conditions for matching a method completion candidate:
164
+ # 1. If an ancestor_index was found, it means that this method is owned by the receiver. The exact index is
165
+ # where in the ancestor chain the method was found. For example, if the ancestors are ["A", "B", "C"] and we
166
+ # found the method declared in `B`, then the ancestors index is 1
167
+ #
168
+ # 2. We already established that this method is owned by the receiver. Now, check if we already added a
169
+ # completion candidate for this method name. If not, then we just go and add it (the left hand side of the or)
170
+ #
171
+ # 3. If we had already found a method entry for the same name, then we need to check if the current entry that
172
+ # we are comparing appears first in the hierarchy or not. For example, imagine we have the method `open` defined
173
+ # in both `File` and its parent `IO`. If we first find the method `open` in `IO`, it will be inserted into the
174
+ # hash. Then, when we find the entry for `open` owned by `File`, we need to replace `IO.open` by `File.open`,
175
+ # since `File.open` appears first in the hierarchy chain and is therefore the correct method being invoked. The
176
+ # last part of the conditional checks if the current entry was found earlier in the hierarchy chain, in which
177
+ # case we must update the existing entry to avoid showing the wrong method declaration for overridden methods
178
+ next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
179
+
180
+ if entry.is_a?(Entry::UnresolvedMethodAlias)
181
+ resolved_alias = resolve_method_alias(entry, receiver_name)
182
+ hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
183
+ else
184
+ hash[entry_name] = [entry, ancestor_index]
185
+ end
139
186
  end
140
- candidates
187
+
188
+ completion_items.values.map!(&:first)
141
189
  end
142
190
 
143
191
  # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
@@ -152,7 +200,11 @@ module RubyIndexer
152
200
  name: String,
153
201
  nesting: T::Array[String],
154
202
  seen_names: T::Array[String],
155
- ).returns(T.nilable(T::Array[Entry]))
203
+ ).returns(T.nilable(T::Array[T.any(
204
+ Entry::Namespace,
205
+ Entry::Alias,
206
+ Entry::UnresolvedAlias,
207
+ )]))
156
208
  end
157
209
  def resolve(name, nesting, seen_names = [])
158
210
  # If we have a top level reference, then we just search for it straight away ignoring the nesting
@@ -181,7 +233,7 @@ module RubyIndexer
181
233
  return entries if entries
182
234
 
183
235
  # Finally, as a fallback, Ruby will search for the constant in the top level namespace
184
- search_top_level(name, seen_names)
236
+ direct_or_aliased_constant(name, seen_names)
185
237
  rescue UnresolvableAliasError
186
238
  nil
187
239
  end
@@ -223,6 +275,12 @@ module RubyIndexer
223
275
  rescue Errno::EISDIR, Errno::ENOENT
224
276
  # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
225
277
  # it
278
+ rescue SystemStackError => e
279
+ if e.backtrace&.first&.include?("prism")
280
+ $stderr.puts "Prism error indexing #{indexable_path.full_path}: #{e.message}"
281
+ else
282
+ raise
283
+ end
226
284
  end
227
285
 
228
286
  # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
@@ -269,20 +327,32 @@ module RubyIndexer
269
327
 
270
328
  # Attempts to find methods for a resolved fully qualified receiver name.
271
329
  # Returns `nil` if the method does not exist on that receiver
272
- sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
330
+ sig do
331
+ params(
332
+ method_name: String,
333
+ receiver_name: String,
334
+ ).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)]))
335
+ end
273
336
  def resolve_method(method_name, receiver_name)
274
337
  method_entries = self[method_name]
275
- ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
276
338
  return unless method_entries
277
339
 
340
+ ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
278
341
  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
342
+ found = method_entries.filter_map do |entry|
343
+ case entry
344
+ when Entry::Member, Entry::MethodAlias
345
+ entry if entry.owner&.name == ancestor
346
+ when Entry::UnresolvedMethodAlias
347
+ # Resolve aliases lazily as we find them
348
+ if entry.owner&.name == ancestor
349
+ resolved_alias = resolve_method_alias(entry, receiver_name)
350
+ resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
351
+ end
352
+ end
283
353
  end
284
354
 
285
- return T.cast(found, T::Array[Entry::Member]) if found.any?
355
+ return found if found.any?
286
356
  end
287
357
 
288
358
  nil
@@ -304,8 +374,25 @@ module RubyIndexer
304
374
  cached_ancestors = @ancestors[fully_qualified_name]
305
375
  return cached_ancestors if cached_ancestors
306
376
 
377
+ parts = fully_qualified_name.split("::")
378
+ singleton_levels = 0
379
+
380
+ parts.reverse_each do |part|
381
+ break unless part.include?("<Class:")
382
+
383
+ singleton_levels += 1
384
+ parts.pop
385
+ end
386
+
387
+ attached_class_name = parts.join("::")
388
+
307
389
  # If we don't have an entry for `name`, raise
308
390
  entries = self[fully_qualified_name]
391
+
392
+ if singleton_levels > 0 && !entries
393
+ entries = [existing_or_new_singleton_class(attached_class_name)]
394
+ end
395
+
309
396
  raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
310
397
 
311
398
  ancestors = [fully_qualified_name]
@@ -328,62 +415,25 @@ module RubyIndexer
328
415
  raise NonExistingNamespaceError,
329
416
  "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
330
417
 
331
- mixin_operations = namespaces.flat_map(&:mixin_operations)
332
- main_namespace_index = 0
333
-
334
418
  # The original nesting where we discovered this namespace, so that we resolve the correct names of the
335
419
  # included/prepended/extended modules and parent classes
336
420
  nesting = T.must(namespaces.first).nesting
337
421
 
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))
422
+ if nesting.any?
423
+ singleton_levels.times do
424
+ nesting << "<Class:#{T.must(nesting.last)}>"
368
425
  end
369
426
  end
370
427
 
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
428
+ linearize_mixins(ancestors, namespaces, nesting)
429
+ linearize_superclass(
430
+ ancestors,
431
+ attached_class_name,
432
+ fully_qualified_name,
433
+ namespaces,
434
+ nesting,
435
+ singleton_levels,
436
+ )
387
437
 
388
438
  ancestors
389
439
  end
@@ -443,8 +493,175 @@ module RubyIndexer
443
493
  @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
444
494
  end
445
495
 
496
+ sig { returns(T::Boolean) }
497
+ def empty?
498
+ @entries.empty?
499
+ end
500
+
501
+ sig { returns(T::Array[String]) }
502
+ def names
503
+ @entries.keys
504
+ end
505
+
506
+ sig { params(name: String).returns(T::Boolean) }
507
+ def indexed?(name)
508
+ @entries.key?(name)
509
+ end
510
+
511
+ sig { returns(Integer) }
512
+ def length
513
+ @entries.count
514
+ end
515
+
516
+ sig { params(name: String).returns(Entry::SingletonClass) }
517
+ def existing_or_new_singleton_class(name)
518
+ *_namespace, unqualified_name = name.split("::")
519
+ full_singleton_name = "#{name}::<Class:#{unqualified_name}>"
520
+ singleton = T.cast(self[full_singleton_name]&.first, T.nilable(Entry::SingletonClass))
521
+
522
+ unless singleton
523
+ attached_ancestor = T.must(self[name]&.first)
524
+
525
+ singleton = Entry::SingletonClass.new(
526
+ [full_singleton_name],
527
+ attached_ancestor.file_path,
528
+ attached_ancestor.location,
529
+ attached_ancestor.name_location,
530
+ [],
531
+ nil,
532
+ )
533
+ add(singleton, skip_prefix_tree: true)
534
+ end
535
+
536
+ singleton
537
+ end
538
+
446
539
  private
447
540
 
541
+ # Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
542
+ # linearized ancestors of the mixins
543
+ sig do
544
+ params(
545
+ ancestors: T::Array[String],
546
+ namespace_entries: T::Array[Entry::Namespace],
547
+ nesting: T::Array[String],
548
+ ).void
549
+ end
550
+ def linearize_mixins(ancestors, namespace_entries, nesting)
551
+ mixin_operations = namespace_entries.flat_map(&:mixin_operations)
552
+ main_namespace_index = 0
553
+
554
+ mixin_operations.each do |operation|
555
+ resolved_module = resolve(operation.module_name, nesting)
556
+ next unless resolved_module
557
+
558
+ module_fully_qualified_name = T.must(resolved_module.first).name
559
+
560
+ case operation
561
+ when Entry::Prepend
562
+ # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
563
+ # the actual namespace twice. However, it does not check if it has been included because you are allowed to
564
+ # prepend the same module after it has already been included
565
+ linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
566
+
567
+ # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
568
+ # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
569
+ # be inserted after `"A`
570
+ uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
571
+ insert_position = linearized_prepends.length - uniq_prepends.length
572
+
573
+ T.unsafe(ancestors).insert(
574
+ insert_position,
575
+ *(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
576
+ )
577
+
578
+ main_namespace_index += linearized_prepends.length
579
+ when Entry::Include
580
+ # When including a module, Ruby will always prevent duplicate entries in case the module has already been
581
+ # prepended or included
582
+ linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
583
+ T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
584
+ end
585
+ end
586
+ end
587
+
588
+ # Linearize the superclass of a given namespace (including modules with the implicit `Module` superclass). This
589
+ # method will mutate the `ancestors` array with the linearized ancestors of the superclass
590
+ sig do
591
+ params(
592
+ ancestors: T::Array[String],
593
+ attached_class_name: String,
594
+ fully_qualified_name: String,
595
+ namespace_entries: T::Array[Entry::Namespace],
596
+ nesting: T::Array[String],
597
+ singleton_levels: Integer,
598
+ ).void
599
+ end
600
+ def linearize_superclass( # rubocop:disable Metrics/ParameterLists
601
+ ancestors,
602
+ attached_class_name,
603
+ fully_qualified_name,
604
+ namespace_entries,
605
+ nesting,
606
+ singleton_levels
607
+ )
608
+ # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
609
+ # from two diffent classes in different files, we simply ignore it
610
+ superclass = T.cast(
611
+ if singleton_levels > 0
612
+ self[attached_class_name]&.find { |n| n.is_a?(Entry::Class) && n.parent_class }
613
+ else
614
+ namespace_entries.find { |n| n.is_a?(Entry::Class) && n.parent_class }
615
+ end,
616
+ T.nilable(Entry::Class),
617
+ )
618
+
619
+ if superclass
620
+ # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
621
+ # error. We need to ensure that this isn't the case
622
+ parent_class = T.must(superclass.parent_class)
623
+
624
+ resolved_parent_class = resolve(parent_class, nesting)
625
+ parent_class_name = resolved_parent_class&.first&.name
626
+
627
+ if parent_class_name && fully_qualified_name != parent_class_name
628
+
629
+ parent_name_parts = [parent_class_name]
630
+ singleton_levels.times do
631
+ parent_name_parts << "<Class:#{parent_name_parts.last}>"
632
+ end
633
+
634
+ ancestors.concat(linearized_ancestors_of(parent_name_parts.join("::")))
635
+ end
636
+
637
+ # When computing the linearization for a class's singleton class, it inherits from the linearized ancestors of
638
+ # the `Class` class
639
+ if parent_class_name&.start_with?("BasicObject") && singleton_levels > 0
640
+ class_class_name_parts = ["Class"]
641
+
642
+ (singleton_levels - 1).times do
643
+ class_class_name_parts << "<Class:#{class_class_name_parts.last}>"
644
+ end
645
+
646
+ ancestors.concat(linearized_ancestors_of(class_class_name_parts.join("::")))
647
+ end
648
+ elsif singleton_levels > 0
649
+ # When computing the linearization for a module's singleton class, it inherits from the linearized ancestors of
650
+ # the `Module` class
651
+ mod = T.cast(self[attached_class_name]&.find { |n| n.is_a?(Entry::Module) }, T.nilable(Entry::Module))
652
+
653
+ if mod
654
+ module_class_name_parts = ["Module"]
655
+
656
+ (singleton_levels - 1).times do
657
+ module_class_name_parts << "<Class:#{module_class_name_parts.last}>"
658
+ end
659
+
660
+ ancestors.concat(linearized_ancestors_of(module_class_name_parts.join("::")))
661
+ end
662
+ end
663
+ end
664
+
448
665
  # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
449
666
  # that doesn't exist, then we return the same UnresolvedAlias
450
667
  sig do
@@ -480,7 +697,11 @@ module RubyIndexer
480
697
  name: String,
481
698
  nesting: T::Array[String],
482
699
  seen_names: T::Array[String],
483
- ).returns(T.nilable(T::Array[Entry]))
700
+ ).returns(T.nilable(T::Array[T.any(
701
+ Entry::Namespace,
702
+ Entry::Alias,
703
+ Entry::UnresolvedAlias,
704
+ )]))
484
705
  end
485
706
  def lookup_enclosing_scopes(name, nesting, seen_names)
486
707
  nesting.length.downto(1).each do |i|
@@ -505,16 +726,20 @@ module RubyIndexer
505
726
  name: String,
506
727
  nesting: T::Array[String],
507
728
  seen_names: T::Array[String],
508
- ).returns(T.nilable(T::Array[Entry]))
729
+ ).returns(T.nilable(T::Array[T.any(
730
+ Entry::Namespace,
731
+ Entry::Alias,
732
+ Entry::UnresolvedAlias,
733
+ )]))
509
734
  end
510
735
  def lookup_ancestor_chain(name, nesting, seen_names)
511
736
  *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
512
- return if T.must(nesting_parts).empty?
737
+ return if nesting_parts.empty?
513
738
 
514
- namespace_entries = resolve(T.must(nesting_parts).join("::"), [], seen_names)
739
+ namespace_entries = resolve(nesting_parts.join("::"), [], seen_names)
515
740
  return unless namespace_entries
516
741
 
517
- ancestors = T.must(nesting_parts).empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
742
+ ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
518
743
 
519
744
  ancestors.each do |ancestor_name|
520
745
  entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
@@ -557,15 +782,50 @@ module RubyIndexer
557
782
  end
558
783
  end
559
784
 
560
- sig { params(full_name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
785
+ sig do
786
+ params(
787
+ full_name: String,
788
+ seen_names: T::Array[String],
789
+ ).returns(
790
+ T.nilable(T::Array[T.any(
791
+ Entry::Namespace,
792
+ Entry::Alias,
793
+ Entry::UnresolvedAlias,
794
+ )]),
795
+ )
796
+ end
561
797
  def direct_or_aliased_constant(full_name, seen_names)
562
798
  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 }
799
+
800
+ T.cast(
801
+ entries&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e },
802
+ T.nilable(T::Array[T.any(
803
+ Entry::Namespace,
804
+ Entry::Alias,
805
+ Entry::UnresolvedAlias,
806
+ )]),
807
+ )
564
808
  end
565
809
 
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 }
810
+ # Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to
811
+ # identify the target or the same unresolved alias entry if we couldn't
812
+ sig do
813
+ params(
814
+ entry: Entry::UnresolvedMethodAlias,
815
+ receiver_name: String,
816
+ ).returns(T.any(Entry::MethodAlias, Entry::UnresolvedMethodAlias))
817
+ end
818
+ def resolve_method_alias(entry, receiver_name)
819
+ return entry if entry.new_name == entry.old_name
820
+
821
+ target_method_entries = resolve_method(entry.old_name, receiver_name)
822
+ return entry unless target_method_entries
823
+
824
+ resolved_alias = Entry::MethodAlias.new(T.must(target_method_entries.first), entry)
825
+ original_entries = T.must(@entries[entry.new_name])
826
+ original_entries.delete(entry)
827
+ original_entries << resolved_alias
828
+ resolved_alias
569
829
  end
570
830
  end
571
831
  end
@@ -48,9 +48,14 @@ module RubyIndexer
48
48
  location = to_ruby_indexer_location(declaration.location)
49
49
  comments = Array(declaration.comment&.string)
50
50
  parent_class = declaration.super_class&.name&.name&.to_s
51
- class_entry = Entry::Class.new(nesting, file_path, location, comments, parent_class)
51
+ class_entry = Entry::Class.new(nesting, file_path, location, location, comments, parent_class)
52
52
  add_declaration_mixins_to_entry(declaration, class_entry)
53
- @index << class_entry
53
+ @index.add(class_entry)
54
+ declaration.members.each do |member|
55
+ next unless member.is_a?(RBS::AST::Members::MethodDefinition)
56
+
57
+ handle_method(member, class_entry)
58
+ end
54
59
  end
55
60
 
56
61
  sig { params(declaration: RBS::AST::Declarations::Module, pathname: Pathname).void }
@@ -59,9 +64,14 @@ module RubyIndexer
59
64
  file_path = pathname.to_s
60
65
  location = to_ruby_indexer_location(declaration.location)
61
66
  comments = Array(declaration.comment&.string)
62
- module_entry = Entry::Module.new(nesting, file_path, location, comments)
67
+ module_entry = Entry::Module.new(nesting, file_path, location, location, comments)
63
68
  add_declaration_mixins_to_entry(declaration, module_entry)
64
- @index << module_entry
69
+ @index.add(module_entry)
70
+ declaration.members.each do |member|
71
+ next unless member.is_a?(RBS::AST::Members::MethodDefinition)
72
+
73
+ handle_method(member, module_entry)
74
+ end
65
75
  end
66
76
 
67
77
  sig { params(rbs_location: RBS::Location).returns(RubyIndexer::Location) }
@@ -83,17 +93,36 @@ module RubyIndexer
83
93
  def add_declaration_mixins_to_entry(declaration, entry)
84
94
  declaration.each_mixin do |mixin|
85
95
  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
+ case mixin
97
+ when RBS::AST::Members::Include
98
+ entry.mixin_operations << Entry::Include.new(name)
99
+ when RBS::AST::Members::Prepend
100
+ entry.mixin_operations << Entry::Prepend.new(name)
101
+ when RBS::AST::Members::Extend
102
+ singleton = @index.existing_or_new_singleton_class(entry.name)
103
+ singleton.mixin_operations << Entry::Include.new(name)
104
+ end
96
105
  end
97
106
  end
107
+
108
+ sig { params(member: RBS::AST::Members::MethodDefinition, owner: Entry::Namespace).void }
109
+ def handle_method(member, owner)
110
+ name = member.name.name
111
+ file_path = member.location.buffer.name
112
+ location = to_ruby_indexer_location(member.location)
113
+ comments = Array(member.comment&.string)
114
+
115
+ visibility = case member.visibility
116
+ when :private
117
+ Entry::Visibility::PRIVATE
118
+ when :protected
119
+ Entry::Visibility::PROTECTED
120
+ else
121
+ Entry::Visibility::PUBLIC
122
+ end
123
+
124
+ real_owner = member.singleton? ? @index.existing_or_new_singleton_class(owner.name) : owner
125
+ @index.add(Entry::Method.new(name, file_path, location, location, comments, [], visibility, real_owner))
126
+ end
98
127
  end
99
128
  end