ruby-lsp 0.16.7 → 0.17.3

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