ruby-lsp 0.16.7 → 0.17.3

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +195 -18
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +129 -12
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +333 -44
  7. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +99 -0
  8. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +58 -11
  10. data/lib/ruby_indexer/test/configuration_test.rb +5 -6
  11. data/lib/ruby_indexer/test/constant_test.rb +9 -9
  12. data/lib/ruby_indexer/test/index_test.rb +867 -7
  13. data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
  14. data/lib/ruby_indexer/test/method_test.rb +57 -0
  15. data/lib/ruby_indexer/test/rbs_indexer_test.rb +42 -0
  16. data/lib/ruby_indexer/test/test_case.rb +10 -1
  17. data/lib/ruby_lsp/addon.rb +8 -8
  18. data/lib/ruby_lsp/document.rb +26 -3
  19. data/lib/ruby_lsp/global_state.rb +37 -16
  20. data/lib/ruby_lsp/internal.rb +2 -0
  21. data/lib/ruby_lsp/listeners/code_lens.rb +2 -2
  22. data/lib/ruby_lsp/listeners/completion.rb +74 -17
  23. data/lib/ruby_lsp/listeners/definition.rb +82 -24
  24. data/lib/ruby_lsp/listeners/hover.rb +62 -6
  25. data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
  26. data/lib/ruby_lsp/node_context.rb +39 -0
  27. data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
  28. data/lib/ruby_lsp/requests/code_actions.rb +16 -15
  29. data/lib/ruby_lsp/requests/completion.rb +21 -13
  30. data/lib/ruby_lsp/requests/completion_resolve.rb +9 -0
  31. data/lib/ruby_lsp/requests/definition.rb +25 -5
  32. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  33. data/lib/ruby_lsp/requests/hover.rb +5 -6
  34. data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
  35. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  36. data/lib/ruby_lsp/requests/support/common.rb +4 -3
  37. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +23 -6
  38. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +5 -1
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  40. data/lib/ruby_lsp/requests/workspace_symbol.rb +7 -4
  41. data/lib/ruby_lsp/server.rb +22 -5
  42. data/lib/ruby_lsp/test_helper.rb +1 -0
  43. metadata +29 -5
@@ -6,6 +6,7 @@ module RubyIndexer
6
6
  extend T::Sig
7
7
 
8
8
  class UnresolvableAliasError < StandardError; end
9
+ class NonExistingNamespaceError < StandardError; end
9
10
 
10
11
  # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
11
12
  ENTRY_SIMILARITY_THRESHOLD = 0.7
@@ -31,6 +32,9 @@ module RubyIndexer
31
32
 
32
33
  # Holds all require paths for every indexed item so that we can provide autocomplete for requires
33
34
  @require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
35
+
36
+ # Holds the linearized ancestors list for every namespace
37
+ @ancestors = T.let({}, T::Hash[String, T::Array[String]])
34
38
  end
35
39
 
36
40
  sig { params(indexable: IndexablePath).void }
@@ -126,35 +130,58 @@ module RubyIndexer
126
130
  results.flat_map(&:first)
127
131
  end
128
132
 
129
- # Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
130
- # the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
131
- # 1. Foo::Bar::Baz
132
- # 2. Foo::Baz
133
- # 3. Baz
134
- sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
135
- def resolve(name, nesting)
133
+ sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) }
134
+ def method_completion_candidates(name, receiver_name)
135
+ 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)
139
+ end
140
+ candidates
141
+ end
142
+
143
+ # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
144
+ # documentation:
145
+ #
146
+ # name: the name of the reference how it was found in the source code (qualified or not)
147
+ # nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"])
148
+ # seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when
149
+ # resolving circular references
150
+ sig do
151
+ params(
152
+ name: String,
153
+ nesting: T::Array[String],
154
+ seen_names: T::Array[String],
155
+ ).returns(T.nilable(T::Array[Entry]))
156
+ end
157
+ def resolve(name, nesting, seen_names = [])
158
+ # If we have a top level reference, then we just search for it straight away ignoring the nesting
136
159
  if name.start_with?("::")
137
- name = name.delete_prefix("::")
138
- results = @entries[name] || @entries[follow_aliased_namespace(name)]
139
- return results&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e }
160
+ entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names)
161
+ return entries if entries
140
162
  end
141
163
 
