ruby-lsp 0.17.4 → 0.17.5

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.
@@ -150,17 +150,42 @@ module RubyIndexer
150
150
  ancestors = linearized_ancestors_of(receiver_name)
151
151
 
152
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
153
+ completion_items = candidates.each_with_object({}) do |entry, hash|
154
+ unless entry.is_a?(Entry::Member) || entry.is_a?(Entry::MethodAlias) ||
155
+ entry.is_a?(Entry::UnresolvedMethodAlias)
156
+ next
157
+ end
158
+
159
+ entry_name = entry.name
160
+ ancestor_index = ancestors.index(entry.owner&.name)
161
+ existing_entry, existing_entry_index = hash[entry_name]
162
+
163
+ # Conditions for matching a method completion candidate:
164
+ # 1. If an ancestor_index was found, it means that this method is owned by the receiver. The exact index is
165
+ # where in the ancestor chain the method was found. For example, if the ancestors are ["A", "B", "C"] and we
166
+ # found the method declared in `B`, then the ancestors index is 1
167
+ #
168
+ # 2. We already established that this method is owned by the receiver. Now, check if we already added a
169
+ # completion candidate for this method name. If not, then we just go and add it (the left hand side of the or)
170
+ #
171
+ # 3. If we had already found a method entry for the same name, then we need to check if the current entry that
172
+ # we are comparing appears first in the hierarchy or not. For example, imagine we have the method `open` defined
173
+ # in both `File` and its parent `IO`. If we first find the method `open` in `IO`, it will be inserted into the
174
+ # hash. Then, when we find the entry for `open` owned by `File`, we need to replace `IO.open` by `File.open`,
175
+ # since `File.open` appears first in the hierarchy chain and is therefore the correct method being invoked. The
176
+ # last part of the conditional checks if the current entry was found earlier in the hierarchy chain, in which
177
+ # case we must update the existing entry to avoid showing the wrong method declaration for overridden methods
178
+ next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
179
+
180
+ if entry.is_a?(Entry::UnresolvedMethodAlias)
181
+ resolved_alias = resolve_method_alias(entry, receiver_name)
182
+ hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
183
+ else
184
+ hash[entry_name] = [entry, ancestor_index]
162
185
  end
163
186
  end
187
+
188
+ completion_items.values.map!(&:first)
164
189
  end
165
190
 
166
191
  # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
@@ -175,7 +200,11 @@ module RubyIndexer
175
200
  name: String,
176
201
  nesting: T::Array[String],
177
202
  seen_names: T::Array[String],
178
- ).returns(T.nilable(T::Array[Entry]))
203
+ ).returns(T.nilable(T::Array[T.any(
204
+ Entry::Namespace,
205
+ Entry::Alias,
206
+ Entry::UnresolvedAlias,
207
+ )]))
179
208
  end
180
209
  def resolve(name, nesting, seen_names = [])
181
210
  # If we have a top level reference, then we just search for it straight away ignoring the nesting
@@ -204,7 +233,7 @@ module RubyIndexer
204
233
  return entries if entries
205
234
 
206
235
  # Finally, as a fallback, Ruby will search for the constant in the top level namespace
207
- search_top_level(name, seen_names)
236
+ direct_or_aliased_constant(name, seen_names)
208
237
  rescue UnresolvableAliasError
209
238
  nil
210
239
  end
@@ -345,8 +374,25 @@ module RubyIndexer
345
374
  cached_ancestors = @ancestors[fully_qualified_name]
346
375
  return cached_ancestors if cached_ancestors
347
376
 
377
+ parts = fully_qualified_name.split("::")
378
+ singleton_levels = 0
379
+
380
+ parts.reverse_each do |part|
381
+ break unless part.include?("<Class:")
382
+
383
+ singleton_levels += 1
384
+ parts.pop
385
+ end
386
+
387
+ attached_class_name = parts.join("::")
388
+
348
389
  # If we don't have an entry for `name`, raise
349
390
  entries = self[fully_qualified_name]
391
+
392
+ if singleton_levels > 0 && !entries
393
+ entries = [existing_or_new_singleton_class(attached_class_name)]
394
+ end
395
+
350
396
  raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
351
397
 
