ruby-lsp 0.17.2 → 0.17.4

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +280 -74
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +102 -102
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +234 -56
  7. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +147 -0
  8. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +49 -2
  10. data/lib/ruby_indexer/test/configuration_test.rb +1 -1
  11. data/lib/ruby_indexer/test/constant_test.rb +1 -1
  12. data/lib/ruby_indexer/test/index_test.rb +702 -71
  13. data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
  14. data/lib/ruby_indexer/test/method_test.rb +74 -24
  15. data/lib/ruby_indexer/test/rbs_indexer_test.rb +67 -0
  16. data/lib/ruby_indexer/test/test_case.rb +7 -0
  17. data/lib/ruby_lsp/document.rb +37 -8
  18. data/lib/ruby_lsp/global_state.rb +43 -18
  19. data/lib/ruby_lsp/internal.rb +2 -0
  20. data/lib/ruby_lsp/listeners/code_lens.rb +2 -2
  21. data/lib/ruby_lsp/listeners/completion.rb +53 -14
  22. data/lib/ruby_lsp/listeners/definition.rb +11 -7
  23. data/lib/ruby_lsp/listeners/hover.rb +14 -7
  24. data/lib/ruby_lsp/listeners/signature_help.rb +5 -2
  25. data/lib/ruby_lsp/node_context.rb +6 -1
  26. data/lib/ruby_lsp/requests/completion.rb +5 -4
  27. data/lib/ruby_lsp/requests/completion_resolve.rb +8 -0
  28. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +88 -0
  29. data/lib/ruby_lsp/requests/support/common.rb +19 -1
  30. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +12 -4
  31. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +91 -0
  32. data/lib/ruby_lsp/requests/workspace_symbol.rb +1 -21
  33. data/lib/ruby_lsp/requests.rb +2 -0
  34. data/lib/ruby_lsp/server.rb +54 -4
  35. data/lib/ruby_lsp/test_helper.rb +1 -1
  36. data/lib/ruby_lsp/type_inferrer.rb +86 -0
  37. metadata +29 -4
@@ -152,8 +152,7 @@ module RubyIndexer
152
152
  end
153
153
  def initialize(nesting, file_path, location, comments, parent_class)
154
154
  super(nesting, file_path, location, comments)
155
-
156
- @parent_class = T.let(parent_class, T.nilable(String))
155
+ @parent_class = parent_class
157
156
  end
158
157
 
159
158
  sig { override.returns(Integer) }
@@ -162,6 +161,22 @@ module RubyIndexer
162
161
  end
163
162
  end
164
163
 
164
+ class SingletonClass < Class
165
+ extend T::Sig
166
+
167
+ sig { params(location: Prism::Location, comments: T::Array[String]).void }
168
+ def update_singleton_information(location, comments)
169
+ # Create a new RubyIndexer::Location object from the Prism location
170
+ @location = Location.new(
171
+ location.start_line,
172
+ location.end_line,
173
+ location.start_column,
174
+ location.end_column,
175
+ )
176
+ @comments.concat(comments)
177
+ end
178
+ end
179
+
165
180
  class Constant < Entry
166
181
  end
167
182
 
@@ -190,6 +205,10 @@ module RubyIndexer
190
205
 
191
206
  # An optional method parameter, e.g. `def foo(a = 123)`
192
207
  class OptionalParameter < Parameter
208
+ sig { override.returns(Symbol) }
209
+ def decorated_name
210
+ :"#{@name} = <default>"
211
+ end
193
212
  end
194
213
 
195
214
  # An required keyword method parameter, e.g. `def foo(a:)`
@@ -204,7 +223,7 @@ module RubyIndexer
204
223
  class OptionalKeywordParameter < Parameter
205
224
  sig { override.returns(Symbol) }
206
225
  def decorated_name
207
- :"#{@name}:"
226
+ :"#{@name}: <default>"
208
227
  end
209
228
  end
210
229
 
@@ -265,6 +284,12 @@ module RubyIndexer
265
284
 
266
285
  sig { abstract.returns(T::Array[Parameter]) }