142
- nesting.length.downto(0).each do |i|
143
- namespace = T.must(nesting[0...i]).join("::")
144
- full_name = namespace.empty? ? name : "#{namespace}::#{name}"
164
+ # Non qualified reference path
165
+ full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name
145
166
 
146
- # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
147
- # because the user might be trying to jump to the alias definition.
148
- #
149
- # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
150
- # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
151
- # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
152
- # `RubyLsp::Interface` part is an alias, that has to be resolved
153
- entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
154
- return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries
155
- end
167
+ # When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the
168
+ # constant. First, it will try to find the constant in the exact namespace where the reference was found
169
+ entries = direct_or_aliased_constant(full_name, seen_names)
170
+ return entries if entries
156
171
 
157
- nil
172
+ # If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes,
173
+ # unwrapping each level one by one. Important note: the top level is not included because that's the fallback of
174
+ # the algorithm after every other possibility has been exhausted
175
+ entries = lookup_enclosing_scopes(name, nesting, seen_names)
176
+ return entries if entries
177
+
178
+ # If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the
179
+ # specific namespace where the reference was found
180
+ entries = lookup_ancestor_chain(name, nesting, seen_names)
181
+ return entries if entries
182
+
183
+ # Finally, as a fallback, Ruby will search for the constant in the top level namespace
184
+ search_top_level(name, seen_names)
158
185
  rescue UnresolvableAliasError
159
186
  nil
160
187
  end
@@ -208,8 +235,8 @@ module RubyIndexer
208
235
  # If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
209
236
  # `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
210
237
  # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
211
- sig { params(name: String).returns(String) }
212
- def follow_aliased_namespace(name)
238
+ sig { params(name: String, seen_names: T::Array[String]).returns(String) }
239
+ def follow_aliased_namespace(name, seen_names = [])
213
240
  return name if @entries[name]
214
241
 
215
242
  parts = name.split("::")
@@ -222,16 +249,16 @@ module RubyIndexer
222
249
  case entry
223
250
  when Entry::Alias
224
251
  target = entry.target
225
- return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
252
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
226
253
  when Entry::UnresolvedAlias
227
- resolved = resolve_alias(entry)
254
+ resolved = resolve_alias(entry, seen_names)
228
255
 
229
256
  if resolved.is_a?(Entry::UnresolvedAlias)
230
257
  raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
231
258
  end
232
259
 
233
260
  target = resolved.target
234
- return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
261
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
235
262
  else
236
263
  real_parts.unshift(T.must(parts[i]))
237
264
  end
@@ -245,38 +272,300 @@ module RubyIndexer
245
272
  sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
246
273
  def resolve_method(method_name, receiver_name)
247
274
  method_entries = self[method_name]
