ruby-lsp 0.17.4 → 0.17.5

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