267
286
  def parameters; end
287
+
288
+ # Returns a string with the decorated names of the parameters of this member. E.g.: `(a, b = 1, c: 2)`
289
+ sig { returns(String) }
290
+ def decorated_parameters
291
+ "(#{parameters.map(&:decorated_name).join(", ")})"
292
+ end
268
293
  end
269
294
 
270
295
  class Accessor < Member
@@ -280,9 +305,6 @@ module RubyIndexer
280
305
 
281
306
  class Method < Member
282
307
  extend T::Sig
283
- extend T::Helpers
284
-
285
- abstract!
286
308
 
287
309
  sig { override.returns(T::Array[Parameter]) }
288
310
  attr_reader :parameters
@@ -293,110 +315,17 @@ module RubyIndexer
293
315
  file_path: String,
294
316
  location: T.any(Prism::Location, RubyIndexer::Location),
295
317
  comments: T::Array[String],
296
- parameters_node: T.nilable(Prism::ParametersNode),
318
+ parameters: T::Array[Parameter],
297
319
  visibility: Visibility,
298
320
  owner: T.nilable(Entry::Namespace),
299
321
  ).void
300
322
  end
301
- def initialize(name, file_path, location, comments, parameters_node, visibility, owner) # rubocop:disable Metrics/ParameterLists
323
+ def initialize(name, file_path, location, comments, parameters, visibility, owner) # rubocop:disable Metrics/ParameterLists
302
324
  super(name, file_path, location, comments, visibility, owner)
303
-
304
- @parameters = T.let(list_params(parameters_node), T::Array[Parameter])
305
- end
306
-
307
- private
308
-
309
- sig { params(parameters_node: T.nilable(Prism::ParametersNode)).returns(T::Array[Parameter]) }
310
- def list_params(parameters_node)
311
- return [] unless parameters_node
312
-
313
- parameters = []
314
-
315
- parameters_node.requireds.each do |required|
316
- name = parameter_name(required)
317
- next unless name
318
-
319
- parameters << RequiredParameter.new(name: name)
320
- end
321
-
322
- parameters_node.optionals.each do |optional|
323
- name = parameter_name(optional)
324
- next unless name
325
-
326
- parameters << OptionalParameter.new(name: name)
327
- end
328
-
329
- parameters_node.keywords.each do |keyword|
330
- name = parameter_name(keyword)
331
- next unless name
332
-
333
- case keyword
334
- when Prism::RequiredKeywordParameterNode
335
- parameters << KeywordParameter.new(name: name)
336
- when Prism::OptionalKeywordParameterNode
337
- parameters << OptionalKeywordParameter.new(name: name)
338
- end
339
- end
340
-
341
- rest = parameters_node.rest
342
-
343
- if rest.is_a?(Prism::RestParameterNode)
344
- rest_name = rest.name || RestParameter::DEFAULT_NAME
345
- parameters << RestParameter.new(name: rest_name)
346
- end
347
-
348
- keyword_rest = parameters_node.keyword_rest
349
-
350
- if keyword_rest.is_a?(Prism::KeywordRestParameterNode)
351
- keyword_rest_name = parameter_name(keyword_rest) || KeywordRestParameter::DEFAULT_NAME
352
- parameters << KeywordRestParameter.new(name: keyword_rest_name)
353
- end
354
-
355
- parameters_node.posts.each do |post|
356
- name = parameter_name(post)
357
- next unless name
358
-
359
- parameters << RequiredParameter.new(name: name)
360
- end
361
-
362
- block = parameters_node.block
363
- parameters << BlockParameter.new(name: block.name || BlockParameter::DEFAULT_NAME) if block
364
-
365
- parameters
366
- end
367
-
368
- sig { params(node: T.nilable(Prism::Node)).returns(T.nilable(Symbol)) }
369
- def parameter_name(node)
370
- case node
371
- when Prism::RequiredParameterNode, Prism::OptionalParameterNode,
372
- Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
373
- Prism::RestParameterNode, Prism::KeywordRestParameterNode
374
- node.name
375
- when Prism::MultiTargetNode
376
- names = node.lefts.map { |parameter_node| parameter_name(parameter_node) }
377
-
378
- rest = node.rest
379
- if rest.is_a?(Prism::SplatNode)
380
- name = rest.expression&.slice
381
- names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym)
382
- end
383
-
384
- names << nil if rest.is_a?(Prism::ImplicitRestNode)
385
-
386
- names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) })
387
-
388
- names_with_commas = names.join(", ")
389
- :"(#{names_with_commas})"
390
- end
325
+ @parameters = parameters
391
326
  end
