ruby-lsp 0.17.3 → 0.17.4

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