248
- owner_entries = self[receiver_name]
249
- return unless owner_entries && method_entries
250
-
251
- owner_name = T.must(owner_entries.first).name
252
- T.cast(
253
- method_entries.grep(Entry::Member).select do |entry|
254
- T.cast(entry, Entry::Member).owner&.name == owner_name
255
- end,
256
- T::Array[Entry::Member],
257
- )
275
+ ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
276
+ return unless method_entries
277
+
278
+ ancestors.each do |ancestor|
279
+ found = method_entries.select do |entry|
280
+ next unless entry.is_a?(Entry::Member)
281
+
282
+ entry.owner&.name == ancestor
283
+ end
284
+
285
+ return T.cast(found, T::Array[Entry::Member]) if found.any?
286
+ end
287
+
288
+ nil
289
+ rescue NonExistingNamespaceError
290
+ nil
291
+ end
292
+
293
+ # Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
294
+ # or constant declarations.
295
+ #
296
+ # When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
297
+ # everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
298
+ # module that prepends another module, then the prepend module appears before the included module.
299
+ #
300
+ # The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
301
+ sig { params(fully_qualified_name: String).returns(T::Array[String]) }
302
+ def linearized_ancestors_of(fully_qualified_name)
303
+ # If we already computed the ancestors for this namespace, return it straight away
304
+ cached_ancestors = @ancestors[fully_qualified_name]
305
+ return cached_ancestors if cached_ancestors
306
+
307
+ # If we don't have an entry for `name`, raise
308
+ entries = self[fully_qualified_name]
309
+ raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
310
+
311
+ ancestors = [fully_qualified_name]
312
+
313
+ # Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
314
+ # this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
315
+ # the cache will reflect the final result
316
+ @ancestors[fully_qualified_name] = ancestors
317
+
318
+ # If none of the entries for `name` are namespaces, raise
319
+ namespaces = entries.filter_map do |entry|
320
+ case entry
321
+ when Entry::Namespace
322
+ entry
323
+ when Entry::Alias
324
+ self[entry.target]&.grep(Entry::Namespace)
325
+ end
326
+ end.flatten
327
+
328
+ raise NonExistingNamespaceError,
329
+ "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
330
+
331
+ mixin_operations = namespaces.flat_map(&:mixin_operations)
332
+ main_namespace_index = 0
333
+
334
+ # The original nesting where we discovered this namespace, so that we resolve the correct names of the
335
+ # included/prepended/extended modules and parent classes
336
+ nesting = T.must(namespaces.first).nesting
337
+
338
+ mixin_operations.each do |operation|
339
+ resolved_module = resolve(operation.module_name, nesting)
340
+ next unless resolved_module
341
+
342
+ module_fully_qualified_name = T.must(resolved_module.first).name
343
+
344
+ case operation
345
+ when Entry::Prepend
346
+ # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
347
+ # the actual namespace twice. However, it does not check if it has been included because you are allowed to
348
+ # prepend the same module after it has already been included
349
+ linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
350
+
351
+ # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
352
+ # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
353
+ # be inserted after `"A`
354
+ uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
355
+ insert_position = linearized_prepends.length - uniq_prepends.length
356
+
357
+ T.unsafe(ancestors).insert(
358
+ insert_position,
359
+ *(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
360
+ )
361
+
362
+ main_namespace_index += linearized_prepends.length
363
+ when Entry::Include
364
+ # When including a module, Ruby will always prevent duplicate entries in case the module has already been
365
+ # prepended or included
366
+ linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
367
+ T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
368
+ end
369
+ end
370
+
371
+ # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
372
+ # from two diffent classes in different files, we simply ignore it
373
+ superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
374
+
375
+ if superclass
376
+ # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
377
+ # error. We need to ensure that this isn't the case
378
+ parent_class = T.must(superclass.parent_class)
379
+
380
+ resolved_parent_class = resolve(parent_class, nesting)
381
+ parent_class_name = resolved_parent_class&.first&.name
382
+
383
+ if parent_class_name && fully_qualified_name != parent_class_name
384
+ ancestors.concat(linearized_ancestors_of(parent_class_name))
385
+ end
386
+ end
387
+
388
+ ancestors
389
+ end
390
+
391
+ # Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
392
+ # and find inherited instance variables as well
393
+ sig { params(variable_name: String, owner_name: String).returns(T.nilable(T::Array[Entry::InstanceVariable])) }
394
+ def resolve_instance_variable(variable_name, owner_name)
395
+ entries = T.cast(self[variable_name], T.nilable(T::Array[Entry::InstanceVariable]))
396
+ return unless entries
397
+
398
+ ancestors = linearized_ancestors_of(owner_name)
399
+ return if ancestors.empty?
400
+
401
+ entries.select { |e| ancestors.include?(e.owner&.name) }
402
+ end
403
+
404
+ # Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
405
+ # include the `@` prefix
406
+ sig { params(name: String, owner_name: String).returns(T::Array[Entry::InstanceVariable]) }
407
+ def instance_variable_completion_candidates(name, owner_name)
408
+ entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
409
+ ancestors = linearized_ancestors_of(owner_name)
410
+
411
+ variables = entries.select { |e| ancestors.any?(e.owner&.name) }
412
+ variables.uniq!(&:name)
413
+ variables
414
+ end
415
+
416
+ # Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed,
417
+ # removed declarations removed and that the ancestor linearization cache is cleared if necessary
418
+ sig { params(indexable: IndexablePath).void }
419
+ def handle_change(indexable)
420
+ original_entries = @files_to_entries[indexable.full_path]
421
+
422
+ delete(indexable)
423
+ index_single(indexable)
424
+
425
+ updated_entries = @files_to_entries[indexable.full_path]
426
+
427
+ return unless original_entries && updated_entries
428
+
429
+ # A change in one ancestor may impact several different others, which could be including that ancestor through
430
+ # indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
431
+ # ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
432
+ # ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
433
+ original_map = T.cast(
434
+ original_entries.select { |e| e.is_a?(Entry::Namespace) },
435
+ T::Array[Entry::Namespace],
436
+ ).to_h { |e| [e.name, e.ancestor_hash] }
437
+
438
+ updated_map = T.cast(
439
+ updated_entries.select { |e| e.is_a?(Entry::Namespace) },
440
+ T::Array[Entry::Namespace],
441
+ ).to_h { |e| [e.name, e.ancestor_hash] }
442
+
443
+ @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
258
444
  end
259
445
 
260
446
  private
261
447
 
262
448
  # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
263
449
  # that doesn't exist, then we return the same UnresolvedAlias
264
- sig { params(entry: Entry::UnresolvedAlias).returns(T.any(Entry::Alias, Entry::UnresolvedAlias)) }
265
- def resolve_alias(entry)
266
- target = resolve(entry.target, entry.nesting)
450
+ sig do
451
+ params(
452
+ entry: Entry::UnresolvedAlias,
453
+ seen_names: T::Array[String],
454
+ ).returns(T.any(Entry::Alias, Entry::UnresolvedAlias))
455
+ end
456
+ def resolve_alias(entry, seen_names)
457
+ alias_name = entry.name
458
+ return entry if seen_names.include?(alias_name)
459
+
460
+ seen_names << alias_name
461
+
462
+ target = resolve(entry.target, entry.nesting, seen_names)
267
463
  return entry unless target
268
464
 
269
465
  target_name = T.must(target.first).name
270
466
  resolved_alias = Entry::Alias.new(target_name, entry)
271
467
 
272
468
  # Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
273
- original_entries = T.must(@entries[entry.name])
469
+ original_entries = T.must(@entries[alias_name])
274
470
  original_entries.delete(entry)
275
471
  original_entries << resolved_alias
276
472
 
277
- @entries_tree.insert(entry.name, original_entries)
473
+ @entries_tree.insert(alias_name, original_entries)
278
474
 
279
475
  resolved_alias
280
476
  end
477
+
478
+ sig do
479
+ params(
480
+ name: String,
481
+ nesting: T::Array[String],
482
+ seen_names: T::Array[String],
483
+ ).returns(T.nilable(T::Array[Entry]))
484
+ end
485
+ def lookup_enclosing_scopes(name, nesting, seen_names)
486
+ nesting.length.downto(1).each do |i|
487
+ namespace = T.must(nesting[0...i]).join("::")
488
+
489
+ # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
490
+ # because the user might be trying to jump to the alias definition.
491
+ #
492
+ # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
493
+ # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
494
+ # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
495
+ # `RubyLsp::Interface` part is an alias, that has to be resolved
496
+ entries = direct_or_aliased_constant("#{namespace}::#{name}", seen_names)
497
+ return entries if entries
498
+ end
499
+
500
+ nil
501
+ end
502
+
503
+ sig do
504
+ params(
505
+ name: String,
506
+ nesting: T::Array[String],
507
+ seen_names: T::Array[String],
508
+ ).returns(T.nilable(T::Array[Entry]))
509
+ end
510
+ def lookup_ancestor_chain(name, nesting, seen_names)
511
+ *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
512
+ return if T.must(nesting_parts).empty?
513
+
514
+ namespace_entries = resolve(T.must(nesting_parts).join("::"), [], seen_names)
515
+ return unless namespace_entries
516
+
517
+ ancestors = T.must(nesting_parts).empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
518
+
519
+ ancestors.each do |ancestor_name|
520
+ entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
521
+ return entries if entries
522
+ end
523
+
524
+ nil
525
+ rescue NonExistingNamespaceError
526
+ nil
527
+ end
528
+
529
+ # Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
530
+ # of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
531
+ # `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
532
+ # nesting
533
+ sig { params(name: String, nesting: T::Array[String]).returns(String) }
534
+ def build_non_redundant_full_name(name, nesting)
535
+ return name if nesting.empty?
536
+
537
+ namespace = nesting.join("::")
538
+
539
+ # If the name is not qualified, we can just concatenate the nesting and the name
540
+ return "#{namespace}::#{name}" unless name.include?("::")
541
+
542
+ name_parts = name.split("::")
543
+
544
+ # Find the first part of the name that is not in the nesting
545
+ index = name_parts.index { |part| !nesting.include?(part) }
546
+
547
+ if index.nil?
548
+ # All parts of the nesting are redundant because they are already present in the name. We can return the name
549
+ # directly
550
+ name
551
+ elsif index == 0
552
+ # No parts of the nesting are in the name, we can concatenate the namespace and the name
553
+ "#{namespace}::#{name}"
554
+ else
555
+ # The name includes some parts of the nesting. We need to remove the redundant parts
556
+ "#{namespace}::#{T.must(name_parts[index..-1]).join("::")}"
557
+ end
558
+ end
559
+
560
+ sig { params(full_name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
561
+ def direct_or_aliased_constant(full_name, seen_names)
562
+ entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
563
+ entries&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
564
+ end
565
+
566
+ sig { params(name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
567
+ def search_top_level(name, seen_names)
568
+ @entries[name]&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
569
+ end
281
570
  end
282
571
  end
@@ -0,0 +1,99 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ class RBSIndexer
6
+ extend T::Sig
7
+
8
+ sig { params(index: Index).void }
9
+ def initialize(index)
10
+ @index = index
11
+ end
12
+
13
+ sig { void }
14
+ def index_ruby_core
15
+ loader = RBS::EnvironmentLoader.new
16
+ RBS::Environment.from_loader(loader).resolve_type_names
17
+
18
+ loader.each_signature do |source, pathname, _buffer, declarations, _directives|
19
+ process_signature(source, pathname, declarations)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ sig { params(source: T.untyped, pathname: Pathname, declarations: T::Array[RBS::AST::Declarations::Base]).void }
26
+ def process_signature(source, pathname, declarations)
27
+ declarations.each do |declaration|
28
+ process_declaration(declaration, pathname)
29
+ end
30
+ end
31
+
32
+ sig { params(declaration: RBS::AST::Declarations::Base, pathname: Pathname).void }
33
+ def process_declaration(declaration, pathname)
34
+ case declaration
35
+ when RBS::AST::Declarations::Class
36
+ handle_class_declaration(declaration, pathname)
37
+ when RBS::AST::Declarations::Module
38
+ handle_module_declaration(declaration, pathname)
39
+ else # rubocop:disable Style/EmptyElse
40
+ # Other kinds not yet handled
41
+ end
42
+ end
43
+
44
+ sig { params(declaration: RBS::AST::Declarations::Class, pathname: Pathname).void }
45
+ def handle_class_declaration(declaration, pathname)
46
+ nesting = [declaration.name.name.to_s]
47
+ file_path = pathname.to_s
48
+ location = to_ruby_indexer_location(declaration.location)
49
+ comments = Array(declaration.comment&.string)
50
+ parent_class = declaration.super_class&.name&.name&.to_s
51
+ class_entry = Entry::Class.new(nesting, file_path, location, comments, parent_class)
52
+ add_declaration_mixins_to_entry(declaration, class_entry)
53
+ @index << class_entry
54
+ end
55
+
56
+ sig { params(declaration: RBS::AST::Declarations::Module, pathname: Pathname).void }
57
+ def handle_module_declaration(declaration, pathname)
58
+ nesting = [declaration.name.name.to_s]
59
+ file_path = pathname.to_s
60
+ location = to_ruby_indexer_location(declaration.location)
61
+ comments = Array(declaration.comment&.string)
62
+ module_entry = Entry::Module.new(nesting, file_path, location, comments)
63
+ add_declaration_mixins_to_entry(declaration, module_entry)
64
+ @index << module_entry
65
+ end
66
+
67
+ sig { params(rbs_location: RBS::Location).returns(RubyIndexer::Location) }
68
+ def to_ruby_indexer_location(rbs_location)
69
+ RubyIndexer::Location.new(
70
+ rbs_location.start_line,
71
+ rbs_location.end_line,
72
+ rbs_location.start_column,
73
+ rbs_location.end_column,
74
+ )
75
+ end
76
+
77
+ sig do
78
+ params(
79
+ declaration: T.any(RBS::AST::Declarations::Class, RBS::AST::Declarations::Module),
80
+ entry: Entry::Namespace,
81
+ ).void
82
+ end
83
+ def add_declaration_mixins_to_entry(declaration, entry)
84
+ declaration.each_mixin do |mixin|
85
+ name = mixin.name.name.to_s
86
+ mixin_operation =
87
+ case mixin
88
+ when RBS::AST::Members::Include
89
+ Entry::Include.new(name)
90
+ when RBS::AST::Members::Extend
91
+ Entry::Extend.new(name)
92
+ when RBS::AST::Members::Prepend
93
+ Entry::Prepend.new(name)
94
+ end
95
+ entry.mixin_operations << mixin_operation if mixin_operation
96
+ end
97
+ end
98
+ end
99
+ end
@@ -11,6 +11,7 @@ require "ruby_indexer/lib/ruby_indexer/entry"
11
11
  require "ruby_indexer/lib/ruby_indexer/configuration"
12
12
  require "ruby_indexer/lib/ruby_indexer/prefix_tree"
13
13
  require "ruby_indexer/lib/ruby_indexer/location"
14
+ require "ruby_indexer/lib/ruby_indexer/rbs_indexer"
14
15
 
15
16
  module RubyIndexer
16
17
  @configuration = T.let(Configuration.new, Configuration)
@@ -191,7 +191,8 @@ module RubyIndexer
191
191
 
192
192
  @index.delete(IndexablePath.new(nil, "/fake/path/foo.rb"))
193
193
  refute_entry("Foo")
194
- assert_empty(@index.instance_variable_get(:@files_to_entries))
194
+
195
+ assert_no_indexed_entries
195
196
  end
196
197
 
197
198
  def test_comments_can_be_attached_to_a_class
@@ -290,13 +291,13 @@ module RubyIndexer
290
291
  RUBY
291
292
 
292
293
  b_const = @index["A::B"].first
293
- assert_equal(:private, b_const.visibility)
294
+ assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
294
295
 
295
296
  c_const = @index["A::C"].first
296
- assert_equal(:private, c_const.visibility)
297
+ assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
297
298
 
298
299
  d_const = @index["A::D"].first
299
- assert_equal(:public, d_const.visibility)
300
+ assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
300
301
  end
301
302
 
302
303
  def test_keeping_track_of_super_classes
@@ -323,7 +324,7 @@ module RubyIndexer
323
324
  assert_equal("Bar", foo.parent_class)
324
325
 
325
326
  baz = T.must(@index["Baz"].first)
326
- assert_nil(baz.parent_class)
327
+ assert_equal("::Object", baz.parent_class)
327
328
 
328
329
  qux = T.must(@index["Something::Qux"].first)
329
330
  assert_equal("::Baz", qux.parent_class)
@@ -369,13 +370,13 @@ module RubyIndexer
369
370
  RUBY
370
371
 
371
372
  foo = T.must(@index["Foo"][0])
372
- assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)
373
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
373
374
 
374
375
  qux = T.must(@index["Foo::Qux"][0])
375
- assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)
376
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
376
377
 
377
378
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
378
- assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
379
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
379
380
  end
380
381
 
381
382
  def test_keeping_track_of_prepended_modules
@@ -415,13 +416,59 @@ module RubyIndexer
415
416
  RUBY
416
417
 
417
418
  foo = T.must(@index["Foo"][0])
418
- assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.prepended_modules)
419
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
420
+
421
+ qux = T.must(@index["Foo::Qux"][0])
422
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
423
+
424
+ constant_path_references = T.must(@index["ConstantPathReferences"][0])
425
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
426
+ end
427
+
428
+ def test_keeping_track_of_extended_modules
429
+ index(<<~RUBY)
430
+ class Foo
431
+ # valid syntaxes that we can index
432
+ extend A1
433
+ self.extend A2
434
+ extend A3, A4
435
+ self.extend A5, A6
436
+
437
+ # valid syntaxes that we cannot index because of their dynamic nature
438
+ extend some_variable_or_method_call
439
+ self.extend some_variable_or_method_call
440
+
441
+ def something
442
+ extend A7 # We should not index this because of this dynamic nature
443
+ end
444
+
445
+ # Valid inner class syntax definition with its own modules prepended
446
+ class Qux
447
+ extend Corge
448
+ self.extend Corge
449
+ extend Baz
450
+
451
+ extend some_variable_or_method_call
452
+ end
453
+ end
454
+
455
+ class ConstantPathReferences
456
+ extend Foo::Bar
457
+ self.extend Foo::Bar2
458
+
459
+ extend dynamic::Bar
460
+ extend Foo::
461
+ end
462
+ RUBY
463
+
464
+ foo = T.must(@index["Foo"][0])
465
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
419
466
 
420
467
  qux = T.must(@index["Foo::Qux"][0])
421
- assert_equal(["Corge", "Corge", "Baz"], qux.prepended_modules)
468
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
422
469
 
423
470
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
424
- assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.prepended_modules)
471
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
425
472
  end
426
473
  end
427
474
  end