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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +241 -91
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +74 -102
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +81 -19
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +50 -2
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
- data/lib/ruby_indexer/test/index_test.rb +326 -27
- data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
- data/lib/ruby_indexer/test/method_test.rb +54 -24
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +27 -2
- data/lib/ruby_lsp/document.rb +37 -8
- data/lib/ruby_lsp/global_state.rb +7 -3
- data/lib/ruby_lsp/internal.rb +1 -0
- data/lib/ruby_lsp/listeners/completion.rb +53 -14
- data/lib/ruby_lsp/listeners/definition.rb +11 -7
- data/lib/ruby_lsp/listeners/hover.rb +14 -7
- data/lib/ruby_lsp/listeners/signature_help.rb +5 -2
- data/lib/ruby_lsp/node_context.rb +6 -1
- data/lib/ruby_lsp/requests/completion.rb +5 -4
- data/lib/ruby_lsp/requests/completion_resolve.rb +8 -0
- data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +88 -0
- data/lib/ruby_lsp/requests/support/common.rb +19 -1
- data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +12 -4
- data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +91 -0
- data/lib/ruby_lsp/requests/workspace_symbol.rb +1 -21
- data/lib/ruby_lsp/requests.rb +2 -0
- data/lib/ruby_lsp/server.rb +54 -15
- data/lib/ruby_lsp/test_helper.rb +1 -1
- data/lib/ruby_lsp/type_inferrer.rb +86 -0
- 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
|
-
|
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,
|
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
|
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
|
-
|
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
|
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
|
-
|
137
|
-
candidates.
|
138
|
-
|
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
|
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.
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
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
|
553
|
+
return if nesting_parts.empty?
|
513
554
|
|
514
|
-
namespace_entries = resolve(
|
555
|
+
namespace_entries = resolve(nesting_parts.join("::"), [], seen_names)
|
515
556
|
return unless namespace_entries
|
516
557
|
|
517
|
-
ancestors =
|
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
|
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
|
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
|