ruby-lsp 0.27.0.beta1 → 0.27.0.beta3

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +0 -46
  4. data/exe/ruby-lsp-check +0 -15
  5. data/lib/ruby_lsp/addon.rb +19 -19
  6. data/lib/ruby_lsp/global_state.rb +1 -6
  7. data/lib/ruby_lsp/internal.rb +3 -2
  8. data/lib/ruby_lsp/listeners/code_lens.rb +1 -1
  9. data/lib/ruby_lsp/listeners/completion.rb +246 -382
  10. data/lib/ruby_lsp/listeners/definition.rb +7 -10
  11. data/lib/ruby_lsp/listeners/document_link.rb +4 -0
  12. data/lib/ruby_lsp/listeners/hover.rb +234 -82
  13. data/lib/ruby_lsp/listeners/signature_help.rb +11 -12
  14. data/lib/ruby_lsp/listeners/spec_style.rb +6 -1
  15. data/lib/ruby_lsp/listeners/test_discovery.rb +38 -15
  16. data/lib/ruby_lsp/listeners/test_style.rb +21 -9
  17. data/lib/ruby_lsp/node_context.rb +31 -8
  18. data/lib/ruby_lsp/requests/completion_resolve.rb +55 -39
  19. data/lib/ruby_lsp/requests/discover_tests.rb +5 -41
  20. data/lib/ruby_lsp/requests/hover.rb +2 -5
  21. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +66 -22
  22. data/lib/ruby_lsp/requests/references.rb +180 -66
  23. data/lib/ruby_lsp/requests/rename.rb +1 -1
  24. data/lib/ruby_lsp/requests/request.rb +3 -33
  25. data/lib/ruby_lsp/requests/support/common.rb +82 -68
  26. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +82 -46
  27. data/lib/ruby_lsp/ruby_document.rb +0 -73
  28. data/lib/ruby_lsp/rubydex/declaration.rb +174 -0
  29. data/lib/ruby_lsp/rubydex/definition.rb +73 -0
  30. data/lib/ruby_lsp/rubydex/reference.rb +6 -1
  31. data/lib/ruby_lsp/rubydex/signature.rb +107 -0
  32. data/lib/ruby_lsp/scripts/compose_bundle.rb +1 -1
  33. data/lib/ruby_lsp/server.rb +56 -171
  34. data/lib/ruby_lsp/test_helper.rb +0 -1
  35. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +1 -1
  36. data/lib/ruby_lsp/type_inferrer.rb +89 -11
  37. metadata +12 -18
  38. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +0 -276
  39. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +0 -1101
  40. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +0 -44
  41. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +0 -605
  42. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +0 -1077
  43. data/lib/ruby_indexer/lib/ruby_indexer/location.rb +0 -37
  44. data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +0 -149
  45. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +0 -294
  46. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +0 -335
  47. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +0 -32
  48. data/lib/ruby_indexer/ruby_indexer.rb +0 -20
  49. data/lib/ruby_lsp/static_docs.rb +0 -20
  50. data/static_docs/break.md +0 -103
  51. data/static_docs/yield.md +0 -81
  52. /data/lib/{ruby_indexer/lib/ruby_indexer → ruby_lsp}/uri.rb +0 -0