392
327
  end
393
328
 
394
- class SingletonMethod < Method
395
- end
396
-
397
- class InstanceMethod < Method
398
- end
399
-
400
329
  # An UnresolvedAlias points to a constant alias with a right hand side that has not yet been resolved. For
401
330
  # example, if we find
402
331
  #
@@ -469,5 +398,76 @@ module RubyIndexer
469
398
  @owner = owner
470
399
  end
471
400
  end
401
+
402
+ # An unresolved method alias is an alias entry for which we aren't sure what the right hand side points to yet. For
403
+ # example, if we have `alias a b`, we create an unresolved alias for `a` because we aren't sure immediate what `b`
404
+ # is referring to
405
+ class UnresolvedMethodAlias < Entry
406
+ extend T::Sig
407
+
408
+ sig { returns(String) }
409
+ attr_reader :new_name, :old_name
410
+
411
+ sig { returns(T.nilable(Entry::Namespace)) }
412
+ attr_reader :owner
413
+
414
+ sig do
415
+ params(
416
+ new_name: String,
417
+ old_name: String,
418
+ owner: T.nilable(Entry::Namespace),
419
+ file_path: String,
420
+ location: Prism::Location,
421
+ comments: T::Array[String],
422
+ ).void
423
+ end
424
+ def initialize(new_name, old_name, owner, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
425
+ super(new_name, file_path, location, comments)
426
+
427
+ @new_name = new_name
428
+ @old_name = old_name
429
+ @owner = owner
430
+ end
431
+ end
432
+
433
+ # A method alias is a resolved alias entry that points to the exact method target it refers to
434
+ class MethodAlias < Entry
435
+ extend T::Sig
436
+
437
+ sig { returns(T.any(Member, MethodAlias)) }
438
+ attr_reader :target
439
+
440
+ sig { params(target: T.any(Member, MethodAlias), unresolved_alias: UnresolvedMethodAlias).void }
441
+ def initialize(target, unresolved_alias)
442
+ full_comments = ["Alias for #{target.name}\n"]
443
+ full_comments.concat(unresolved_alias.comments)
444
+ full_comments << "\n"
445
+ full_comments.concat(target.comments)
446
+
447
+ super(
448
+ unresolved_alias.new_name,
449
+ unresolved_alias.file_path,
450
+ unresolved_alias.location,
451
+ full_comments,
452
+ )
453
+
454
+ @target = target
455
+ end
456
+
457
+ sig { returns(T.nilable(Entry::Namespace)) }
458
+ def owner
459
+ @target.owner
460
+ end
461
+
462
+ sig { returns(T::Array[Parameter]) }
463
+ def parameters
464
+ @target.parameters
465
+ end
466
+
467
+ sig { returns(String) }
468
+ def decorated_parameters
469
+ @target.decorated_parameters
470
+ end
471
+ end
472
472
  end
473
473
  end
@@ -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,45 +140,71 @@ 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
+ candidates.filter_map do |entry|
154
+ case entry
155
+ when Entry::Member, Entry::MethodAlias
156
+ entry if ancestors.any?(entry.owner&.name)
157
+ when Entry::UnresolvedMethodAlias
158
+ if ancestors.any?(entry.owner&.name)
159
+ resolved_alias = resolve_method_alias(entry, receiver_name)
160
+ resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
161
+ end
162
+ end
139
163
  end
140
- candidates
141
164
  end
142
165
 
143
- # Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
144
- # the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
145
- # 1. Foo::Bar::Baz
146
- # 2. Foo::Baz
147
- # 3. Baz
148
- sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
149
- def resolve(name, nesting)
166
+ # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
167
+ # documentation:
168
+ #
169
+ # name: the name of the reference how it was found in the source code (qualified or not)
170
+ # nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"])
171
+ # seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when
172
+ # resolving circular references
173
+ sig do
174
+ params(
175
+ name: String,
176
+ nesting: T::Array[String],
177
+ seen_names: T::Array[String],
178
+ ).returns(T.nilable(T::Array[Entry]))
179
+ end
180
+ def resolve(name, nesting, seen_names = [])
181
+ # If we have a top level reference, then we just search for it straight away ignoring the nesting
150
182
  if name.start_with?("::")
151
- name = name.delete_prefix("::")
152
- results = @entries[name] || @entries[follow_aliased_namespace(name)]
153
- return results&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e }
183
+ entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names)
184
+ return entries if entries
154
185
  end
