ruby-lsp 0.17.2 → 0.17.4

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