ruby-lsp 0.17.3 → 0.17.4

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +241 -91
  4. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +74 -102
  5. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +81 -19
  6. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +50 -2
  7. data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
  8. data/lib/ruby_indexer/test/index_test.rb +326 -27
  9. data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
  10. data/lib/ruby_indexer/test/method_test.rb +54 -24
  11. data/lib/ruby_indexer/test/rbs_indexer_test.rb +27 -2
  12. data/lib/ruby_lsp/document.rb +37 -8
  13. data/lib/ruby_lsp/global_state.rb +7 -3
  14. data/lib/ruby_lsp/internal.rb +1 -0
  15. data/lib/ruby_lsp/listeners/completion.rb +53 -14
  16. data/lib/ruby_lsp/listeners/definition.rb +11 -7
  17. data/lib/ruby_lsp/listeners/hover.rb +14 -7
  18. data/lib/ruby_lsp/listeners/signature_help.rb +5 -2
  19. data/lib/ruby_lsp/node_context.rb +6 -1
  20. data/lib/ruby_lsp/requests/completion.rb +5 -4
  21. data/lib/ruby_lsp/requests/completion_resolve.rb +8 -0
  22. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +88 -0
  23. data/lib/ruby_lsp/requests/support/common.rb +19 -1
  24. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +12 -4
  25. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +91 -0
  26. data/lib/ruby_lsp/requests/workspace_symbol.rb +1 -21
  27. data/lib/ruby_lsp/requests.rb +2 -0
  28. data/lib/ruby_lsp/server.rb +54 -15
  29. data/lib/ruby_lsp/test_helper.rb +1 -1
  30. data/lib/ruby_lsp/type_inferrer.rb +86 -0
  31. metadata +5 -2
@@ -152,8 +152,7 @@ module RubyIndexer
152
152
  end
153
153
  def initialize(nesting, file_path, location, comments, parent_class)
154
154
  super(nesting, file_path, location, comments)
155
-
156
- @parent_class = T.let(parent_class, T.nilable(String))
155
+ @parent_class = parent_class
157
156
  end
158
157
 
159
158
  sig { override.returns(Integer) }
@@ -162,6 +161,22 @@ module RubyIndexer
162
161
  end
163
162
  end
164
163
 
164
+ class SingletonClass < Class
165
+ extend T::Sig
166
+
167
+ sig { params(location: Prism::Location, comments: T::Array[String]).void }
168
+ def update_singleton_information(location, comments)
169
+ # Create a new RubyIndexer::Location object from the Prism location
170
+ @location = Location.new(
171
+ location.start_line,
172
+ location.end_line,
173
+ location.start_column,
174
+ location.end_column,
175
+ )
176
+ @comments.concat(comments)
177
+ end
178
+ end
179
+
165
180
  class Constant < Entry
166
181
  end
167
182
 
@@ -190,6 +205,10 @@ module RubyIndexer
190
205
 
191
206
  # An optional method parameter, e.g. `def foo(a = 123)`
192
207
  class OptionalParameter < Parameter
208
+ sig { override.returns(Symbol) }
209
+ def decorated_name
210
+ :"#{@name} = <default>"
211
+ end
193
212
  end
194
213
 
195
214
  # An required keyword method parameter, e.g. `def foo(a:)`
@@ -204,7 +223,7 @@ module RubyIndexer
204
223
  class OptionalKeywordParameter < Parameter
205
224
  sig { override.returns(Symbol) }
206
225
  def decorated_name
207
- :"#{@name}:"
226
+ :"#{@name}: <default>"
208
227
  end
209
228
  end
210
229
 
@@ -265,6 +284,12 @@ module RubyIndexer
265
284
 
266
285
  sig { abstract.returns(T::Array[Parameter]) }
267
286
  def parameters; end
287
+
288
+ # Returns a string with the decorated names of the parameters of this member. E.g.: `(a, b = 1, c: 2)`
289
+ sig { returns(String) }
290
+ def decorated_parameters
291
+ "(#{parameters.map(&:decorated_name).join(", ")})"
292
+ end
268
293
  end
269
294
 
270
295
  class Accessor < Member
@@ -280,9 +305,6 @@ module RubyIndexer
280
305
 