155
186
 
156
- nesting.length.downto(0).each do |i|
157
- namespace = T.must(nesting[0...i]).join("::")
158
- full_name = namespace.empty? ? name : "#{namespace}::#{name}"
187
+ # Non qualified reference path
188
+ full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name
159
189
 
160
- # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
161
- # because the user might be trying to jump to the alias definition.
162
- #
163
- # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
164
- # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
165
- # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
166
- # `RubyLsp::Interface` part is an alias, that has to be resolved
167
- entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
168
- return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries
169
- end
190
+ # When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the
191
+ # constant. First, it will try to find the constant in the exact namespace where the reference was found
192
+ entries = direct_or_aliased_constant(full_name, seen_names)
193
+ return entries if entries
170
194
 
171
- nil
195
+ # If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes,
196
+ # unwrapping each level one by one. Important note: the top level is not included because that's the fallback of
197
+ # the algorithm after every other possibility has been exhausted
198
+ entries = lookup_enclosing_scopes(name, nesting, seen_names)
199
+ return entries if entries
200
+
201
+ # If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the
202
+ # specific namespace where the reference was found
203
+ entries = lookup_ancestor_chain(name, nesting, seen_names)
204
+ return entries if entries
205
+
206
+ # Finally, as a fallback, Ruby will search for the constant in the top level namespace
207
+ search_top_level(name, seen_names)
172
208
  rescue UnresolvableAliasError
173
209
  nil
174
210
  end
@@ -210,6 +246,12 @@ module RubyIndexer
210
246
  rescue Errno::EISDIR, Errno::ENOENT
211
247
  # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
212
248
  # it
249
+ rescue SystemStackError => e
250
+ if e.backtrace&.first&.include?("prism")
251
+ $stderr.puts "Prism error indexing #{indexable_path.full_path}: #{e.message}"
252
+ else
253
+ raise
254
+ end
213
255
  end
214
256
 
215
257
  # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
@@ -222,8 +264,8 @@ module RubyIndexer
222
264
  # If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
223
265
  # `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
224
266
  # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
225
- sig { params(name: String).returns(String) }
226
- def follow_aliased_namespace(name)
267
+ sig { params(name: String, seen_names: T::Array[String]).returns(String) }
268
+ def follow_aliased_namespace(name, seen_names = [])
227
269
  return name if @entries[name]
228
270
 
229
271
  parts = name.split("::")
@@ -236,16 +278,16 @@ module RubyIndexer
236
278
  case entry
237
279
  when Entry::Alias
238
280
  target = entry.target
239
- return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
281
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
240
282
  when Entry::UnresolvedAlias
241
- resolved = resolve_alias(entry)
283
+ resolved = resolve_alias(entry, seen_names)
242
284
 
243
285
  if resolved.is_a?(Entry::UnresolvedAlias)
244
286
  raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
245
287
  end
246
288
 
247
289
  target = resolved.target
248
- return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
290
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
249
291
  else
250
292
  real_parts.unshift(T.must(parts[i]))
251
293
  end
@@ -256,20 +298,32 @@ module RubyIndexer
256
298
 
257
299
  # Attempts to find methods for a resolved fully qualified receiver name.
