ruby-lsp 0.17.3 → 0.17.5

Sign up to get free protection for your applications and to get access to all the features.
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