281
306
  class Method < Member
282
307
  extend T::Sig
283
- extend T::Helpers
284
-
285
- abstract!
286
308
 
287
309
  sig { override.returns(T::Array[Parameter]) }
288
310
  attr_reader :parameters
@@ -293,110 +315,17 @@ module RubyIndexer
293
315
  file_path: String,
294
316
  location: T.any(Prism::Location, RubyIndexer::Location),
295
317
  comments: T::Array[String],
296
- parameters_node: T.nilable(Prism::ParametersNode),
318
+ parameters: T::Array[Parameter],
297
319
  visibility: Visibility,
298
320
  owner: T.nilable(Entry::Namespace),
299
321
  ).void
300
322
  end
301
- def initialize(name, file_path, location, comments, parameters_node, visibility, owner) # rubocop:disable Metrics/ParameterLists
323
+ def initialize(name, file_path, location, comments, parameters, visibility, owner) # rubocop:disable Metrics/ParameterLists
302
324
  super(name, file_path, location, comments, visibility, owner)
303
-
304
- @parameters = T.let(list_params(parameters_node), T::Array[Parameter])
305
- end
306
-
307
- private
308
-
309
- sig { params(parameters_node: T.nilable(Prism::ParametersNode)).returns(T::Array[Parameter]) }
310
- def list_params(parameters_node)
311
- return [] unless parameters_node
312
-
313
- parameters = []
314
-
315
- parameters_node.requireds.each do |required|
316
- name = parameter_name(required)
317
- next unless name
318
-
319
- parameters << RequiredParameter.new(name: name)
320
- end
321
-
322
- parameters_node.optionals.each do |optional|
323
- name = parameter_name(optional)
324
- next unless name
325
-
326
- parameters << OptionalParameter.new(name: name)
327
- end
328
-
329
- parameters_node.keywords.each do |keyword|
330
- name = parameter_name(keyword)
331
- next unless name
332
-
333
- case keyword
334
- when Prism::RequiredKeywordParameterNode
335
- parameters << KeywordParameter.new(name: name)
336
- when Prism::OptionalKeywordParameterNode
337
- parameters << OptionalKeywordParameter.new(name: name)
338
- end
339
- end
340
-
341
- rest = parameters_node.rest
342
-
343
- if rest.is_a?(Prism::RestParameterNode)
344
- rest_name = rest.name || RestParameter::DEFAULT_NAME
345
- parameters << RestParameter.new(name: rest_name)
346
- end
347
-
348
- keyword_rest = parameters_node.keyword_rest
349
-
350
- if keyword_rest.is_a?(Prism::KeywordRestParameterNode)
351
- keyword_rest_name = parameter_name(keyword_rest) || KeywordRestParameter::DEFAULT_NAME
352
- parameters << KeywordRestParameter.new(name: keyword_rest_name)
353
- end
354
-
355
- parameters_node.posts.each do |post|
356
- name = parameter_name(post)
357
- next unless name
358
-
359
- parameters << RequiredParameter.new(name: name)
360
- end
361
-
362
- block = parameters_node.block
363
- parameters << BlockParameter.new(name: block.name || BlockParameter::DEFAULT_NAME) if block
364
-
365
- parameters
366
- end
367
-
368
- sig { params(node: T.nilable(Prism::Node)).returns(T.nilable(Symbol)) }
369
- def parameter_name(node)
370
- case node
371
- when Prism::RequiredParameterNode, Prism::OptionalParameterNode,
372
- Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
373
- Prism::RestParameterNode, Prism::KeywordRestParameterNode
374
- node.name
375
- when Prism::MultiTargetNode
376
- names = node.lefts.map { |parameter_node| parameter_name(parameter_node) }
377
-
378
- rest = node.rest
379
- if rest.is_a?(Prism::SplatNode)
380
- name = rest.expression&.slice
381
- names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym)
382
- end
383
-
384
- names << nil if rest.is_a?(Prism::ImplicitRestNode)
385
-
386
- names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) })
387
-
388
- names_with_commas = names.join(", ")
389
- :"(#{names_with_commas})"
390
- end
325
+ @parameters = parameters
391
326
  end
392
327
  end