258
300
  # Returns `nil` if the method does not exist on that receiver
259
- sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
301
+ sig do
302
+ params(
303
+ method_name: String,
304
+ receiver_name: String,
305
+ ).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)]))
306
+ end
260
307
  def resolve_method(method_name, receiver_name)
261
308
  method_entries = self[method_name]
262
- ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
263
309
  return unless method_entries
264
310
 
311
+ ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
265
312
  ancestors.each do |ancestor|
266
- found = method_entries.select do |entry|
267
- next unless entry.is_a?(Entry::Member)
268
-
269
- entry.owner&.name == ancestor
313
+ found = method_entries.filter_map do |entry|
314
+ case entry
315
+ when Entry::Member, Entry::MethodAlias
316
+ entry if entry.owner&.name == ancestor
317
+ when Entry::UnresolvedMethodAlias
318
+ # Resolve aliases lazily as we find them
319
+ if entry.owner&.name == ancestor
320
+ resolved_alias = resolve_method_alias(entry, receiver_name)
321
+ resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
322
+ end
323
+ end
270
324
  end
271
325
 
272
- return T.cast(found, T::Array[Entry::Member]) if found.any?
326
+ return found if found.any?
273
327
  end
274
328
 
275
329
  nil
@@ -291,6 +345,10 @@ module RubyIndexer
291
345
  cached_ancestors = @ancestors[fully_qualified_name]
292
346
  return cached_ancestors if cached_ancestors
293
347
 
348
+ # If we don't have an entry for `name`, raise
349
+ entries = self[fully_qualified_name]
350
+ raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
351
+
294
352
  ancestors = [fully_qualified_name]
295
353
 
296
354
  # Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
@@ -298,10 +356,6 @@ module RubyIndexer
298
356
  # the cache will reflect the final result
299
357
  @ancestors[fully_qualified_name] = ancestors
300
358
 
301
- # If we don't have an entry for `name`, raise
302
- entries = resolve(fully_qualified_name, [])
303
- raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
304
-
305
359
  # If none of the entries for `name` are namespaces, raise
306
360
  namespaces = entries.filter_map do |entry|
307
361
  case entry
@@ -395,8 +449,8 @@ module RubyIndexer
395
449
  entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
396
450
  ancestors = linearized_ancestors_of(owner_name)
397
451
 
398
- variables = entries.uniq(&:name)
399
- variables.select! { |e| ancestors.any?(e.owner&.name) }
452
+ variables = entries.select { |e| ancestors.any?(e.owner&.name) }
453
+ variables.uniq!(&:name)
400
454
  variables
401
455
  end
402
456
 
@@ -434,22 +488,146 @@ module RubyIndexer
434
488
 
435
489
  # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
436
490
  # that doesn't exist, then we return the same UnresolvedAlias
437
- sig { params(entry: Entry::UnresolvedAlias).returns(T.any(Entry::Alias, Entry::UnresolvedAlias)) }
438
- def resolve_alias(entry)
439
- target = resolve(entry.target, entry.nesting)
491
+ sig do
492
+ params(
493
+ entry: Entry::UnresolvedAlias,
494
+ seen_names: T::Array[String],
495
+ ).returns(T.any(Entry::Alias, Entry::UnresolvedAlias))
496
+ end
497
+ def resolve_alias(entry, seen_names)
498
+ alias_name = entry.name
499
+ return entry if seen_names.include?(alias_name)
500
+
501
+ seen_names << alias_name
502
+
503
+ target = resolve(entry.target, entry.nesting, seen_names)
440
504
  return entry unless target
441
505
 
442
506
  target_name = T.must(target.first).name
443
507
  resolved_alias = Entry::Alias.new(target_name, entry)
444
508
 
445
509
  # Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
446
- original_entries = T.must(@entries[entry.name])
510
+ original_entries = T.must(@entries[alias_name])
447
511
  original_entries.delete(entry)
448
512
  original_entries << resolved_alias
449
513
 
450
- @entries_tree.insert(entry.name, original_entries)
514
+ @entries_tree.insert(alias_name, original_entries)
451
515
 
