ruby-lsp 0.17.2 → 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/README.md +2 -0
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +280 -74
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +102 -102
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +234 -56
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +147 -0
- data/lib/ruby_indexer/ruby_indexer.rb +1 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +49 -2
- data/lib/ruby_indexer/test/configuration_test.rb +1 -1
- data/lib/ruby_indexer/test/constant_test.rb +1 -1
- data/lib/ruby_indexer/test/index_test.rb +702 -71
- data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
- data/lib/ruby_indexer/test/method_test.rb +74 -24
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +67 -0
- data/lib/ruby_indexer/test/test_case.rb +7 -0
- data/lib/ruby_lsp/document.rb +37 -8
- data/lib/ruby_lsp/global_state.rb +43 -18
- data/lib/ruby_lsp/internal.rb +2 -0
- data/lib/ruby_lsp/listeners/code_lens.rb +2 -2
- 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 -4
- data/lib/ruby_lsp/test_helper.rb +1 -1
- data/lib/ruby_lsp/type_inferrer.rb +86 -0
- metadata +29 -4
@@ -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
|
#
|
@@ -469,5 +398,76 @@ module RubyIndexer
|
|
469
398
|
@owner = owner
|
470
399
|
end
|
471
400
|
end
|
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
|
405
|
+
class UnresolvedMethodAlias < Entry
|
406
|
+
extend T::Sig
|
407
|
+
|
408
|
+
sig { returns(String) }
|
409
|
+
attr_reader :new_name, :old_name
|
410
|
+
|
411
|
+
sig { returns(T.nilable(Entry::Namespace)) }
|
412
|
+
attr_reader :owner
|
413
|
+
|
414
|
+
sig do
|
415
|
+
params(
|
416
|
+
new_name: String,
|
417
|
+
old_name: String,
|
418
|
+
owner: T.nilable(Entry::Namespace),
|
419
|
+
file_path: String,
|
420
|
+
location: Prism::Location,
|
421
|
+
comments: T::Array[String],
|
422
|
+
).void
|
423
|
+
end
|
424
|
+
def initialize(new_name, old_name, owner, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
|
425
|
+
super(new_name, file_path, location, comments)
|
426
|
+
|
427
|
+
@new_name = new_name
|
428
|
+
@old_name = old_name
|
429
|
+
@owner = owner
|
430
|
+
end
|
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
|
472
472
|
end
|
473
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,45 +140,71 @@ 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
|
-
#
|
144
|
-
#
|
145
|
-
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
|
149
|
-
|
166
|
+
# Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
|
167
|
+
# documentation:
|
168
|
+
#
|
169
|
+
# name: the name of the reference how it was found in the source code (qualified or not)
|
170
|
+
# nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"])
|
171
|
+
# seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when
|
172
|
+
# resolving circular references
|
173
|
+
sig do
|
174
|
+
params(
|
175
|
+
name: String,
|
176
|
+
nesting: T::Array[String],
|
177
|
+
seen_names: T::Array[String],
|
178
|
+
).returns(T.nilable(T::Array[Entry]))
|
179
|
+
end
|
180
|
+
def resolve(name, nesting, seen_names = [])
|
181
|
+
# If we have a top level reference, then we just search for it straight away ignoring the nesting
|
150
182
|
if name.start_with?("::")
|
151
|
-
|
152
|
-
|
153
|
-
return results&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e }
|
183
|
+
entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names)
|
184
|
+
return entries if entries
|
154
185
|
end
|
155
186
|
|
156
|
-
|
157
|
-
|
158
|
-
full_name = namespace.empty? ? name : "#{namespace}::#{name}"
|
187
|
+
# Non qualified reference path
|
188
|
+
full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name
|
159
189
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
# the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
|
165
|
-
# `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
|
166
|
-
# `RubyLsp::Interface` part is an alias, that has to be resolved
|
167
|
-
entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
|
168
|
-
return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries
|
169
|
-
end
|
190
|
+
# When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the
|
191
|
+
# constant. First, it will try to find the constant in the exact namespace where the reference was found
|
192
|
+
entries = direct_or_aliased_constant(full_name, seen_names)
|
193
|
+
return entries if entries
|
170
194
|
|
171
|
-
|
195
|
+
# If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes,
|
196
|
+
# unwrapping each level one by one. Important note: the top level is not included because that's the fallback of
|
197
|
+
# the algorithm after every other possibility has been exhausted
|
198
|
+
entries = lookup_enclosing_scopes(name, nesting, seen_names)
|
199
|
+
return entries if entries
|
200
|
+
|
201
|
+
# If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the
|
202
|
+
# specific namespace where the reference was found
|
203
|
+
entries = lookup_ancestor_chain(name, nesting, seen_names)
|
204
|
+
return entries if entries
|
205
|
+
|
206
|
+
# Finally, as a fallback, Ruby will search for the constant in the top level namespace
|
207
|
+
search_top_level(name, seen_names)
|
172
208
|
rescue UnresolvableAliasError
|
173
209
|
nil
|
174
210
|
end
|
@@ -210,6 +246,12 @@ module RubyIndexer
|
|
210
246
|
rescue Errno::EISDIR, Errno::ENOENT
|
211
247
|
# If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
|
212
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
|
213
255
|
end
|
214
256
|
|
215
257
|
# Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
|
@@ -222,8 +264,8 @@ module RubyIndexer
|
|
222
264
|
# If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
|
223
265
|
# `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
|
224
266
|
# aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
|
225
|
-
sig { params(name: String).returns(String) }
|
226
|
-
def follow_aliased_namespace(name)
|
267
|
+
sig { params(name: String, seen_names: T::Array[String]).returns(String) }
|
268
|
+
def follow_aliased_namespace(name, seen_names = [])
|
227
269
|
return name if @entries[name]
|
228
270
|
|
229
271
|
parts = name.split("::")
|
@@ -236,16 +278,16 @@ module RubyIndexer
|
|
236
278
|
case entry
|
237
279
|
when Entry::Alias
|
238
280
|
target = entry.target
|
239
|
-
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
|
281
|
+
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
|
240
282
|
when Entry::UnresolvedAlias
|
241
|
-
resolved = resolve_alias(entry)
|
283
|
+
resolved = resolve_alias(entry, seen_names)
|
242
284
|
|
243
285
|
if resolved.is_a?(Entry::UnresolvedAlias)
|
244
286
|
raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
|
245
287
|
end
|
246
288
|
|
247
289
|
target = resolved.target
|
248
|
-
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
|
290
|
+
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
|
249
291
|
else
|
250
292
|
real_parts.unshift(T.must(parts[i]))
|
251
293
|
end
|
@@ -256,20 +298,32 @@ module RubyIndexer
|
|
256
298
|
|
257
299
|
# Attempts to find methods for a resolved fully qualified receiver name.
|
258
300
|
# Returns `nil` if the method does not exist on that receiver
|
259
|
-
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
|
260
307
|
def resolve_method(method_name, receiver_name)
|
261
308
|
method_entries = self[method_name]
|
262
|
-
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
|
263
309
|
return unless method_entries
|
264
310
|
|
311
|
+
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
|
265
312
|
ancestors.each do |ancestor|
|
266
|
-
found = method_entries.
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
270
324
|
end
|
271
325
|
|
272
|
-
return
|
326
|
+
return found if found.any?
|
273
327
|
end
|
274
328
|
|
275
329
|
nil
|
@@ -291,6 +345,10 @@ module RubyIndexer
|
|
291
345
|
cached_ancestors = @ancestors[fully_qualified_name]
|
292
346
|
return cached_ancestors if cached_ancestors
|
293
347
|
|
348
|
+
# If we don't have an entry for `name`, raise
|
349
|
+
entries = self[fully_qualified_name]
|
350
|
+
raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
|
351
|
+
|
294
352
|
ancestors = [fully_qualified_name]
|
295
353
|
|
296
354
|
# Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
|
@@ -298,10 +356,6 @@ module RubyIndexer
|
|
298
356
|
# the cache will reflect the final result
|
299
357
|
@ancestors[fully_qualified_name] = ancestors
|
300
358
|
|
301
|
-
# If we don't have an entry for `name`, raise
|
302
|
-
entries = resolve(fully_qualified_name, [])
|
303
|
-
raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
|
304
|
-
|
305
359
|
# If none of the entries for `name` are namespaces, raise
|
306
360
|
namespaces = entries.filter_map do |entry|
|
307
361
|
case entry
|
@@ -395,8 +449,8 @@ module RubyIndexer
|
|
395
449
|
entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
|
396
450
|
ancestors = linearized_ancestors_of(owner_name)
|
397
451
|
|
398
|
-
variables = entries.
|
399
|
-
variables.
|
452
|
+
variables = entries.select { |e| ancestors.any?(e.owner&.name) }
|
453
|
+
variables.uniq!(&:name)
|
400
454
|
variables
|
401
455
|
end
|
402
456
|
|
@@ -434,22 +488,146 @@ module RubyIndexer
|
|
434
488
|
|
435
489
|
# Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
|
436
490
|
# that doesn't exist, then we return the same UnresolvedAlias
|
437
|
-
sig
|
438
|
-
|
439
|
-
|
491
|
+
sig do
|
492
|
+
params(
|
493
|
+
entry: Entry::UnresolvedAlias,
|
494
|
+
seen_names: T::Array[String],
|
495
|
+
).returns(T.any(Entry::Alias, Entry::UnresolvedAlias))
|
496
|
+
end
|
497
|
+
def resolve_alias(entry, seen_names)
|
498
|
+
alias_name = entry.name
|
499
|
+
return entry if seen_names.include?(alias_name)
|
500
|
+
|
501
|
+
seen_names << alias_name
|
502
|
+
|
503
|
+
target = resolve(entry.target, entry.nesting, seen_names)
|
440
504
|
return entry unless target
|
441
505
|
|
442
506
|
target_name = T.must(target.first).name
|
443
507
|
resolved_alias = Entry::Alias.new(target_name, entry)
|
444
508
|
|
445
509
|
# Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
|
446
|
-
original_entries = T.must(@entries[
|
510
|
+
original_entries = T.must(@entries[alias_name])
|
447
511
|
original_entries.delete(entry)
|
448
512
|
original_entries << resolved_alias
|
449
513
|
|
450
|
-
@entries_tree.insert(
|
514
|
+
@entries_tree.insert(alias_name, original_entries)
|
451
515
|
|
452
516
|
resolved_alias
|
453
517
|
end
|
518
|
+
|
519
|
+
sig do
|
520
|
+
params(
|
521
|
+
name: String,
|
522
|
+
nesting: T::Array[String],
|
523
|
+
seen_names: T::Array[String],
|
524
|
+
).returns(T.nilable(T::Array[Entry]))
|
525
|
+
end
|
526
|
+
def lookup_enclosing_scopes(name, nesting, seen_names)
|
527
|
+
nesting.length.downto(1).each do |i|
|
528
|
+
namespace = T.must(nesting[0...i]).join("::")
|
529
|
+
|
530
|
+
# If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
|
531
|
+
# because the user might be trying to jump to the alias definition.
|
532
|
+
#
|
533
|
+
# However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
|
534
|
+
# the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
|
535
|
+
# `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
|
536
|
+
# `RubyLsp::Interface` part is an alias, that has to be resolved
|
537
|
+
entries = direct_or_aliased_constant("#{namespace}::#{name}", seen_names)
|
538
|
+
return entries if entries
|
539
|
+
end
|
540
|
+
|
541
|
+
nil
|
542
|
+
end
|
543
|
+
|
544
|
+
sig do
|
545
|
+
params(
|
546
|
+
name: String,
|
547
|
+
nesting: T::Array[String],
|
548
|
+
seen_names: T::Array[String],
|
549
|
+
).returns(T.nilable(T::Array[Entry]))
|
550
|
+
end
|
551
|
+
def lookup_ancestor_chain(name, nesting, seen_names)
|
552
|
+
*nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
|
553
|
+
return if nesting_parts.empty?
|
554
|
+
|
555
|
+
namespace_entries = resolve(nesting_parts.join("::"), [], seen_names)
|
556
|
+
return unless namespace_entries
|
557
|
+
|
558
|
+
ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name)
|
559
|
+
|
560
|
+
ancestors.each do |ancestor_name|
|
561
|
+
entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
|
562
|
+
return entries if entries
|
563
|
+
end
|
564
|
+
|
565
|
+
nil
|
566
|
+
rescue NonExistingNamespaceError
|
567
|
+
nil
|
568
|
+
end
|
569
|
+
|
570
|
+
# Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
|
571
|
+
# of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
|
572
|
+
# `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
|
573
|
+
# nesting
|
574
|
+
sig { params(name: String, nesting: T::Array[String]).returns(String) }
|
575
|
+
def build_non_redundant_full_name(name, nesting)
|
576
|
+
return name if nesting.empty?
|
577
|
+
|
578
|
+
namespace = nesting.join("::")
|
579
|
+
|
580
|
+
# If the name is not qualified, we can just concatenate the nesting and the name
|
581
|
+
return "#{namespace}::#{name}" unless name.include?("::")
|
582
|
+
|
583
|
+
name_parts = name.split("::")
|
584
|
+
|
585
|
+
# Find the first part of the name that is not in the nesting
|
586
|
+
index = name_parts.index { |part| !nesting.include?(part) }
|
587
|
+
|
588
|
+
if index.nil?
|
589
|
+
# All parts of the nesting are redundant because they are already present in the name. We can return the name
|
590
|
+
# directly
|
591
|
+
name
|
592
|
+
elsif index == 0
|
593
|
+
# No parts of the nesting are in the name, we can concatenate the namespace and the name
|
594
|
+
"#{namespace}::#{name}"
|
595
|
+
else
|
596
|
+
# The name includes some parts of the nesting. We need to remove the redundant parts
|
597
|
+
"#{namespace}::#{T.must(name_parts[index..-1]).join("::")}"
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
sig { params(full_name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
|
602
|
+
def direct_or_aliased_constant(full_name, seen_names)
|
603
|
+
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
|
+
|
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 }
|
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
|
454
632
|
end
|
455
633
|
end
|