393
328
 
394
- class SingletonMethod < Method
395
- end
396
-
397
- class InstanceMethod < Method
398
- end
399
-
400
329
  # An UnresolvedAlias points to a constant alias with a right hand side that has not yet been resolved. For
401
330
  # example, if we find
402
331
  #
@@ -470,6 +399,9 @@ module RubyIndexer
470
399
  end
471
400
  end
472
401
 
402
+ # An unresolved method alias is an alias entry for which we aren't sure what the right hand side points to yet. For
403
+ # example, if we have `alias a b`, we create an unresolved alias for `a` because we aren't sure immediate what `b`
404
+ # is referring to
473
405
  class UnresolvedMethodAlias < Entry
474
406
  extend T::Sig
475
407
 
@@ -497,5 +429,45 @@ module RubyIndexer
497
429
  @owner = owner
498
430
  end
499
431
  end
432
+
433
+ # A method alias is a resolved alias entry that points to the exact method target it refers to
434
+ class MethodAlias < Entry
435
+ extend T::Sig
436
+
437
+ sig { returns(T.any(Member, MethodAlias)) }
438
+ attr_reader :target
439
+
440
+ sig { params(target: T.any(Member, MethodAlias), unresolved_alias: UnresolvedMethodAlias).void }
441
+ def initialize(target, unresolved_alias)
442
+ full_comments = ["Alias for #{target.name}\n"]
443
+ full_comments.concat(unresolved_alias.comments)
444
+ full_comments << "\n"
445
+ full_comments.concat(target.comments)
446
+
447
+ super(
448
+ unresolved_alias.new_name,
449
+ unresolved_alias.file_path,
450
+ unresolved_alias.location,
451
+ full_comments,
452
+ )
453
+
454
+ @target = target
455
+ end
456
+
457
+ sig { returns(T.nilable(Entry::Namespace)) }
458
+ def owner
459
+ @target.owner
460
+ end
461
+
462
+ sig { returns(T::Array[Parameter]) }
463
+ def parameters
464
+ @target.parameters
465
+ end
466
+
467
+ sig { returns(String) }
468
+ def decorated_parameters
469
+ @target.decorated_parameters
470
+ end
471
+ end
500
472
  end
501
473
  end
@@ -65,13 +65,13 @@ module RubyIndexer
65
65
  @require_paths_tree.delete(require_path) if require_path
66
66
  end
67
67
 
68
- sig { params(entry: Entry).void }
69
- def <<(entry)
68
+ sig { params(entry: Entry, skip_prefix_tree: T::Boolean).void }
69
+ def add(entry, skip_prefix_tree: false)
70
70
  name = entry.name
71
71
 
72
72
  (@entries[name] ||= []) << entry
73
73
  (@files_to_entries[entry.file_path] ||= []) << entry
74
- @entries_tree.insert(name, T.must(@entries[name]))
74
+ @entries_tree.insert(name, T.must(@entries[name])) unless skip_prefix_tree
75
75
  end
76
76
 
77
77
  sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) }
@@ -118,11 +118,21 @@ module RubyIndexer
118
118
  # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
119
119
  sig { params(query: T.nilable(String)).returns(T::Array[Entry]) }
120
120
  def fuzzy_search(query)
121
- return @entries.flat_map { |_name, entries| entries } unless query
121
+ unless query
122
+ entries = @entries.filter_map do |_name, entries|
123
+ next if entries.first.is_a?(Entry::SingletonClass)
124
+
125
+ entries
126
+ end
127
+
128
+ return entries.flatten
129
+ end
122
130
 
123
131
  normalized_query = query.gsub("::", "").downcase
124
132
 
125
133
  results = @entries.filter_map do |name, entries|
134
+ next if entries.first.is_a?(Entry::SingletonClass)
135
+
126
136
  similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
127
137
  [entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
128
138
  end
@@ -130,14 +140,27 @@ module RubyIndexer
130
140
  results.flat_map(&:first)
131
141
  end
132
142
 
133
- sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) }
143
+ sig do
144
+ params(
145
+ name: T.nilable(String),
146
+ receiver_name: String,
147
+ ).returns(T::Array[T.any(Entry::Member, Entry::MethodAlias)])
148
+ end
134
149
  def method_completion_candidates(name, receiver_name)