@@ -1,1077 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module RubyIndexer
5
- class Index
6
- class UnresolvableAliasError < StandardError; end
7
- class NonExistingNamespaceError < StandardError; end
8
- class IndexNotEmptyError < StandardError; end
9
-
10
- # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
11
- ENTRY_SIMILARITY_THRESHOLD = 0.7
12
-
13
- #: Configuration
14
- attr_reader :configuration
15
-
16
- #: bool
17
- attr_reader :initial_indexing_completed
18
-
19
- class << self
20
- # Returns the real nesting of a constant name taking into account top level
21
- # references that may be included anywhere in the name or nesting where that
22
- # constant was found
23
- #: (Array[String] stack, String? name) -> Array[String]
24
- def actual_nesting(stack, name)
25
- nesting = name ? stack + [name] : stack
26
- corrected_nesting = []
27
-
28
- nesting.reverse_each do |name|
29
- corrected_nesting.prepend(name.delete_prefix("::"))
30
-
31
- break if name.start_with?("::")
32
- end
33
-
34
- corrected_nesting
35
- end
36
-
37
- # Returns the unresolved name for a constant reference including all parts of a constant path, or `nil` if the
38
- # constant contains dynamic or incomplete parts
39
- #: (Prism::Node) -> String?
40
- def constant_name(node)
41
- case node
42
- when Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathTargetNode
43
- node.full_name
44
- end
45
- rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
46
- Prism::ConstantPathNode::MissingNodesInConstantPathError
47
- nil
48
- end
49
- end
50
-
51
- #: -> void
52
- def initialize
53
- # Holds all entries in the index using the following format:
54
- # {
55
- # "Foo" => [#<Entry::Class>, #<Entry::Class>],
56
- # "Foo::Bar" => [#<Entry::Class>],
57
- # }
58
- @entries = {} #: Hash[String, Array[Entry]]
59
-
60
- # Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion
61
- @entries_tree = PrefixTree.new #: PrefixTree[Array[Entry]]
62
-
63
- # Holds references to where entries where discovered so that we can easily delete them
64
- # {
65
- # "file:///my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
66
- # "file:///my/project/bar.rb" => [#<Entry::Class>],
67
- # "untitled:Untitled-1" => [#<Entry::Class>],
68
- # }
69
- @uris_to_entries = {} #: Hash[String, Array[Entry]]
70
-
71
- # Holds all require paths for every indexed item so that we can provide autocomplete for requires
72
- @require_paths_tree = PrefixTree.new #: PrefixTree[URI::Generic]
73
-
74
- # Holds the linearized ancestors list for every namespace
75
- @ancestors = {} #: Hash[String, Array[String]]
76
-
77
- # Map of module name to included hooks that have to be executed when we include the given module
78
- @included_hooks = {} #: Hash[String, Array[^(Index index, Entry::Namespace base) -> void]]
79
-
80
- @configuration = RubyIndexer::Configuration.new #: Configuration
81
-
82
- @initial_indexing_completed = false #: bool
83
- end
84
-
85
- # Register an included `hook` that will be executed when `module_name` is included into any namespace
86
- #: (String module_name) { (Index index, Entry::Namespace base) -> void } -> void
87
- def register_included_hook(module_name, &hook)
88
- (@included_hooks[module_name] ||= []) << hook
89
- end
90
-
91
- #: (URI::Generic uri, ?skip_require_paths_tree: bool) -> void
92
- def delete(uri, skip_require_paths_tree: false)
93
- key = uri.to_s
94
- # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
95
- # left, delete the constant from the index.
96
- @uris_to_entries[key]&.each do |entry|
97
- name = entry.name
98
- entries = @entries[name]
99
- next unless entries
100
-
101
- # Delete the specific entry from the list for this name
102
- entries.delete(entry)
103
-
104
- # If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update
105
- # the prefix tree with the current entries
106
- if entries.empty?
107
- @entries.delete(name)
108
- @entries_tree.delete(name)
109
- else
110
- @entries_tree.insert(name, entries)
111
- end
112
- end
113
-
114
- @uris_to_entries.delete(key)
115
- return if skip_require_paths_tree
116
-
117
- require_path = uri.require_path
118
- @require_paths_tree.delete(require_path) if require_path
119
- end
120
-
121
- #: (Entry entry, ?skip_prefix_tree: bool) -> void
122
- def add(entry, skip_prefix_tree: false)
123
- name = entry.name
124
-
125
- (@entries[name] ||= []) << entry
126
- (@uris_to_entries[entry.uri.to_s] ||= []) << entry
127
-
128
- unless skip_prefix_tree
129
- @entries_tree.insert(
130
- name,
131
- @entries[name], #: as !nil
132
- )
133
- end
134
- end
135
-
136
- #: (String fully_qualified_name) -> Array[Entry]?
137
- def [](fully_qualified_name)
138
- @entries[fully_qualified_name.delete_prefix("::")]
139
- end
140
-
141
- #: (String query) -> Array[URI::Generic]
142
- def search_require_paths(query)
143
- @require_paths_tree.search(query)
144
- end
145
-
146
- # Searches for a constant based on an unqualified name and returns the first possible match regardless of whether
147
- # there are more possible matching entries
148
- #: (String name) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
149
- def first_unqualified_const(name)
150
- # Look for an exact match first
151
- _name, entries = @entries.find do |const_name, _entries|
152
- const_name == name || const_name.end_with?("::#{name}")
153
- end
154
-
155
- # If an exact match is not found, then try to find a constant that ends with the name
156
- unless entries
157
- _name, entries = @entries.find do |const_name, _entries|
158
- const_name.end_with?(name)
159
- end
160
- end
161
-
162
- entries #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
163
- end
164
-
165
- # Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches
166
- # to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given
167
- # name match. For example:
168
- # ## Example
169
- # ```ruby
170
- # # If the index has two entries for `Foo::Bar` and one for `Foo::Baz`, then:
171
- # index.prefix_search("Foo::B")
172
- # # Will return:
173
- # [
174
- # [#<Entry::Class name="Foo::Bar">, #<Entry::Class name="Foo::Bar">],
175
- # [#<Entry::Class name="Foo::Baz">],
176
- # ]
177
- # ```
178
- #: (String query, ?Array[String]? nesting) -> Array[Array[Entry]]
179
- def prefix_search(query, nesting = nil)
180
- unless nesting
181
- results = @entries_tree.search(query)
182
- results.uniq!
183
- return results
184
- end
185
-
186
- results = nesting.length.downto(0).flat_map do |i|
187
- prefix = nesting[0...i] #: as !nil
188
- .join("::")
189
- namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
190
- @entries_tree.search(namespaced_query)
191
- end
192
-
193
- results.uniq!
194
- results
195
- end
196
-
197
- # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
198
- #: (String? query) ?{ (Entry) -> bool? } -> Array[Entry]
199
- def fuzzy_search(query, &condition)
200
- unless query
201
- entries = @entries.filter_map do |_name, entries|
202
- next if entries.first.is_a?(Entry::SingletonClass)
203
-
204
- entries = entries.select(&condition) if condition
205
- entries
206
- end
207
-
208
- return entries.flatten
209
- end
210
-
211
- normalized_query = query.gsub("::", "").downcase
212
-
213
- results = @entries.filter_map do |name, entries|
214
- next if entries.first.is_a?(Entry::SingletonClass)
215
-
216
- entries = entries.select(&condition) if condition
217
- next if entries.empty?
218
-
219
- similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
220
- [entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
221
- end
222
- results.sort_by!(&:last)
223
- results.flat_map(&:first)
224
- end
225
-
226
- #: (String? name, String receiver_name) -> Array[(Entry::Member | Entry::MethodAlias)]
227
- def method_completion_candidates(name, receiver_name)
228
- ancestors = linearized_ancestors_of(receiver_name)
229
-
230
- candidates = name ? prefix_search(name).flatten : @entries.values.flatten
231
- completion_items = candidates.each_with_object({}) do |entry, hash|
232
- unless entry.is_a?(Entry::Member) || entry.is_a?(Entry::MethodAlias) ||
233
- entry.is_a?(Entry::UnresolvedMethodAlias)
234
- next
235
- end
236
-
237
- entry_name = entry.name
238
- ancestor_index = ancestors.index(entry.owner&.name)
239
- existing_entry, existing_entry_index = hash[entry_name]
240
-
241
- # Conditions for matching a method completion candidate:
242
- # 1. If an ancestor_index was found, it means that this method is owned by the receiver. The exact index is
243
- # where in the ancestor chain the method was found. For example, if the ancestors are ["A", "B", "C"] and we
244
- # found the method declared in `B`, then the ancestors index is 1
245
- #
246
- # 2. We already established that this method is owned by the receiver. Now, check if we already added a
247
- # completion candidate for this method name. If not, then we just go and add it (the left hand side of the or)
248
- #
249
- # 3. If we had already found a method entry for the same name, then we need to check if the current entry that
250
- # we are comparing appears first in the hierarchy or not. For example, imagine we have the method `open` defined
251
- # in both `File` and its parent `IO`. If we first find the method `open` in `IO`, it will be inserted into the
252
- # hash. Then, when we find the entry for `open` owned by `File`, we need to replace `IO.open` by `File.open`,
253
- # since `File.open` appears first in the hierarchy chain and is therefore the correct method being invoked. The
254
- # last part of the conditional checks if the current entry was found earlier in the hierarchy chain, in which
255
- # case we must update the existing entry to avoid showing the wrong method declaration for overridden methods
256
- next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
257
-
258
- if entry.is_a?(Entry::UnresolvedMethodAlias)
259
- resolved_alias = resolve_method_alias(entry, receiver_name, [])
260
- hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
261
- else
262
- hash[entry_name] = [entry, ancestor_index]
263
- end
264
- end
265
-
266
- completion_items.values.map!(&:first)
267
- end
268
-
269
- #: (String name, Array[String] nesting) -> Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
270
- def constant_completion_candidates(name, nesting)
271
- # If we have a top level reference, then we don't need to include completions inside the current nesting
272
- if name.start_with?("::")
273
- return @entries_tree.search(name.delete_prefix("::")) #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
274
- end
275
-
276
- # Otherwise, we have to include every possible constant the user might be referring to. This is essentially the
277
- # same algorithm as resolve, but instead of returning early we concatenate all unique results
278
-
279
- # Direct constants inside this namespace
280
- entries = @entries_tree.search(nesting.any? ? "#{nesting.join("::")}::#{name}" : name)
281
-
282
- # Constants defined in enclosing scopes
283
- nesting.length.downto(1) do |i|
284
- namespace = nesting[0...i] #: as !nil
285
- .join("::")
286
- entries.concat(@entries_tree.search("#{namespace}::#{name}"))
287
- end
288
-
289
- # Inherited constants
290
- if name.end_with?("::")
291
- entries.concat(inherited_constant_completion_candidates(nil, nesting + [name]))
292
- else
293
- entries.concat(inherited_constant_completion_candidates(name, nesting))
294
- end
295
-
296
- # Top level constants
297
- entries.concat(@entries_tree.search(name))
298
-
299
- # Filter only constants since methods may have names that look like constants
300
- entries.select! do |definitions|
301
- definitions.select! do |entry|
302
- entry.is_a?(Entry::Constant) || entry.is_a?(Entry::ConstantAlias) ||
303
- entry.is_a?(Entry::Namespace) || entry.is_a?(Entry::UnresolvedConstantAlias)
304
- end
305
-
306
- definitions.any?
307
- end
308
-
309
- entries.uniq!
310
- entries #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
311
- end
312
-
313
- # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
314
- # documentation:
315
- #
316
- # name: the name of the reference how it was found in the source code (qualified or not)
317
- # nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"])
318
- # seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when
319
- # resolving circular references
320
- #: (String name, Array[String] nesting, ?Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
321
- def resolve(name, nesting, seen_names = [])
322
- # If we have a top level reference, then we just search for it straight away ignoring the nesting
323
- if name.start_with?("::")
324
- entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names)
325
- return entries if entries
326
- end
327
-
328
- # Non qualified reference path
329
- full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name
330
-
331
- # When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the
332
- # constant. First, it will try to find the constant in the exact namespace where the reference was found
333
- entries = direct_or_aliased_constant(full_name, seen_names)
334
- return entries if entries
335
-
336
- # If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes,
337
- # unwrapping each level one by one. Important note: the top level is not included because that's the fallback of
338
- # the algorithm after every other possibility has been exhausted
339
- entries = lookup_enclosing_scopes(name, nesting, seen_names)
340
- return entries if entries
341
-
342
- # If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the
343
- # specific namespace where the reference was found
344
- entries = lookup_ancestor_chain(name, nesting, seen_names)
345
- return entries if entries
346
-
347
- # Finally, as a fallback, Ruby will search for the constant in the top level namespace
348
- direct_or_aliased_constant(name, seen_names)
349
- rescue UnresolvableAliasError
350
- nil
351
- end
352
-
353
- # Index all files for the given URIs, which defaults to what is configured. A block can be used to track and control
354
- # indexing progress. That block is invoked with the current progress percentage and should return `true` to continue
355
- # indexing or `false` to stop indexing.
356
- #: (?uris: Array[URI::Generic]) ?{ (Integer progress) -> bool } -> void
357
- def index_all(uris: @configuration.indexable_uris, &block)
358
- # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the
359
- # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this
360
- # behavior and can take appropriate action.
361
- if @initial_indexing_completed
362
- raise IndexNotEmptyError,
363
- "The index is not empty. To prevent invalid entries, `index_all` can only be called once."
364
- end
365
-
366
- RBSIndexer.new(self).index_ruby_core
367
- # Calculate how many paths are worth 1% of progress
368
- progress_step = (uris.length / 100.0).ceil
369
-
370
- uris.each_with_index do |uri, index|
371
- if block && index % progress_step == 0
372
- progress = (index / progress_step) + 1
373
- break unless block.call(progress)
374
- end
375
-
376
- index_file(uri, collect_comments: false)
377
- end
378
-
379
- @initial_indexing_completed = true
380
- end
381
-
382
- #: (URI::Generic uri, String source, ?collect_comments: bool) -> void
383
- def index_single(uri, source, collect_comments: true)
384
- dispatcher = Prism::Dispatcher.new
385
-
386
- result = Prism.parse(source)
387
- listener = DeclarationListener.new(self, dispatcher, result, uri, collect_comments: collect_comments)
388
- dispatcher.dispatch(result.value)
389
-
390
- require_path = uri.require_path
391
- @require_paths_tree.insert(require_path, uri) if require_path
392
-
393
- indexing_errors = listener.indexing_errors.uniq
394
- indexing_errors.each { |error| $stderr.puts(error) } if indexing_errors.any?
395
- rescue SystemStackError => e
396
- if e.backtrace&.first&.include?("prism")
397
- $stderr.puts "Prism error indexing #{uri}: #{e.message}"
398
- else
399
- raise
400
- end
401
- end
402
-
403
- # Indexes a File URI by reading the contents from disk
404
- #: (URI::Generic uri, ?collect_comments: bool) -> void
405
- def index_file(uri, collect_comments: true)
406
- path = uri.full_path #: as !nil
407
- index_single(uri, File.read(path), collect_comments: collect_comments)
408
- rescue Errno::EISDIR, Errno::ENOENT
409
- # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
410
- # it
411
- end
412
-
413
- # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
414
- # it. The idea is that we test the name in parts starting from the complete name to the first namespace. For
415
- # `Foo::Bar::Baz`, we would test:
416
- # 1. Is `Foo::Bar::Baz` an alias? Get the target and recursively follow its target
417
- # 2. Is `Foo::Bar` an alias? Get the target and recursively follow its target
418
- # 3. Is `Foo` an alias? Get the target and recursively follow its target
419
- #
420
- # If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
421
- # `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
422
- # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
423
- #: (String name, ?Array[String] seen_names) -> String
424
- def follow_aliased_namespace(name, seen_names = [])
425
- parts = name.split("::")
426
- real_parts = []
427
-
428
- (parts.length - 1).downto(0) do |i|
429
- current_name = parts[0..i] #: as !nil
430
- .join("::")
431
-
432
- entry = unless seen_names.include?(current_name)
433
- @entries[current_name]&.first
434
- end
435
-
436
- case entry
437
- when Entry::ConstantAlias
438
- target = entry.target
439
- return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
440
- when Entry::UnresolvedConstantAlias
441
- resolved = resolve_alias(entry, seen_names)
442
-
443
- if resolved.is_a?(Entry::UnresolvedConstantAlias)
444
- raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
445
- end
446
-
447
- target = resolved.target
448
- return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
449
- else
450
- real_parts.unshift(
451
- parts[i], #: as !nil
452
- )
453
- end
454
- end
455
-
456
- real_parts.join("::")
457
- end
458
-
459
- # Attempts to find methods for a resolved fully qualified receiver name. Do not provide the `seen_names` parameter
460
- # as it is used only internally to prevent infinite loops when resolving circular aliases
461
- # Returns `nil` if the method does not exist on that receiver
462
- #: (String method_name, String receiver_name, ?Array[String] seen_names, ?inherited_only: bool) -> Array[(Entry::Member | Entry::MethodAlias)]?
463
- def resolve_method(method_name, receiver_name, seen_names = [], inherited_only: false)
464
- method_entries = self[method_name]
465
- return unless method_entries
466
-
467
- ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
468
- ancestors.each do |ancestor|
469
- next if inherited_only && ancestor == receiver_name
470
-
471
- found = method_entries.filter_map do |entry|
472
- case entry
473
- when Entry::Member, Entry::MethodAlias
474
- entry if entry.owner&.name == ancestor
475
- when Entry::UnresolvedMethodAlias
476
- # Resolve aliases lazily as we find them
477
- if entry.owner&.name == ancestor
478
- resolved_alias = resolve_method_alias(entry, receiver_name, seen_names)
479
- resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
480
- end
481
- end
482
- end
483
-
484
- return found if found.any?
485
- end
486
-
487
- nil
488
- rescue NonExistingNamespaceError
489
- nil
490
- end
491
-
492
- # Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
493
- # or constant declarations.
494
- #
495
- # When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
496
- # everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
497
- # module that prepends another module, then the prepend module appears before the included module.
498
- #
499
- # The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
500
- #: (String fully_qualified_name) -> Array[String]
501
- def linearized_ancestors_of(fully_qualified_name)
502
- # If we already computed the ancestors for this namespace, return it straight away
503
- cached_ancestors = @ancestors[fully_qualified_name]
504
- return cached_ancestors if cached_ancestors
505
-
506
- parts = fully_qualified_name.split("::")
507
- singleton_levels = 0
508
-
509
- parts.reverse_each do |part|
510
- break unless part.start_with?("<")
511
-
512
- singleton_levels += 1
513
- parts.pop
514
- end
515
-
516
- attached_class_name = parts.join("::")
517
-
518
- # If we don't have an entry for `name`, raise
519
- entries = self[fully_qualified_name]
520
-
521
- if singleton_levels > 0 && !entries && indexed?(attached_class_name)
522
- entries = [existing_or_new_singleton_class(attached_class_name)]
523
- end
524
-
525
- raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
526
-
527
- ancestors = [fully_qualified_name]
528
-
529
- # Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
530
- # this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
531
- # the cache will reflect the final result
532
- @ancestors[fully_qualified_name] = ancestors
533
-
534
- # If none of the entries for `name` are namespaces, raise
535
- namespaces = entries.filter_map do |entry|
536
- case entry
537
- when Entry::Namespace
538
- entry
539
- when Entry::ConstantAlias
540
- self[entry.target]&.grep(Entry::Namespace)
541
- end
542
- end.flatten
543
-
544
- raise NonExistingNamespaceError,
545
- "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
546
-
547
- # The original nesting where we discovered this namespace, so that we resolve the correct names of the
548
- # included/prepended/extended modules and parent classes
549
- nesting = namespaces.first #: as !nil
550
- .nesting.flat_map { |n| n.split("::") }
551
-
552
- if nesting.any?
553
- singleton_levels.times do
554
- nesting << "<#{nesting.last}>"
555
- end
556
- end
557
-
558
- # We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
559
- # new singleton methods or to extend a module through an include. There's no need to support instance methods, the
560
- # inclusion of another module or the prepending of another module, because those features are already a part of
561
- # Ruby and can be used directly without any metaprogramming
562
- run_included_hooks(attached_class_name, nesting) if singleton_levels > 0
563
-
564
- linearize_mixins(ancestors, namespaces, nesting)
565
- linearize_superclass(
566
- ancestors,
567
- attached_class_name,
568
- fully_qualified_name,
569
- namespaces,
570
- nesting,
571
- singleton_levels,
572
- )
573
-
574
- ancestors
575
- end
576
-
577
- # Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
578
- # and find inherited instance variables as well
579
- #: (String variable_name, String owner_name) -> Array[Entry::InstanceVariable]?
580
- def resolve_instance_variable(variable_name, owner_name)
581
- entries = self[variable_name] #: as Array[Entry::InstanceVariable]?
582
- return unless entries
583
-
584
- ancestors = linearized_ancestors_of(owner_name)
585
- return if ancestors.empty?
586
-
587
- entries.select { |e| ancestors.include?(e.owner&.name) }
588
- end
589
-
590
- #: (String variable_name, String owner_name) -> Array[Entry::ClassVariable]?
591
- def resolve_class_variable(variable_name, owner_name)
592
- entries = self[variable_name]&.grep(Entry::ClassVariable)
593
- return unless entries&.any?
594
-
595
- ancestors = linearized_attached_ancestors(owner_name)
596
- return if ancestors.empty?
597
-
598
- entries.select { |e| ancestors.include?(e.owner&.name) }
599
- end
600
-
601
- # Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
602
- # include the `@` prefix
603
- #: (String name, String owner_name) -> Array[(Entry::InstanceVariable | Entry::ClassVariable)]
604
- def instance_variable_completion_candidates(name, owner_name)
605
- entries = prefix_search(name).flatten #: as Array[Entry::InstanceVariable | Entry::ClassVariable]
606
- # Avoid wasting time linearizing ancestors if we didn't find anything
607
- return entries if entries.empty?
608
-
609
- ancestors = linearized_ancestors_of(owner_name)
610
-
611
- instance_variables, class_variables = entries.partition { |e| e.is_a?(Entry::InstanceVariable) }
612
- variables = instance_variables.select { |e| ancestors.any?(e.owner&.name) }
613
-
614
- # Class variables are only owned by the attached class in our representation. If the owner is in a singleton
615
- # context, we have to search for ancestors of the attached class
616
- if class_variables.any?
617
- name_parts = owner_name.split("::")
618
-
619
- if name_parts.last&.start_with?("<")
620
- attached_name = name_parts[0..-2] #: as !nil
621
- .join("::")
622
- attached_ancestors = linearized_ancestors_of(attached_name)
623
- variables.concat(class_variables.select { |e| attached_ancestors.any?(e.owner&.name) })
624
- else
625
- variables.concat(class_variables.select { |e| ancestors.any?(e.owner&.name) })
626
- end
627
- end
628
-
629
- variables.uniq!(&:name)
630
- variables
631
- end
632
-
633
- #: (String name, String owner_name) -> Array[Entry::ClassVariable]
634
- def class_variable_completion_candidates(name, owner_name)
635
- entries = prefix_search(name).flatten #: as Array[Entry::ClassVariable]
636
- # Avoid wasting time linearizing ancestors if we didn't find anything
637
- return entries if entries.empty?
638
-
639
- ancestors = linearized_attached_ancestors(owner_name)
640
- variables = entries.select { |e| ancestors.any?(e.owner&.name) }
641
- variables.uniq!(&:name)
642
- variables
643
- end
644
-
645
- # Synchronizes a change made to the given URI. This method will ensure that new declarations are indexed, removed
646
- # declarations removed and that the ancestor linearization cache is cleared if necessary. If a block is passed, the
647
- # consumer of this API has to handle deleting and inserting/updating entries in the index instead of passing the
648
- # document's source (used to handle unsaved changes to files)
649
- #: (URI::Generic uri, ?String? source) ?{ (Index index) -> void } -> void
650
- def handle_change(uri, source = nil, &block)
651
- key = uri.to_s
652
- original_entries = @uris_to_entries[key]
653
-
654
- if block
655
- block.call(self)
656
- else
657
- delete(uri)
658
- index_single(
659
- uri,
660
- source, #: as !nil
661
- )
662
- end
663
-
664
- updated_entries = @uris_to_entries[key]
665
- return unless original_entries && updated_entries
666
-
667
- # A change in one ancestor may impact several different others, which could be including that ancestor through
668
- # indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
669
- # ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
670
- # ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
671
- original_map = original_entries
672
- .select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace]
673
- .to_h { |e| [e.name, e.ancestor_hash] }
674
-
675
- updated_map = updated_entries
676
- .select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace]
677
- .to_h { |e| [e.name, e.ancestor_hash] }
678
-
679
- @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
680
- end
681
-
682
- #: -> void
683
- def clear_ancestors
684
- @ancestors.clear
685
- end
686
-
687
- #: -> bool
688
- def empty?
689
- @entries.empty?
690
- end
691
-
692
- #: -> Array[String]
693
- def names
694
- @entries.keys
695
- end
696
-
697
- #: (String name) -> bool
698
- def indexed?(name)
699
- @entries.key?(name)
700
- end
701
-
702
- #: -> Integer
703
- def length
704
- @entries.count
705
- end
706
-
707
- #: (String name) -> Entry::SingletonClass
708
- def existing_or_new_singleton_class(name)
709
- *_namespace, unqualified_name = name.split("::")
710
- full_singleton_name = "#{name}::<#{unqualified_name}>"
711
- singleton = self[full_singleton_name]&.first #: as Entry::SingletonClass?
712
-
713
- unless singleton
714
- attached_ancestor = self[name]&.first #: as !nil
715
-
716
- singleton = Entry::SingletonClass.new(
717
- @configuration,
718
- [full_singleton_name],
719
- attached_ancestor.uri,
720
- attached_ancestor.location,
721
- attached_ancestor.name_location,
722
- nil,
723
- nil,
724
- )
725
- add(singleton, skip_prefix_tree: true)
726
- end
727
-
728
- singleton
729
- end
730
-
731
- #: [T] (String uri, ?Class[(T & Entry)]? type) -> (Array[Entry] | Array[T])?
732
- def entries_for(uri, type = nil)
733
- entries = @uris_to_entries[uri.to_s]
734
- return entries unless type
735
-
736
- entries&.grep(type)
737
- end
738
-
739
- private
740
-
741
- # Always returns the linearized ancestors for the attached class, regardless of whether `name` refers to a singleton
742
- # or attached namespace
743
- #: (String name) -> Array[String]
744
- def linearized_attached_ancestors(name)
745
- name_parts = name.split("::")
746
-
747
- if name_parts.last&.start_with?("<")
748
- attached_name = name_parts[0..-2] #: as !nil
749
- .join("::")
750
- linearized_ancestors_of(attached_name)
751
- else
752
- linearized_ancestors_of(name)
753
- end
754
- end
755
-
756
- # Runs the registered included hooks
757
- #: (String fully_qualified_name, Array[String] nesting) -> void
758
- def run_included_hooks(fully_qualified_name, nesting)
759
- return if @included_hooks.empty?
760
-
761
- namespaces = self[fully_qualified_name]&.grep(Entry::Namespace)
762
- return unless namespaces
763
-
764
- namespaces.each do |namespace|
765
- namespace.mixin_operations.each do |operation|
766
- next unless operation.is_a?(Entry::Include)
767
-
768
- # First we resolve the include name, so that we know the actual module being referred to in the include
769
- resolved_modules = resolve(operation.module_name, nesting)
770
- next unless resolved_modules
771
-
772
- module_name = resolved_modules.first #: as !nil
773
- .name
774
-
775
- # Then we grab any hooks registered for that module
776
- hooks = @included_hooks[module_name]
777
- next unless hooks
778
-
779
- # We invoke the hooks with the index and the namespace that included the module
780
- hooks.each { |hook| hook.call(self, namespace) }
781
- end
782
- end
783
- end
784
-
785
- # Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
786
- # linearized ancestors of the mixins
787
- #: (Array[String] ancestors, Array[Entry::Namespace] namespace_entries, Array[String] nesting) -> void
788
- def linearize_mixins(ancestors, namespace_entries, nesting)
789
- mixin_operations = namespace_entries.flat_map(&:mixin_operations)
790
- main_namespace_index = 0
791
-
792
- mixin_operations.each do |operation|
793
- resolved_module = resolve(operation.module_name, nesting)
794
- next unless resolved_module
795
-
796
- module_fully_qualified_name = resolved_module.first #: as !nil
797
- .name
798
-
799
- case operation
800
- when Entry::Prepend
801
- # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
802
- # the actual namespace twice. However, it does not check if it has been included because you are allowed to
803
- # prepend the same module after it has already been included
804
- linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
805
-
806
- # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
807
- # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
808
- # be inserted after `"A`
809
- prepended_ancestors = ancestors[0...main_namespace_index] #: as !nil
810
- uniq_prepends = linearized_prepends - prepended_ancestors
811
- insert_position = linearized_prepends.length - uniq_prepends.length
812
-
813
- ancestors #: as untyped
814
- .insert(insert_position, *uniq_prepends)
815
-
816
- main_namespace_index += linearized_prepends.length
817
- when Entry::Include
818
- # When including a module, Ruby will always prevent duplicate entries in case the module has already been
819
- # prepended or included
820
- linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
821
- ancestors #: as untyped
822
- .insert(main_namespace_index + 1, *(linearized_includes - ancestors))
823
- end
824
- end
825
- end
826
-
827
- # Linearize the superclass of a given namespace (including modules with the implicit `Module` superclass). This
828
- # method will mutate the `ancestors` array with the linearized ancestors of the superclass
829
- #: (Array[String] ancestors, String attached_class_name, String fully_qualified_name, Array[Entry::Namespace] namespace_entries, Array[String] nesting, Integer singleton_levels) -> void
830
- def linearize_superclass( # rubocop:disable Metrics/ParameterLists
831
- ancestors,
832
- attached_class_name,
833
- fully_qualified_name,
834
- namespace_entries,
835
- nesting,
836
- singleton_levels
837
- )
838
- # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
839
- # from two different classes in different files, we simply ignore it
840
- possible_parents = singleton_levels > 0 ? self[attached_class_name] : namespace_entries
841
- superclass = nil #: Entry::Class?
842
-
843
- possible_parents&.each do |n|
844
- # Ignore non class entries
845
- next unless n.is_a?(Entry::Class)
846
-
847
- parent_class = n.parent_class
848
- next unless parent_class
849
-
850
- # Always set the superclass, but break early if we found one that isn't `::Object` (meaning we found an explicit
851
- # parent class and not the implicit default). Note that when setting different parents to the same class, which
852
- # is invalid, we pick whatever is the first one we find
853
- superclass = n
854
- break if parent_class != "::Object"
855
- end
856
-
857
- if superclass
858
- # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
859
- # error. We need to ensure that this isn't the case
860
- parent_class = superclass.parent_class #: as !nil
861
-
862
- resolved_parent_class = resolve(parent_class, nesting)
863
- parent_class_name = resolved_parent_class&.first&.name
864
-
865
- if parent_class_name && fully_qualified_name != parent_class_name
866
-
867
- parent_name_parts = parent_class_name.split("::")
868
- singleton_levels.times do
869
- parent_name_parts << "<#{parent_name_parts.last}>"
870
- end
871
-
872
- ancestors.concat(linearized_ancestors_of(parent_name_parts.join("::")))
873
- end
874
-
875
- # When computing the linearization for a class's singleton class, it inherits from the linearized ancestors of
876
- # the `Class` class
877
- if parent_class_name&.start_with?("BasicObject") && singleton_levels > 0
878
- class_class_name_parts = ["Class"]
879
-
880
- (singleton_levels - 1).times do
881
- class_class_name_parts << "<#{class_class_name_parts.last}>"
882
- end
883
-
884
- ancestors.concat(linearized_ancestors_of(class_class_name_parts.join("::")))
885
- end
886
- elsif singleton_levels > 0
887
- # When computing the linearization for a module's singleton class, it inherits from the linearized ancestors of
888
- # the `Module` class
889
- mod = self[attached_class_name]&.find { |n| n.is_a?(Entry::Module) } #: as Entry::Module?
890
-
891
- if mod
892
- module_class_name_parts = ["Module"]
893
-
894
- (singleton_levels - 1).times do
895
- module_class_name_parts << "<#{module_class_name_parts.last}>"
896
- end
897
-
898
- ancestors.concat(linearized_ancestors_of(module_class_name_parts.join("::")))
899
- end
900
- end
901
- end
902
-
903
- # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
904
- # that doesn't exist, then we return the same UnresolvedAlias
905
- #: (Entry::UnresolvedConstantAlias entry, Array[String] seen_names) -> (Entry::ConstantAlias | Entry::UnresolvedConstantAlias)
906
- def resolve_alias(entry, seen_names)
907
- alias_name = entry.name
908
- return entry if seen_names.include?(alias_name)
909
-
910
- seen_names << alias_name
911
-
912
- target = resolve(entry.target, entry.nesting, seen_names)
913
- return entry unless target
914
-
915
- # Self referential alias can be unresolved we should bail out from resolving
916
- return entry if target.first == entry
917
-
918
- target_name = target.first #: as !nil
919
- .name
920
- resolved_alias = Entry::ConstantAlias.new(target_name, entry)
921
-
922
- # Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
923
- original_entries = @entries[alias_name] #: as !nil
924
- original_entries.delete(entry)
925
- original_entries << resolved_alias
926
-
927
- @entries_tree.insert(alias_name, original_entries)
928
-
929
- resolved_alias
930
- end
931
-
932
- #: (String name, Array[String] nesting, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
933
- def lookup_enclosing_scopes(name, nesting, seen_names)
934
- nesting.length.downto(1) do |i|
935
- namespace = nesting[0...i] #: as !nil
936
- .join("::")
937
-
938
- # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
939
- # because the user might be trying to jump to the alias definition.
940
- #
941
- # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
942
- # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
943
- # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
944
- # `RubyLsp::Interface` part is an alias, that has to be resolved
945
- entries = direct_or_aliased_constant("#{namespace}::#{name}", seen_names)
946
- return entries if entries
947
- end
948
-
949
- nil
950
- end
951
-
952
- #: (String name, Array[String] nesting, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
953
- def lookup_ancestor_chain(name, nesting, seen_names)
954
- *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
955
- return if nesting_parts.empty?
956
-
957
- namespace_entries = resolve(nesting_parts.join("::"), [], seen_names)
958
- return unless namespace_entries
959
-
960
- namespace_name = namespace_entries.first #: as !nil
961
- .name
962
- ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(namespace_name)
963
-
964
- ancestors.each do |ancestor_name|
965
- entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
966
- return entries if entries
967
- end
968
-
969
- nil
970
- rescue NonExistingNamespaceError
971
- nil
972
- end
973
-
974
- #: (String? name, Array[String] nesting) -> Array[Array[(Entry::Namespace | Entry::ConstantAlias | Entry::UnresolvedConstantAlias | Entry::Constant)]]
975
- def inherited_constant_completion_candidates(name, nesting)
976
- namespace_entries = if name
977
- *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
978
- return [] if nesting_parts.empty?
979
-
980
- resolve(nesting_parts.join("::"), [])
981
- else
982
- resolve(nesting.join("::"), [])
983
- end
984
- return [] unless namespace_entries
985
-
986
- namespace_name = namespace_entries.first #: as !nil
987
- .name
988
- ancestors = linearized_ancestors_of(namespace_name)
989
- candidates = ancestors.flat_map do |ancestor_name|
990
- @entries_tree.search("#{ancestor_name}::#{constant_name}")
991
- end
992
-
993
- # For candidates with the same name, we must only show the first entry in the inheritance chain, since that's the
994
- # one the user will be referring to in completion
995
- completion_items = candidates.each_with_object({}) do |entries, hash|
996
- *parts, short_name = entries.first #: as !nil
997
- .name.split("::")
998
- namespace_name = parts.join("::")
999
- ancestor_index = ancestors.index(namespace_name)
1000
- existing_entry, existing_entry_index = hash[short_name]
1001
-
1002
- next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
1003
-
1004
- hash[short_name] = [entries, ancestor_index]
1005
- end
1006
-
1007
- completion_items.values.map!(&:first)
1008
- rescue NonExistingNamespaceError
1009
- []
1010
- end
1011
-
1012
- # Removes redundancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo`
1013
- # inside of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up
1014
- # with `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and
1015
- # the nesting
1016
- #: (String name, Array[String] nesting) -> String
1017
- def build_non_redundant_full_name(name, nesting)
1018
- # If there's no nesting, then we can just return the name as is
1019
- return name if nesting.empty?
1020
-
1021
- # If the name is not qualified, we can just concatenate the nesting and the name
1022
- return "#{nesting.join("::")}::#{name}" unless name.include?("::")
1023
-
1024
- name_parts = name.split("::")
1025
- first_redundant_part = nesting.index(name_parts[0])
1026
-
1027
- # If there are no redundant parts between the name and the nesting, then the full name is both combined
1028
- return "#{nesting.join("::")}::#{name}" unless first_redundant_part
1029
-
1030
- # Otherwise, push all of the leading parts of the nesting that aren't redundant into the name. For example, if we
1031
- # have a reference to `Foo::Bar` inside the `[Namespace, Foo]` nesting, then only the `Foo` part is redundant, but
1032
- # we still need to include the `Namespace` part
1033
- name_parts.unshift(*nesting[0...first_redundant_part])
1034
- name_parts.join("::")
1035
- end
1036
-
1037
- # Tries to return direct entry from index then non seen canonicalized alias or nil
1038
- #: (String full_name, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
1039
- def direct_or_aliased_constant(full_name, seen_names)
1040
- if (entries = @entries[full_name])
1041
- return entries.map do |e|
1042
- e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
1043
- end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
1044
- end
1045
-
1046
- aliased = follow_aliased_namespace(full_name, seen_names)
1047
- return if full_name == aliased || seen_names.include?(aliased)
1048
-
1049
- @entries[aliased]&.map do |e|
1050
- e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
1051
- end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
1052
- end
1053
-
1054
- # Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to
1055
- # identify the target or the same unresolved alias entry if we couldn't
1056
- #: (Entry::UnresolvedMethodAlias entry, String receiver_name, Array[String] seen_names) -> (Entry::MethodAlias | Entry::UnresolvedMethodAlias)
1057
- def resolve_method_alias(entry, receiver_name, seen_names)
1058
- new_name = entry.new_name
1059
- return entry if new_name == entry.old_name
1060
- return entry if seen_names.include?(new_name)
1061
-
1062
- seen_names << new_name
1063
-
1064
- target_method_entries = resolve_method(entry.old_name, receiver_name, seen_names)
1065
- return entry unless target_method_entries
1066
-
1067
- resolved_alias = Entry::MethodAlias.new(
1068
- target_method_entries.first, #: as !nil
1069
- entry,
1070
- )
1071
- original_entries = @entries[new_name] #: as !nil
1072
- original_entries.delete(entry)
1073
- original_entries << resolved_alias
1074
- resolved_alias
1075
- end
1076
- end
1077
- end