352
398
  ancestors = [fully_qualified_name]
@@ -369,62 +415,25 @@ module RubyIndexer
369
415
  raise NonExistingNamespaceError,
370
416
  "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
371
417
 
372
- mixin_operations = namespaces.flat_map(&:mixin_operations)
373
- main_namespace_index = 0
374
-
375
418
  # The original nesting where we discovered this namespace, so that we resolve the correct names of the
376
419
  # included/prepended/extended modules and parent classes
377
420
  nesting = T.must(namespaces.first).nesting
378
421
 
379
- mixin_operations.each do |operation|
380
- resolved_module = resolve(operation.module_name, nesting)
381
- next unless resolved_module
382
-
383
- module_fully_qualified_name = T.must(resolved_module.first).name
384
-
385
- case operation
386
- when Entry::Prepend
387
- # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
388
- # the actual namespace twice. However, it does not check if it has been included because you are allowed to
389
- # prepend the same module after it has already been included
390
- linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
391
-
392
- # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
393
- # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
394
- # be inserted after `"A`
395
- uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
396
- insert_position = linearized_prepends.length - uniq_prepends.length
397
-
398
- T.unsafe(ancestors).insert(
399
- insert_position,
400
- *(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
401
- )
402
-
403
- main_namespace_index += linearized_prepends.length
404
- when Entry::Include
405
- # When including a module, Ruby will always prevent duplicate entries in case the module has already been
406
- # prepended or included
407
- linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
408
- T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
422
+ if nesting.any?
423
+ singleton_levels.times do
424
+ nesting << "<Class:#{T.must(nesting.last)}>"
409
425
  end
410
426
  end
411
427
 
412
- # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
413
- # from two diffent classes in different files, we simply ignore it
414
- superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
415
-
416
- if superclass
417
- # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
418
- # error. We need to ensure that this isn't the case
419
- parent_class = T.must(superclass.parent_class)
420
-
421
- resolved_parent_class = resolve(parent_class, nesting)
422
- parent_class_name = resolved_parent_class&.first&.name
423
-
424
- if parent_class_name && fully_qualified_name != parent_class_name
425
- ancestors.concat(linearized_ancestors_of(parent_class_name))
426
- end
427
- end
428
+ linearize_mixins(ancestors, namespaces, nesting)
429
+ linearize_superclass(
430
+ ancestors,
431
+ attached_class_name,
432
+ fully_qualified_name,
433
+ namespaces,
434
+ nesting,
435
+ singleton_levels,
436
+ )
428
437
 
429
438
  ancestors
430
439
  end
@@ -484,8 +493,175 @@ module RubyIndexer
484
493
  @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
485
494
  end
486
495
 
496
+ sig { returns(T::Boolean) }
497
+ def empty?
498
+ @entries.empty?
499
+ end
500
+
501
+ sig { returns(T::Array[String]) }
502
+ def names
503
+ @entries.keys
504
+ end
505
+
506
+ sig { params(name: String).returns(T::Boolean) }
507
+ def indexed?(name)
508
+ @entries.key?(name)
509
+ end
510
+
511
+ sig { returns(Integer) }
512
+ def length
513
+ @entries.count
514
+ end
515
+
516
+ sig { params(name: String).returns(Entry::SingletonClass) }
517
+ def existing_or_new_singleton_class(name)
518
+ *_namespace, unqualified_name = name.split("::")
519
+ full_singleton_name = "#{name}::<Class:#{unqualified_name}>"
520
+ singleton = T.cast(self[full_singleton_name]&.first, T.nilable(Entry::SingletonClass))
521
+
522
+ unless singleton
523
+ attached_ancestor = T.must(self[name]&.first)
524
+
525
+ singleton = Entry::SingletonClass.new(
526
+ [full_singleton_name],
527
+ attached_ancestor.file_path,
528
+ attached_ancestor.location,
529
+ attached_ancestor.name_location,
530
+ [],
531
+ nil,
532
+ )
533
+ add(singleton, skip_prefix_tree: true)
534
+ end
535
+
536
+ singleton
537
+ end
538
+
487
539
  private
488
540
 
541
+ # Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
542
+ # linearized ancestors of the mixins
543
+ sig do
544
+ params(
545
+ ancestors: T::Array[String],
546
+ namespace_entries: T::Array[Entry::Namespace],
547
+ nesting: T::Array[String],
548
+ ).void
549
+ end
550
+ def linearize_mixins(ancestors, namespace_entries, nesting)
551
+ mixin_operations = namespace_entries.flat_map(&:mixin_operations)
552
+ main_namespace_index = 0
553
+
554
+ mixin_operations.each do |operation|
555
+ resolved_module = resolve(operation.module_name, nesting)
556
+ next unless resolved_module
557
+
558
+ module_fully_qualified_name = T.must(resolved_module.first).name
559
+
560
+ case operation
561
+ when Entry::Prepend
562
+ # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
563
+ # the actual namespace twice. However, it does not check if it has been included because you are allowed to
564
+ # prepend the same module after it has already been included
565
+ linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
566
+
567
+ # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
568
+ # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
569
+ # be inserted after `"A`
570
+ uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
571
+ insert_position = linearized_prepends.length - uniq_prepends.length
572
+
573
+ T.unsafe(ancestors).insert(
574
+ insert_position,
575
+ *(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
576
+ )
577
+
578
+ main_namespace_index += linearized_prepends.length
579
+ when Entry::Include
580
+ # When including a module, Ruby will always prevent duplicate entries in case the module has already been
581
+ # prepended or included
582
+ linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
583
+ T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
584
+ end
585
+ end
586
+ end
587
+
588
+ # Linearize the superclass of a given namespace (including modules with the implicit `Module` superclass). This
589
+ # method will mutate the `ancestors` array with the linearized ancestors of the superclass
590
+ sig do
591
+ params(
592
+ ancestors: T::Array[String],
593
+ attached_class_name: String,
594
+ fully_qualified_name: String,
595
+ namespace_entries: T::Array[Entry::Namespace],
596
+ nesting: T::Array[String],
597
+ singleton_levels: Integer,
598
+ ).void
599
+ end
600
+ def linearize_superclass( # rubocop:disable Metrics/ParameterLists
601
+ ancestors,
602
+ attached_class_name,
603
+ fully_qualified_name,
604
+ namespace_entries,
605
+ nesting,
606
+ singleton_levels
607
+ )
608
+ # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
609
+ # from two diffent classes in different files, we simply ignore it
610
+ superclass = T.cast(
611
+ if singleton_levels > 0
612
+ self[attached_class_name]&.find { |n| n.is_a?(Entry::Class) && n.parent_class }
613
+ else
614
+ namespace_entries.find { |n| n.is_a?(Entry::Class) && n.parent_class }
615
+ end,
616
+ T.nilable(Entry::Class),
617
+ )
618
+
619
+ if superclass
620
+ # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
621
+ # error. We need to ensure that this isn't the case
622
+ parent_class = T.must(superclass.parent_class)
623
+
624
+ resolved_parent_class = resolve(parent_class, nesting)
625
+ parent_class_name = resolved_parent_class&.first&.name
626
+
627
+ if parent_class_name && fully_qualified_name != parent_class_name
628
+
629
+ parent_name_parts = [parent_class_name]
630
+ singleton_levels.times do
631
+ parent_name_parts << "<Class:#{parent_name_parts.last}>"
632
+ end
633
+
634
+ ancestors.concat(linearized_ancestors_of(parent_name_parts.join("::")))
635
+ end
636
+
637
+ # When computing the linearization for a class's singleton class, it inherits from the linearized ancestors of
638
+ # the `Class` class
639
+ if parent_class_name&.start_with?("BasicObject") && singleton_levels > 0
640
+ class_class_name_parts = ["Class"]
641
+
642
+ (singleton_levels - 1).times do
643
+ class_class_name_parts << "<Class:#{class_class_name_parts.last}>"
644
+ end
645
+
646
+ ancestors.concat(linearized_ancestors_of(class_class_name_parts.join("::")))
647
+ end
648
+ elsif singleton_levels > 0
649
+ # When computing the linearization for a module's singleton class, it inherits from the linearized ancestors of
650
+ # the `Module` class
651
+ mod = T.cast(self[attached_class_name]&.find { |n| n.is_a?(Entry::Module) }, T.nilable(Entry::Module))
652
+
653
+ if mod
654
+ module_class_name_parts = ["Module"]
655
+
656
+ (singleton_levels - 1).times do
657
+ module_class_name_parts << "<Class:#{module_class_name_parts.last}>"
658
+ end
659
+
660
+ ancestors.concat(linearized_ancestors_of(module_class_name_parts.join("::")))
661
+ end
662
+ end
663
+ end
664
+
489
665
  # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
490
666
  # that doesn't exist, then we return the same UnresolvedAlias
491
667
  sig do
@@ -521,7 +697,11 @@ module RubyIndexer
521
697
  name: String,
522
698
  nesting: T::Array[String],
523
699
  seen_names: T::Array[String],
524
- ).returns(T.nilable(T::Array[Entry]))
700
+ ).returns(T.nilable(T::Array[T.any(
701
+ Entry::Namespace,
702
+ Entry::Alias,
703
+ Entry::UnresolvedAlias,
704
+ )]))
525
705
  end