452
516
  resolved_alias
453
517
  end
518
+
519
+ sig do
520
+ params(
521
+ name: String,
522
+ nesting: T::Array[String],
523
+ seen_names: T::Array[String],
524
+ ).returns(T.nilable(T::Array[Entry]))
525
+ end
526
+ def lookup_enclosing_scopes(name, nesting, seen_names)
527
+ nesting.length.downto(1).each do |i|
528
+ namespace = T.must(nesting[0...i]).join("::")
529
+
530
+ # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
531
+ # because the user might be trying to jump to the alias definition.
532
+ #
533
+ # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
534
+ # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
535
+ # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
536
+ # `RubyLsp::Interface` part is an alias, that has to be resolved
537
+ entries = direct_or_aliased_constant("#{namespace}::#{name}", seen_names)
538
+ return entries if entries
539
+ end
540
+
541
+ nil
542
+ end
543
+
544
+ sig do
545
+ params(
546
+ name: String,
547
+ nesting: T::Array[String],
548
+ seen_names: T::Array[String],
549
+ ).returns(T.nilable(T::Array[Entry]))
550
+ end
551
+ def lookup_ancestor_chain(name, nesting, seen_names)
552
+ *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
553
+ return if nesting_parts.empty?
554
+
555
+ namespace_entries = resolve(nesting_parts.join("::"), [], seen_names)
556
+ return unless namespace_entries
557
+
558
+ ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
559
+
560
+ ancestors.each do |ancestor_name|
561
+ entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
562
+ return entries if entries
563
+ end
564
+
565
+ nil
566
+ rescue NonExistingNamespaceError
567
+ nil
568
+ end
569
+
570
+ # Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
571
+ # of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
572
+ # `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
573
+ # nesting
574
+ sig { params(name: String, nesting: T::Array[String]).returns(String) }
575
+ def build_non_redundant_full_name(name, nesting)
576
+ return name if nesting.empty?
577
+
578
+ namespace = nesting.join("::")
579
+
580
+ # If the name is not qualified, we can just concatenate the nesting and the name
581
+ return "#{namespace}::#{name}" unless name.include?("::")
582
+
583
+ name_parts = name.split("::")
584
+
585
+ # Find the first part of the name that is not in the nesting
586
+ index = name_parts.index { |part| !nesting.include?(part) }
587
+
588
+ if index.nil?
589
+ # All parts of the nesting are redundant because they are already present in the name. We can return the name
590
+ # directly
591
+ name
592
+ elsif index == 0
593
+ # No parts of the nesting are in the name, we can concatenate the namespace and the name
594
+ "#{namespace}::#{name}"
595
+ else
596
+ # The name includes some parts of the nesting. We need to remove the redundant parts
597
+ "#{namespace}::#{T.must(name_parts[index..-1]).join("::")}"
598
+ end
599
+ end
600
+
601
+ sig { params(full_name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
602
+ def direct_or_aliased_constant(full_name, seen_names)
603
+ entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
604
+ entries&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
605
+ end
606
+
607
+ sig { params(name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
608
+ def search_top_level(name, seen_names)
609
+ @entries[name]&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
610
+ end
611
+
612
+ # Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to
613
+ # identify the target or the same unresolved alias entry if we couldn't
614
+ sig do
615
+ params(
616
+ entry: Entry::UnresolvedMethodAlias,
617
+ receiver_name: String,
618
+ ).returns(T.any(Entry::MethodAlias, Entry::UnresolvedMethodAlias))
619
+ end
620
+ def resolve_method_alias(entry, receiver_name)
621
+ return entry if entry.new_name == entry.old_name
622
+
623
+ target_method_entries = resolve_method(entry.old_name, receiver_name)
624
+ return entry unless target_method_entries
625
+
626
+ resolved_alias = Entry::MethodAlias.new(T.must(target_method_entries.first), entry)
627
+ original_entries = T.must(@entries[entry.new_name])
628
+ original_entries.delete(entry)
629
+ original_entries << resolved_alias
630
+ resolved_alias
631
+ end
454
632
  end
455
633
  end