135
150
  ancestors = linearized_ancestors_of(receiver_name)
136
- candidates = prefix_search(name).flatten
137
- candidates.select! do |entry|
138
- entry.is_a?(RubyIndexer::Entry::Member) && ancestors.any?(entry.owner&.name)
151
+
152
+ candidates = name ? prefix_search(name).flatten : @entries.values.flatten
153
+ candidates.filter_map do |entry|
154
+ case entry
155
+ when Entry::Member, Entry::MethodAlias
156
+ entry if ancestors.any?(entry.owner&.name)
157
+ when Entry::UnresolvedMethodAlias
158
+ if ancestors.any?(entry.owner&.name)
159
+ resolved_alias = resolve_method_alias(entry, receiver_name)
160
+ resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
161
+ end
162
+ end
139
163
  end
140
- candidates
141
164
  end
142
165
 
143
166
  # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
@@ -223,6 +246,12 @@ module RubyIndexer
223
246
  rescue Errno::EISDIR, Errno::ENOENT
224
247
  # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
225
248
  # it
249
+ rescue SystemStackError => e
250
+ if e.backtrace&.first&.include?("prism")
251
+ $stderr.puts "Prism error indexing #{indexable_path.full_path}: #{e.message}"
252
+ else
253
+ raise
254
+ end
226
255
  end
227
256
 
228
257
  # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
@@ -269,20 +298,32 @@ module RubyIndexer
269
298
 
270
299
  # Attempts to find methods for a resolved fully qualified receiver name.
271
300
  # Returns `nil` if the method does not exist on that receiver
272
- sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
301
+ sig do
302
+ params(
303
+ method_name: String,
304
+ receiver_name: String,
305
+ ).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)]))
306
+ end
273
307
  def resolve_method(method_name, receiver_name)
274
308
  method_entries = self[method_name]
275
- ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
276
309
  return unless method_entries
277
310
 
311
+ ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
278
312
  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
313
+ found = method_entries.filter_map do |entry|
314
+ case entry
315
+ when Entry::Member, Entry::MethodAlias
316
+ entry if entry.owner&.name == ancestor
317
+ when Entry::UnresolvedMethodAlias
318
+ # Resolve aliases lazily as we find them
319
+ if entry.owner&.name == ancestor
320
+ resolved_alias = resolve_method_alias(entry, receiver_name)
321
+ resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
322
+ end
323
+ end
283
324
  end
284
325
 
285
- return T.cast(found, T::Array[Entry::Member]) if found.any?
326
+ return found if found.any?
286
327
  end
287
328
 
288
329
  nil
@@ -509,12 +550,12 @@ module RubyIndexer
509
550
  end
510
551
  def lookup_ancestor_chain(name, nesting, seen_names)
511
552
  *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
512
- return if T.must(nesting_parts).empty?
553
+ return if nesting_parts.empty?
513
554
 
514
- namespace_entries = resolve(T.must(nesting_parts).join("::"), [], seen_names)
555
+ namespace_entries = resolve(nesting_parts.join("::"), [], seen_names)
515
556
  return unless namespace_entries
516
557
 
517
- ancestors = T.must(nesting_parts).empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
558
+ ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
518
559
 
519
560
  ancestors.each do |ancestor_name|
520
561
  entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
@@ -567,5 +608,26 @@ module RubyIndexer
567
608
  def search_top_level(name, seen_names)
568
609
  @entries[name]&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e }
569
610
  end
611
+
612
+ # Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to
613
+ # identify the target or the same unresolved alias entry if we couldn't
614
+ sig do
615
+ params(
616
+ entry: Entry::UnresolvedMethodAlias,
617
+ receiver_name: String,
618
+ ).returns(T.any(Entry::MethodAlias, Entry::UnresolvedMethodAlias))
619
+ end
620
+ def resolve_method_alias(entry, receiver_name)
621
+ return entry if entry.new_name == entry.old_name
622
+
623
+ target_method_entries = resolve_method(entry.old_name, receiver_name)
624
+ return entry unless target_method_entries
625
+
626
+ resolved_alias = Entry::MethodAlias.new(T.must(target_method_entries.first), entry)
627
+ original_entries = T.must(@entries[entry.new_name])
628
+ original_entries.delete(entry)
629
+ original_entries << resolved_alias
630
+ resolved_alias
631
+ end
570
632
  end