526
706
  def lookup_enclosing_scopes(name, nesting, seen_names)
527
707
  nesting.length.downto(1).each do |i|
@@ -546,7 +726,11 @@ module RubyIndexer
546
726
  name: String,
547
727
  nesting: T::Array[String],
548
728
  seen_names: T::Array[String],
549
- ).returns(T.nilable(T::Array[Entry]))
729
+ ).returns(T.nilable(T::Array[T.any(
730
+ Entry::Namespace,
731
+ Entry::Alias,
732
+ Entry::UnresolvedAlias,
733
+ )]))
550
734
  end
551
735
  def lookup_ancestor_chain(name, nesting, seen_names)
552
736
  *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
@@ -598,15 +782,29 @@ module RubyIndexer
598
782
  end
599
783
  end
600
784
 
601
- sig { params(full_name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
785
+ sig do
786
+ params(
787
+ full_name: String,
788
+ seen_names: T::Array[String],
789
+ ).returns(
790
+ T.nilable(T::Array[T.any(
791
+ Entry::Namespace,
792
+ Entry::Alias,
793
+ Entry::UnresolvedAlias,
794
+ )]),
795
+ )
796
+ end
602
797
  def direct_or_aliased_constant(full_name, seen_names)
603
798
  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
799
 
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 }
800
+ T.cast(
801
+ entries&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e },
802
+ T.nilable(T::Array[T.any(
803
+ Entry::Namespace,
804
+ Entry::Alias,
805
+ Entry::UnresolvedAlias,
806
+ )]),
807
+ )
610
808
  end