571
633
  end
@@ -50,7 +50,12 @@ module RubyIndexer
50
50
  parent_class = declaration.super_class&.name&.name&.to_s
51
51
  class_entry = Entry::Class.new(nesting, file_path, location, comments, parent_class)
52
52
  add_declaration_mixins_to_entry(declaration, class_entry)
53
- @index << class_entry
53
+ @index.add(class_entry)
54
+ declaration.members.each do |member|
55
+ next unless member.is_a?(RBS::AST::Members::MethodDefinition)
56
+
57
+ handle_method(member, class_entry)
58
+ end
54
59
  end
55
60
 
56
61
  sig { params(declaration: RBS::AST::Declarations::Module, pathname: Pathname).void }
@@ -61,7 +66,12 @@ module RubyIndexer
61
66
  comments = Array(declaration.comment&.string)
62
67
  module_entry = Entry::Module.new(nesting, file_path, location, comments)
63
68
  add_declaration_mixins_to_entry(declaration, module_entry)
64
- @index << module_entry
69
+ @index.add(module_entry)
70
+ declaration.members.each do |member|
71
+ next unless member.is_a?(RBS::AST::Members::MethodDefinition)
72
+
73
+ handle_method(member, module_entry)
74
+ end
65
75
  end
66
76
 
67
77
  sig { params(rbs_location: RBS::Location).returns(RubyIndexer::Location) }
@@ -95,5 +105,43 @@ module RubyIndexer
95
105
  entry.mixin_operations << mixin_operation if mixin_operation
96
106
  end
97
107
  end
108
+
109
+ sig { params(member: RBS::AST::Members::MethodDefinition, owner: Entry::Namespace).void }
110
+ def handle_method(member, owner)
111
+ name = member.name.name
112
+ file_path = member.location.buffer.name
113
+ location = to_ruby_indexer_location(member.location)
114
+ comments = Array(member.comment&.string)
115
+
116
+ visibility = case member.visibility
117
+ when :private
118
+ Entry::Visibility::PRIVATE
119
+ when :protected
120
+ Entry::Visibility::PROTECTED
121
+ else
122
+ Entry::Visibility::PUBLIC
123
+ end
124
+
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
145
+ end
98
146
  end
99
147
  end
@@ -470,5 +470,51 @@ module RubyIndexer
470
470
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
471
471
  assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
472
472
  end
473
+
474
+ def test_tracking_singleton_classes
475
+ index(<<~RUBY)
476
+ class Foo; end
477
+ class Foo
478
+ # Some extra comments
479
+ class << self
480
+ end
481
+ end
482
+ RUBY
483
+
484
+ foo = T.must(@index["Foo::<Class:Foo>"].first)
485
+ assert_equal(4, foo.location.start_line)
486
+ assert_equal("Some extra comments", foo.comments.join("\n"))
487
+ end
488
+
489
+ def test_dynamic_singleton_class_blocks
490
+ index(<<~RUBY)
491
+ class Foo
492
+ # Some extra comments
493
+ class << bar
494
+ end
495
+ end
496
+ RUBY
497
+
498
+ singleton = T.must(@index["Foo::<Class:bar>"].first)
499
+
500
+ # Even though this is not correct, we consider any dynamic singleton class block as a regular singleton class.
501
+ # That pattern cannot be properly analyzed statically and assuming that it's always a regular singleton simplifies
502
+ # the implementation considerably.
503
+ assert_equal(3, singleton.location.start_line)
504
+ assert_equal("Some extra comments", singleton.comments.join("\n"))
505
+ end
506
+
507
+ def test_namespaces_inside_singleton_blocks
508
+ index(<<~RUBY)
509
+ class Foo
510
+ class << self
511
+ class Bar
512
+ end
513
+ end
514
+ end
515
+ RUBY
516
+
517
+ assert_entry("Foo::<Class:Foo>::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7")
518
+ end
473
519
  end
474
520
  end