611
809
 
612
810
  # Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to
@@ -48,7 +48,7 @@ module RubyIndexer
48
48
  location = to_ruby_indexer_location(declaration.location)
49
49
  comments = Array(declaration.comment&.string)
50
50
  parent_class = declaration.super_class&.name&.name&.to_s
51
- class_entry = Entry::Class.new(nesting, file_path, location, comments, parent_class)
51
+ class_entry = Entry::Class.new(nesting, file_path, location, location, comments, parent_class)
52
52
  add_declaration_mixins_to_entry(declaration, class_entry)
53
53
  @index.add(class_entry)
54
54
  declaration.members.each do |member|
@@ -64,7 +64,7 @@ module RubyIndexer
64
64
  file_path = pathname.to_s
65
65
  location = to_ruby_indexer_location(declaration.location)
66
66
  comments = Array(declaration.comment&.string)
67
- module_entry = Entry::Module.new(nesting, file_path, location, comments)
67
+ module_entry = Entry::Module.new(nesting, file_path, location, location, comments)
68
68
  add_declaration_mixins_to_entry(declaration, module_entry)
69
69
  @index.add(module_entry)
70
70
  declaration.members.each do |member|
@@ -93,16 +93,15 @@ module RubyIndexer
93
93
  def add_declaration_mixins_to_entry(declaration, entry)
94
94
  declaration.each_mixin do |mixin|
95
95
  name = mixin.name.name.to_s
96
- mixin_operation =
97
- case mixin
98
- when RBS::AST::Members::Include
99
- Entry::Include.new(name)
100
- when RBS::AST::Members::Extend
101
- Entry::Extend.new(name)
102
- when RBS::AST::Members::Prepend
103
- Entry::Prepend.new(name)
104
- end
105
- entry.mixin_operations << mixin_operation if mixin_operation
96
+ case mixin
97
+ when RBS::AST::Members::Include
98
+ entry.mixin_operations << Entry::Include.new(name)
99
+ when RBS::AST::Members::Prepend
100
+ entry.mixin_operations << Entry::Prepend.new(name)
101
+ when RBS::AST::Members::Extend
102
+ singleton = @index.existing_or_new_singleton_class(entry.name)
103
+ singleton.mixin_operations << Entry::Include.new(name)
104
+ end
106
105
  end
107
106
  end
108
107
 
@@ -122,26 +121,8 @@ module RubyIndexer
122
121
  Entry::Visibility::PUBLIC
123
122
  end
124
123
 
125
- real_owner = member.singleton? ? existing_or_new_singleton_klass(owner) : owner
126
- @index.add(Entry::Method.new(name, file_path, location, comments, [], visibility, real_owner))
127
- end
128
-
129
- sig { params(owner: Entry::Namespace).returns(T.nilable(Entry::Class)) }
130
- def existing_or_new_singleton_klass(owner)
131
- *_parts, name = owner.name.split("::")
132
-
133
- # Return the existing singleton class if available
134
- singleton_entries = T.cast(
135
- @index["#{owner.name}::<Class:#{name}>"],
136
- T.nilable(T::Array[Entry::SingletonClass]),
137
- )
138
- return singleton_entries.first if singleton_entries
139
-
140
- # If not available, create the singleton class lazily
141
- nesting = owner.nesting + ["<Class:#{name}>"]
142
- entry = Entry::SingletonClass.new(nesting, owner.file_path, owner.location, [], nil)
143
- @index.add(entry, skip_prefix_tree: true)
144
- entry
124
+ real_owner = member.singleton? ? @index.existing_or_new_singleton_class(owner.name) : owner
125
+ @index.add(Entry::Method.new(name, file_path, location, location, comments, [], visibility, real_owner))
145
126
  end
146
127
  end
147
128
  end
@@ -461,13 +461,13 @@ module RubyIndexer
461
461
  end
462
462
  RUBY
463
463
 
464
- foo = T.must(@index["Foo"][0])
464
+ foo = T.must(@index["Foo::<Class:Foo>"][0])
465
465
  assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
466
466
 
467
- qux = T.must(@index["Foo::Qux"][0])
467
+ qux = T.must(@index["Foo::Qux::<Class:Qux>"][0])
468
468
  assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
469
469
 
470
- constant_path_references = T.must(@index["ConstantPathReferences"][0])
470
+ constant_path_references = T.must(@index["ConstantPathReferences::<Class:ConstantPathReferences>"][0])
471
471
  assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
472
472
  end
473
473
 
@@ -516,5 +516,35 @@ module RubyIndexer
516
516
 
517
517
  assert_entry("Foo::<Class:Foo>::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7")
518
518
  end
519
+
520
+ def test_name_location_points_to_constant_path_location
521
+ index(<<~RUBY)
522
+ class Foo
523
+ def foo; end
524
+ end
525
+
526
+ module Bar
527
+ def bar; end
528
+ end
529
+ RUBY
530
+
531
+ foo = T.must(@index["Foo"].first)
532
+ refute_equal(foo.location, foo.name_location)
533
+
534
+ name_location = foo.name_location
535
+ assert_equal(1, name_location.start_line)
536
+ assert_equal(1, name_location.end_line)
537
+ assert_equal(6, name_location.start_column)
538
+ assert_equal(9, name_location.end_column)
539
+
540
+ bar = T.must(@index["Bar"].first)
541
+ refute_equal(bar.location, bar.name_location)
542
+
543
+ name_location = bar.name_location
544
+ assert_equal(5, name_location.start_line)
545
+ assert_equal(5, name_location.end_line)
546
+ assert_equal(7, name_location.start_column)
547
+ assert_equal(10, name_location.end_column)
548
+ end
519
549
  end
520
550
  end