ruby-lsp 0.27.0.beta2 → 0.27.0.beta3
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/exe/ruby-lsp +0 -46
- data/exe/ruby-lsp-check +0 -15
- data/lib/ruby_lsp/global_state.rb +0 -5
- data/lib/ruby_lsp/internal.rb +2 -1
- data/lib/ruby_lsp/listeners/code_lens.rb +1 -1
- data/lib/ruby_lsp/listeners/completion.rb +246 -382
- data/lib/ruby_lsp/listeners/definition.rb +6 -9
- data/lib/ruby_lsp/listeners/hover.rb +11 -9
- data/lib/ruby_lsp/listeners/signature_help.rb +11 -12
- data/lib/ruby_lsp/listeners/test_discovery.rb +17 -1
- data/lib/ruby_lsp/listeners/test_style.rb +1 -1
- data/lib/ruby_lsp/requests/completion_resolve.rb +49 -29
- data/lib/ruby_lsp/requests/references.rb +21 -7
- data/lib/ruby_lsp/requests/rename.rb +1 -1
- data/lib/ruby_lsp/requests/support/common.rb +69 -68
- data/lib/ruby_lsp/ruby_document.rb +0 -73
- data/lib/ruby_lsp/rubydex/declaration.rb +128 -2
- data/lib/ruby_lsp/rubydex/definition.rb +16 -0
- data/lib/ruby_lsp/rubydex/signature.rb +107 -0
- data/lib/ruby_lsp/scripts/compose_bundle.rb +1 -1
- data/lib/ruby_lsp/server.rb +7 -162
- data/lib/ruby_lsp/test_helper.rb +0 -1
- data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +1 -1
- data/lib/ruby_lsp/type_inferrer.rb +2 -2
- metadata +11 -14
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +0 -276
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +0 -1101
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +0 -44
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +0 -605
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +0 -1077
- data/lib/ruby_indexer/lib/ruby_indexer/location.rb +0 -37
- data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +0 -149
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +0 -294
- data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +0 -32
- data/lib/ruby_indexer/ruby_indexer.rb +0 -19
- /data/lib/{ruby_indexer/lib/ruby_indexer → ruby_lsp}/uri.rb +0 -0
|
@@ -1,1077 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
module RubyIndexer
|
|
5
|
-
class Index
|
|
6
|
-
class UnresolvableAliasError < StandardError; end
|
|
7
|
-
class NonExistingNamespaceError < StandardError; end
|
|
8
|
-
class IndexNotEmptyError < StandardError; end
|
|
9
|
-
|
|
10
|
-
# The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
|
|
11
|
-
ENTRY_SIMILARITY_THRESHOLD = 0.7
|
|
12
|
-
|
|
13
|
-
#: Configuration
|
|
14
|
-
attr_reader :configuration
|
|
15
|
-
|
|
16
|
-
#: bool
|
|
17
|
-
attr_reader :initial_indexing_completed
|
|
18
|
-
|
|
19
|
-
class << self
|
|
20
|
-
# Returns the real nesting of a constant name taking into account top level
|
|
21
|
-
# references that may be included anywhere in the name or nesting where that
|
|
22
|
-
# constant was found
|
|
23
|
-
#: (Array[String] stack, String? name) -> Array[String]
|
|
24
|
-
def actual_nesting(stack, name)
|
|
25
|
-
nesting = name ? stack + [name] : stack
|
|
26
|
-
corrected_nesting = []
|
|
27
|
-
|
|
28
|
-
nesting.reverse_each do |name|
|
|
29
|
-
corrected_nesting.prepend(name.delete_prefix("::"))
|
|
30
|
-
|
|
31
|
-
break if name.start_with?("::")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
corrected_nesting
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Returns the unresolved name for a constant reference including all parts of a constant path, or `nil` if the
|
|
38
|
-
# constant contains dynamic or incomplete parts
|
|
39
|
-
#: (Prism::Node) -> String?
|
|
40
|
-
def constant_name(node)
|
|
41
|
-
case node
|
|
42
|
-
when Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathTargetNode
|
|
43
|
-
node.full_name
|
|
44
|
-
end
|
|
45
|
-
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
|
|
46
|
-
Prism::ConstantPathNode::MissingNodesInConstantPathError
|
|
47
|
-
nil
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
#: -> void
|
|
52
|
-
def initialize
|
|
53
|
-
# Holds all entries in the index using the following format:
|
|
54
|
-
# {
|
|
55
|
-
# "Foo" => [#<Entry::Class>, #<Entry::Class>],
|
|
56
|
-
# "Foo::Bar" => [#<Entry::Class>],
|
|
57
|
-
# }
|
|
58
|
-
@entries = {} #: Hash[String, Array[Entry]]
|
|
59
|
-
|
|
60
|
-
# Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion
|
|
61
|
-
@entries_tree = PrefixTree.new #: PrefixTree[Array[Entry]]
|
|
62
|
-
|
|
63
|
-
# Holds references to where entries where discovered so that we can easily delete them
|
|
64
|
-
# {
|
|
65
|
-
# "file:///my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
|
|
66
|
-
# "file:///my/project/bar.rb" => [#<Entry::Class>],
|
|
67
|
-
# "untitled:Untitled-1" => [#<Entry::Class>],
|
|
68
|
-
# }
|
|
69
|
-
@uris_to_entries = {} #: Hash[String, Array[Entry]]
|
|
70
|
-
|
|
71
|
-
# Holds all require paths for every indexed item so that we can provide autocomplete for requires
|
|
72
|
-
@require_paths_tree = PrefixTree.new #: PrefixTree[URI::Generic]
|
|
73
|
-
|
|
74
|
-
# Holds the linearized ancestors list for every namespace
|
|
75
|
-
@ancestors = {} #: Hash[String, Array[String]]
|
|
76
|
-
|
|
77
|
-
# Map of module name to included hooks that have to be executed when we include the given module
|
|
78
|
-
@included_hooks = {} #: Hash[String, Array[^(Index index, Entry::Namespace base) -> void]]
|
|
79
|
-
|
|
80
|
-
@configuration = RubyIndexer::Configuration.new #: Configuration
|
|
81
|
-
|
|
82
|
-
@initial_indexing_completed = false #: bool
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Register an included `hook` that will be executed when `module_name` is included into any namespace
|
|
86
|
-
#: (String module_name) { (Index index, Entry::Namespace base) -> void } -> void
|
|
87
|
-
def register_included_hook(module_name, &hook)
|
|
88
|
-
(@included_hooks[module_name] ||= []) << hook
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
#: (URI::Generic uri, ?skip_require_paths_tree: bool) -> void
|
|
92
|
-
def delete(uri, skip_require_paths_tree: false)
|
|
93
|
-
key = uri.to_s
|
|
94
|
-
# For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
|
|
95
|
-
# left, delete the constant from the index.
|
|
96
|
-
@uris_to_entries[key]&.each do |entry|
|
|
97
|
-
name = entry.name
|
|
98
|
-
entries = @entries[name]
|
|
99
|
-
next unless entries
|
|
100
|
-
|
|
101
|
-
# Delete the specific entry from the list for this name
|
|
102
|
-
entries.delete(entry)
|
|
103
|
-
|
|
104
|
-
# If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update
|
|
105
|
-
# the prefix tree with the current entries
|
|
106
|
-
if entries.empty?
|
|
107
|
-
@entries.delete(name)
|
|
108
|
-
@entries_tree.delete(name)
|
|
109
|
-
else
|
|
110
|
-
@entries_tree.insert(name, entries)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
@uris_to_entries.delete(key)
|
|
115
|
-
return if skip_require_paths_tree
|
|
116
|
-
|
|
117
|
-
require_path = uri.require_path
|
|
118
|
-
@require_paths_tree.delete(require_path) if require_path
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
#: (Entry entry, ?skip_prefix_tree: bool) -> void
|
|
122
|
-
def add(entry, skip_prefix_tree: false)
|
|
123
|
-
name = entry.name
|
|
124
|
-
|
|
125
|
-
(@entries[name] ||= []) << entry
|
|
126
|
-
(@uris_to_entries[entry.uri.to_s] ||= []) << entry
|
|
127
|
-
|
|
128
|
-
unless skip_prefix_tree
|
|
129
|
-
@entries_tree.insert(
|
|
130
|
-
name,
|
|
131
|
-
@entries[name], #: as !nil
|
|
132
|
-
)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
#: (String fully_qualified_name) -> Array[Entry]?
|
|
137
|
-
def [](fully_qualified_name)
|
|
138
|
-
@entries[fully_qualified_name.delete_prefix("::")]
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
#: (String query) -> Array[URI::Generic]
|
|
142
|
-
def search_require_paths(query)
|
|
143
|
-
@require_paths_tree.search(query)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Searches for a constant based on an unqualified name and returns the first possible match regardless of whether
|
|
147
|
-
# there are more possible matching entries
|
|
148
|
-
#: (String name) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
|
|
149
|
-
def first_unqualified_const(name)
|
|
150
|
-
# Look for an exact match first
|
|
151
|
-
_name, entries = @entries.find do |const_name, _entries|
|
|
152
|
-
const_name == name || const_name.end_with?("::#{name}")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# If an exact match is not found, then try to find a constant that ends with the name
|
|
156
|
-
unless entries
|
|
157
|
-
_name, entries = @entries.find do |const_name, _entries|
|
|
158
|
-
const_name.end_with?(name)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
entries #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches
|
|
166
|
-
# to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given
|
|
167
|
-
# name match. For example:
|
|
168
|
-
# ## Example
|
|
169
|
-
# ```ruby
|
|
170
|
-
# # If the index has two entries for `Foo::Bar` and one for `Foo::Baz`, then:
|
|
171
|
-
# index.prefix_search("Foo::B")
|
|
172
|
-
# # Will return:
|
|
173
|
-
# [
|
|
174
|
-
# [#<Entry::Class name="Foo::Bar">, #<Entry::Class name="Foo::Bar">],
|
|
175
|
-
# [#<Entry::Class name="Foo::Baz">],
|
|
176
|
-
# ]
|
|
177
|
-
# ```
|
|
178
|
-
#: (String query, ?Array[String]? nesting) -> Array[Array[Entry]]
|
|
179
|
-
def prefix_search(query, nesting = nil)
|
|
180
|
-
unless nesting
|
|
181
|
-
results = @entries_tree.search(query)
|
|
182
|
-
results.uniq!
|
|
183
|
-
return results
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
results = nesting.length.downto(0).flat_map do |i|
|
|
187
|
-
prefix = nesting[0...i] #: as !nil
|
|
188
|
-
.join("::")
|
|
189
|
-
namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
|
|
190
|
-
@entries_tree.search(namespaced_query)
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
results.uniq!
|
|
194
|
-
results
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
|
|
198
|
-
#: (String? query) ?{ (Entry) -> bool? } -> Array[Entry]
|
|
199
|
-
def fuzzy_search(query, &condition)
|
|
200
|
-
unless query
|
|
201
|
-
entries = @entries.filter_map do |_name, entries|
|
|
202
|
-
next if entries.first.is_a?(Entry::SingletonClass)
|
|
203
|
-
|
|
204
|
-
entries = entries.select(&condition) if condition
|
|
205
|
-
entries
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
return entries.flatten
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
normalized_query = query.gsub("::", "").downcase
|
|
212
|
-
|
|
213
|
-
results = @entries.filter_map do |name, entries|
|
|
214
|
-
next if entries.first.is_a?(Entry::SingletonClass)
|
|
215
|
-
|
|
216
|
-
entries = entries.select(&condition) if condition
|
|
217
|
-
next if entries.empty?
|
|
218
|
-
|
|
219
|
-
similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
|
|
220
|
-
[entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
|
|
221
|
-
end
|
|
222
|
-
results.sort_by!(&:last)
|
|
223
|
-
results.flat_map(&:first)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
#: (String? name, String receiver_name) -> Array[(Entry::Member | Entry::MethodAlias)]
|
|
227
|
-
def method_completion_candidates(name, receiver_name)
|
|
228
|
-
ancestors = linearized_ancestors_of(receiver_name)
|
|
229
|
-
|
|
230
|
-
candidates = name ? prefix_search(name).flatten : @entries.values.flatten
|
|
231
|
-
completion_items = candidates.each_with_object({}) do |entry, hash|
|
|
232
|
-
unless entry.is_a?(Entry::Member) || entry.is_a?(Entry::MethodAlias) ||
|
|
233
|
-
entry.is_a?(Entry::UnresolvedMethodAlias)
|
|
234
|
-
next
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
entry_name = entry.name
|
|
238
|
-
ancestor_index = ancestors.index(entry.owner&.name)
|
|
239
|
-
existing_entry, existing_entry_index = hash[entry_name]
|
|
240
|
-
|
|
241
|
-
# Conditions for matching a method completion candidate:
|
|
242
|
-
# 1. If an ancestor_index was found, it means that this method is owned by the receiver. The exact index is
|
|
243
|
-
# where in the ancestor chain the method was found. For example, if the ancestors are ["A", "B", "C"] and we
|
|
244
|
-
# found the method declared in `B`, then the ancestors index is 1
|
|
245
|
-
#
|
|
246
|
-
# 2. We already established that this method is owned by the receiver. Now, check if we already added a
|
|
247
|
-
# completion candidate for this method name. If not, then we just go and add it (the left hand side of the or)
|
|
248
|
-
#
|
|
249
|
-
# 3. If we had already found a method entry for the same name, then we need to check if the current entry that
|
|
250
|
-
# we are comparing appears first in the hierarchy or not. For example, imagine we have the method `open` defined
|
|
251
|
-
# in both `File` and its parent `IO`. If we first find the method `open` in `IO`, it will be inserted into the
|
|
252
|
-
# hash. Then, when we find the entry for `open` owned by `File`, we need to replace `IO.open` by `File.open`,
|
|
253
|
-
# since `File.open` appears first in the hierarchy chain and is therefore the correct method being invoked. The
|
|
254
|
-
# last part of the conditional checks if the current entry was found earlier in the hierarchy chain, in which
|
|
255
|
-
# case we must update the existing entry to avoid showing the wrong method declaration for overridden methods
|
|
256
|
-
next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
|
|
257
|
-
|
|
258
|
-
if entry.is_a?(Entry::UnresolvedMethodAlias)
|
|
259
|
-
resolved_alias = resolve_method_alias(entry, receiver_name, [])
|
|
260
|
-
hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
|
|
261
|
-
else
|
|
262
|
-
hash[entry_name] = [entry, ancestor_index]
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
completion_items.values.map!(&:first)
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
#: (String name, Array[String] nesting) -> Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
|
|
270
|
-
def constant_completion_candidates(name, nesting)
|
|
271
|
-
# If we have a top level reference, then we don't need to include completions inside the current nesting
|
|
272
|
-
if name.start_with?("::")
|
|
273
|
-
return @entries_tree.search(name.delete_prefix("::")) #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# Otherwise, we have to include every possible constant the user might be referring to. This is essentially the
|
|
277
|
-
# same algorithm as resolve, but instead of returning early we concatenate all unique results
|
|
278
|
-
|
|
279
|
-
# Direct constants inside this namespace
|
|
280
|
-
entries = @entries_tree.search(nesting.any? ? "#{nesting.join("::")}::#{name}" : name)
|
|
281
|
-
|
|
282
|
-
# Constants defined in enclosing scopes
|
|
283
|
-
nesting.length.downto(1) do |i|
|
|
284
|
-
namespace = nesting[0...i] #: as !nil
|
|
285
|
-
.join("::")
|
|
286
|
-
entries.concat(@entries_tree.search("#{namespace}::#{name}"))
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
# Inherited constants
|
|
290
|
-
if name.end_with?("::")
|
|
291
|
-
entries.concat(inherited_constant_completion_candidates(nil, nesting + [name]))
|
|
292
|
-
else
|
|
293
|
-
entries.concat(inherited_constant_completion_candidates(name, nesting))
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# Top level constants
|
|
297
|
-
entries.concat(@entries_tree.search(name))
|
|
298
|
-
|
|
299
|
-
# Filter only constants since methods may have names that look like constants
|
|
300
|
-
entries.select! do |definitions|
|
|
301
|
-
definitions.select! do |entry|
|
|
302
|
-
entry.is_a?(Entry::Constant) || entry.is_a?(Entry::ConstantAlias) ||
|
|
303
|
-
entry.is_a?(Entry::Namespace) || entry.is_a?(Entry::UnresolvedConstantAlias)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
definitions.any?
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
entries.uniq!
|
|
310
|
-
entries #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
# Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter
|
|
314
|
-
# documentation:
|
|
315
|
-
#
|
|
316
|
-
# name: the name of the reference how it was found in the source code (qualified or not)
|
|
317
|
-
# nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"])
|
|
318
|
-
# seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when
|
|
319
|
-
# resolving circular references
|
|
320
|
-
#: (String name, Array[String] nesting, ?Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
|
|
321
|
-
def resolve(name, nesting, seen_names = [])
|
|
322
|
-
# If we have a top level reference, then we just search for it straight away ignoring the nesting
|
|
323
|
-
if name.start_with?("::")
|
|
324
|
-
entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names)
|
|
325
|
-
return entries if entries
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Non qualified reference path
|
|
329
|
-
full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name
|
|
330
|
-
|
|
331
|
-
# When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the
|
|
332
|
-
# constant. First, it will try to find the constant in the exact namespace where the reference was found
|
|
333
|
-
entries = direct_or_aliased_constant(full_name, seen_names)
|
|
334
|
-
return entries if entries
|
|
335
|
-
|
|
336
|
-
# If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes,
|
|
337
|
-
# unwrapping each level one by one. Important note: the top level is not included because that's the fallback of
|
|
338
|
-
# the algorithm after every other possibility has been exhausted
|
|
339
|
-
entries = lookup_enclosing_scopes(name, nesting, seen_names)
|
|
340
|
-
return entries if entries
|
|
341
|
-
|
|
342
|
-
# If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the
|
|
343
|
-
# specific namespace where the reference was found
|
|
344
|
-
entries = lookup_ancestor_chain(name, nesting, seen_names)
|
|
345
|
-
return entries if entries
|
|
346
|
-
|
|
347
|
-
# Finally, as a fallback, Ruby will search for the constant in the top level namespace
|
|
348
|
-
direct_or_aliased_constant(name, seen_names)
|
|
349
|
-
rescue UnresolvableAliasError
|
|
350
|
-
nil
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Index all files for the given URIs, which defaults to what is configured. A block can be used to track and control
|
|
354
|
-
# indexing progress. That block is invoked with the current progress percentage and should return `true` to continue
|
|
355
|
-
# indexing or `false` to stop indexing.
|
|
356
|
-
#: (?uris: Array[URI::Generic]) ?{ (Integer progress) -> bool } -> void
|
|
357
|
-
def index_all(uris: @configuration.indexable_uris, &block)
|
|
358
|
-
# When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the
|
|
359
|
-
# existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this
|
|
360
|
-
# behavior and can take appropriate action.
|
|
361
|
-
if @initial_indexing_completed
|
|
362
|
-
raise IndexNotEmptyError,
|
|
363
|
-
"The index is not empty. To prevent invalid entries, `index_all` can only be called once."
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
RBSIndexer.new(self).index_ruby_core
|
|
367
|
-
# Calculate how many paths are worth 1% of progress
|
|
368
|
-
progress_step = (uris.length / 100.0).ceil
|
|
369
|
-
|
|
370
|
-
uris.each_with_index do |uri, index|
|
|
371
|
-
if block && index % progress_step == 0
|
|
372
|
-
progress = (index / progress_step) + 1
|
|
373
|
-
break unless block.call(progress)
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
index_file(uri, collect_comments: false)
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
@initial_indexing_completed = true
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
#: (URI::Generic uri, String source, ?collect_comments: bool) -> void
|
|
383
|
-
def index_single(uri, source, collect_comments: true)
|
|
384
|
-
dispatcher = Prism::Dispatcher.new
|
|
385
|
-
|
|
386
|
-
result = Prism.parse(source)
|
|
387
|
-
listener = DeclarationListener.new(self, dispatcher, result, uri, collect_comments: collect_comments)
|
|
388
|
-
dispatcher.dispatch(result.value)
|
|
389
|
-
|
|
390
|
-
require_path = uri.require_path
|
|
391
|
-
@require_paths_tree.insert(require_path, uri) if require_path
|
|
392
|
-
|
|
393
|
-
indexing_errors = listener.indexing_errors.uniq
|
|
394
|
-
indexing_errors.each { |error| $stderr.puts(error) } if indexing_errors.any?
|
|
395
|
-
rescue SystemStackError => e
|
|
396
|
-
if e.backtrace&.first&.include?("prism")
|
|
397
|
-
$stderr.puts "Prism error indexing #{uri}: #{e.message}"
|
|
398
|
-
else
|
|
399
|
-
raise
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
# Indexes a File URI by reading the contents from disk
|
|
404
|
-
#: (URI::Generic uri, ?collect_comments: bool) -> void
|
|
405
|
-
def index_file(uri, collect_comments: true)
|
|
406
|
-
path = uri.full_path #: as !nil
|
|
407
|
-
index_single(uri, File.read(path), collect_comments: collect_comments)
|
|
408
|
-
rescue Errno::EISDIR, Errno::ENOENT
|
|
409
|
-
# If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
|
|
410
|
-
# it
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
# Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
|
|
414
|
-
# it. The idea is that we test the name in parts starting from the complete name to the first namespace. For
|
|
415
|
-
# `Foo::Bar::Baz`, we would test:
|
|
416
|
-
# 1. Is `Foo::Bar::Baz` an alias? Get the target and recursively follow its target
|
|
417
|
-
# 2. Is `Foo::Bar` an alias? Get the target and recursively follow its target
|
|
418
|
-
# 3. Is `Foo` an alias? Get the target and recursively follow its target
|
|
419
|
-
#
|
|
420
|
-
# If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
|
|
421
|
-
# `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
|
|
422
|
-
# aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
|
|
423
|
-
#: (String name, ?Array[String] seen_names) -> String
|
|
424
|
-
def follow_aliased_namespace(name, seen_names = [])
|
|
425
|
-
parts = name.split("::")
|
|
426
|
-
real_parts = []
|
|
427
|
-
|
|
428
|
-
(parts.length - 1).downto(0) do |i|
|
|
429
|
-
current_name = parts[0..i] #: as !nil
|
|
430
|
-
.join("::")
|
|
431
|
-
|
|
432
|
-
entry = unless seen_names.include?(current_name)
|
|
433
|
-
@entries[current_name]&.first
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
case entry
|
|
437
|
-
when Entry::ConstantAlias
|
|
438
|
-
target = entry.target
|
|
439
|
-
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
|
|
440
|
-
when Entry::UnresolvedConstantAlias
|
|
441
|
-
resolved = resolve_alias(entry, seen_names)
|
|
442
|
-
|
|
443
|
-
if resolved.is_a?(Entry::UnresolvedConstantAlias)
|
|
444
|
-
raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
target = resolved.target
|
|
448
|
-
return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
|
|
449
|
-
else
|
|
450
|
-
real_parts.unshift(
|
|
451
|
-
parts[i], #: as !nil
|
|
452
|
-
)
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
real_parts.join("::")
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
# Attempts to find methods for a resolved fully qualified receiver name. Do not provide the `seen_names` parameter
|
|
460
|
-
# as it is used only internally to prevent infinite loops when resolving circular aliases
|
|
461
|
-
# Returns `nil` if the method does not exist on that receiver
|
|
462
|
-
#: (String method_name, String receiver_name, ?Array[String] seen_names, ?inherited_only: bool) -> Array[(Entry::Member | Entry::MethodAlias)]?
|
|
463
|
-
def resolve_method(method_name, receiver_name, seen_names = [], inherited_only: false)
|
|
464
|
-
method_entries = self[method_name]
|
|
465
|
-
return unless method_entries
|
|
466
|
-
|
|
467
|
-
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
|
|
468
|
-
ancestors.each do |ancestor|
|
|
469
|
-
next if inherited_only && ancestor == receiver_name
|
|
470
|
-
|
|
471
|
-
found = method_entries.filter_map do |entry|
|
|
472
|
-
case entry
|
|
473
|
-
when Entry::Member, Entry::MethodAlias
|
|
474
|
-
entry if entry.owner&.name == ancestor
|
|
475
|
-
when Entry::UnresolvedMethodAlias
|
|
476
|
-
# Resolve aliases lazily as we find them
|
|
477
|
-
if entry.owner&.name == ancestor
|
|
478
|
-
resolved_alias = resolve_method_alias(entry, receiver_name, seen_names)
|
|
479
|
-
resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
return found if found.any?
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
nil
|
|
488
|
-
rescue NonExistingNamespaceError
|
|
489
|
-
nil
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
|
|
493
|
-
# or constant declarations.
|
|
494
|
-
#
|
|
495
|
-
# When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
|
|
496
|
-
# everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
|
|
497
|
-
# module that prepends another module, then the prepend module appears before the included module.
|
|
498
|
-
#
|
|
499
|
-
# The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
|
|
500
|
-
#: (String fully_qualified_name) -> Array[String]
|
|
501
|
-
def linearized_ancestors_of(fully_qualified_name)
|
|
502
|
-
# If we already computed the ancestors for this namespace, return it straight away
|
|
503
|
-
cached_ancestors = @ancestors[fully_qualified_name]
|
|
504
|
-
return cached_ancestors if cached_ancestors
|
|
505
|
-
|
|
506
|
-
parts = fully_qualified_name.split("::")
|
|
507
|
-
singleton_levels = 0
|
|
508
|
-
|
|
509
|
-
parts.reverse_each do |part|
|
|
510
|
-
break unless part.start_with?("<")
|
|
511
|
-
|
|
512
|
-
singleton_levels += 1
|
|
513
|
-
parts.pop
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
attached_class_name = parts.join("::")
|
|
517
|
-
|
|
518
|
-
# If we don't have an entry for `name`, raise
|
|
519
|
-
entries = self[fully_qualified_name]
|
|
520
|
-
|
|
521
|
-
if singleton_levels > 0 && !entries && indexed?(attached_class_name)
|
|
522
|
-
entries = [existing_or_new_singleton_class(attached_class_name)]
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
|
|
526
|
-
|
|
527
|
-
ancestors = [fully_qualified_name]
|
|
528
|
-
|
|
529
|
-
# Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
|
|
530
|
-
# this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
|
|
531
|
-
# the cache will reflect the final result
|
|
532
|
-
@ancestors[fully_qualified_name] = ancestors
|
|
533
|
-
|
|
534
|
-
# If none of the entries for `name` are namespaces, raise
|
|
535
|
-
namespaces = entries.filter_map do |entry|
|
|
536
|
-
case entry
|
|
537
|
-
when Entry::Namespace
|
|
538
|
-
entry
|
|
539
|
-
when Entry::ConstantAlias
|
|
540
|
-
self[entry.target]&.grep(Entry::Namespace)
|
|
541
|
-
end
|
|
542
|
-
end.flatten
|
|
543
|
-
|
|
544
|
-
raise NonExistingNamespaceError,
|
|
545
|
-
"None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
|
|
546
|
-
|
|
547
|
-
# The original nesting where we discovered this namespace, so that we resolve the correct names of the
|
|
548
|
-
# included/prepended/extended modules and parent classes
|
|
549
|
-
nesting = namespaces.first #: as !nil
|
|
550
|
-
.nesting.flat_map { |n| n.split("::") }
|
|
551
|
-
|
|
552
|
-
if nesting.any?
|
|
553
|
-
singleton_levels.times do
|
|
554
|
-
nesting << "<#{nesting.last}>"
|
|
555
|
-
end
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
# We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
|
|
559
|
-
# new singleton methods or to extend a module through an include. There's no need to support instance methods, the
|
|
560
|
-
# inclusion of another module or the prepending of another module, because those features are already a part of
|
|
561
|
-
# Ruby and can be used directly without any metaprogramming
|
|
562
|
-
run_included_hooks(attached_class_name, nesting) if singleton_levels > 0
|
|
563
|
-
|
|
564
|
-
linearize_mixins(ancestors, namespaces, nesting)
|
|
565
|
-
linearize_superclass(
|
|
566
|
-
ancestors,
|
|
567
|
-
attached_class_name,
|
|
568
|
-
fully_qualified_name,
|
|
569
|
-
namespaces,
|
|
570
|
-
nesting,
|
|
571
|
-
singleton_levels,
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
ancestors
|
|
575
|
-
end
|
|
576
|
-
|
|
577
|
-
# Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
|
|
578
|
-
# and find inherited instance variables as well
|
|
579
|
-
#: (String variable_name, String owner_name) -> Array[Entry::InstanceVariable]?
|
|
580
|
-
def resolve_instance_variable(variable_name, owner_name)
|
|
581
|
-
entries = self[variable_name] #: as Array[Entry::InstanceVariable]?
|
|
582
|
-
return unless entries
|
|
583
|
-
|
|
584
|
-
ancestors = linearized_ancestors_of(owner_name)
|
|
585
|
-
return if ancestors.empty?
|
|
586
|
-
|
|
587
|
-
entries.select { |e| ancestors.include?(e.owner&.name) }
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
#: (String variable_name, String owner_name) -> Array[Entry::ClassVariable]?
|
|
591
|
-
def resolve_class_variable(variable_name, owner_name)
|
|
592
|
-
entries = self[variable_name]&.grep(Entry::ClassVariable)
|
|
593
|
-
return unless entries&.any?
|
|
594
|
-
|
|
595
|
-
ancestors = linearized_attached_ancestors(owner_name)
|
|
596
|
-
return if ancestors.empty?
|
|
597
|
-
|
|
598
|
-
entries.select { |e| ancestors.include?(e.owner&.name) }
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
# Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
|
|
602
|
-
# include the `@` prefix
|
|
603
|
-
#: (String name, String owner_name) -> Array[(Entry::InstanceVariable | Entry::ClassVariable)]
|
|
604
|
-
def instance_variable_completion_candidates(name, owner_name)
|
|
605
|
-
entries = prefix_search(name).flatten #: as Array[Entry::InstanceVariable | Entry::ClassVariable]
|
|
606
|
-
# Avoid wasting time linearizing ancestors if we didn't find anything
|
|
607
|
-
return entries if entries.empty?
|
|
608
|
-
|
|
609
|
-
ancestors = linearized_ancestors_of(owner_name)
|
|
610
|
-
|
|
611
|
-
instance_variables, class_variables = entries.partition { |e| e.is_a?(Entry::InstanceVariable) }
|
|
612
|
-
variables = instance_variables.select { |e| ancestors.any?(e.owner&.name) }
|
|
613
|
-
|
|
614
|
-
# Class variables are only owned by the attached class in our representation. If the owner is in a singleton
|
|
615
|
-
# context, we have to search for ancestors of the attached class
|
|
616
|
-
if class_variables.any?
|
|
617
|
-
name_parts = owner_name.split("::")
|
|
618
|
-
|
|
619
|
-
if name_parts.last&.start_with?("<")
|
|
620
|
-
attached_name = name_parts[0..-2] #: as !nil
|
|
621
|
-
.join("::")
|
|
622
|
-
attached_ancestors = linearized_ancestors_of(attached_name)
|
|
623
|
-
variables.concat(class_variables.select { |e| attached_ancestors.any?(e.owner&.name) })
|
|
624
|
-
else
|
|
625
|
-
variables.concat(class_variables.select { |e| ancestors.any?(e.owner&.name) })
|
|
626
|
-
end
|
|
627
|
-
end
|
|
628
|
-
|
|
629
|
-
variables.uniq!(&:name)
|
|
630
|
-
variables
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
#: (String name, String owner_name) -> Array[Entry::ClassVariable]
|
|
634
|
-
def class_variable_completion_candidates(name, owner_name)
|
|
635
|
-
entries = prefix_search(name).flatten #: as Array[Entry::ClassVariable]
|
|
636
|
-
# Avoid wasting time linearizing ancestors if we didn't find anything
|
|
637
|
-
return entries if entries.empty?
|
|
638
|
-
|
|
639
|
-
ancestors = linearized_attached_ancestors(owner_name)
|
|
640
|
-
variables = entries.select { |e| ancestors.any?(e.owner&.name) }
|
|
641
|
-
variables.uniq!(&:name)
|
|
642
|
-
variables
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
# Synchronizes a change made to the given URI. This method will ensure that new declarations are indexed, removed
|
|
646
|
-
# declarations removed and that the ancestor linearization cache is cleared if necessary. If a block is passed, the
|
|
647
|
-
# consumer of this API has to handle deleting and inserting/updating entries in the index instead of passing the
|
|
648
|
-
# document's source (used to handle unsaved changes to files)
|
|
649
|
-
#: (URI::Generic uri, ?String? source) ?{ (Index index) -> void } -> void
|
|
650
|
-
def handle_change(uri, source = nil, &block)
|
|
651
|
-
key = uri.to_s
|
|
652
|
-
original_entries = @uris_to_entries[key]
|
|
653
|
-
|
|
654
|
-
if block
|
|
655
|
-
block.call(self)
|
|
656
|
-
else
|
|
657
|
-
delete(uri)
|
|
658
|
-
index_single(
|
|
659
|
-
uri,
|
|
660
|
-
source, #: as !nil
|
|
661
|
-
)
|
|
662
|
-
end
|
|
663
|
-
|
|
664
|
-
updated_entries = @uris_to_entries[key]
|
|
665
|
-
return unless original_entries && updated_entries
|
|
666
|
-
|
|
667
|
-
# A change in one ancestor may impact several different others, which could be including that ancestor through
|
|
668
|
-
# indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
|
|
669
|
-
# ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
|
|
670
|
-
# ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
|
|
671
|
-
original_map = original_entries
|
|
672
|
-
.select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace]
|
|
673
|
-
.to_h { |e| [e.name, e.ancestor_hash] }
|
|
674
|
-
|
|
675
|
-
updated_map = updated_entries
|
|
676
|
-
.select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace]
|
|
677
|
-
.to_h { |e| [e.name, e.ancestor_hash] }
|
|
678
|
-
|
|
679
|
-
@ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
|
|
680
|
-
end
|
|
681
|
-
|
|
682
|
-
#: -> void
|
|
683
|
-
def clear_ancestors
|
|
684
|
-
@ancestors.clear
|
|
685
|
-
end
|
|
686
|
-
|
|
687
|
-
#: -> bool
|
|
688
|
-
def empty?
|
|
689
|
-
@entries.empty?
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
#: -> Array[String]
|
|
693
|
-
def names
|
|
694
|
-
@entries.keys
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
#: (String name) -> bool
|
|
698
|
-
def indexed?(name)
|
|
699
|
-
@entries.key?(name)
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
#: -> Integer
|
|
703
|
-
def length
|
|
704
|
-
@entries.count
|
|
705
|
-
end
|
|
706
|
-
|
|
707
|
-
#: (String name) -> Entry::SingletonClass
|
|
708
|
-
def existing_or_new_singleton_class(name)
|
|
709
|
-
*_namespace, unqualified_name = name.split("::")
|
|
710
|
-
full_singleton_name = "#{name}::<#{unqualified_name}>"
|
|
711
|
-
singleton = self[full_singleton_name]&.first #: as Entry::SingletonClass?
|
|
712
|
-
|
|
713
|
-
unless singleton
|
|
714
|
-
attached_ancestor = self[name]&.first #: as !nil
|
|
715
|
-
|
|
716
|
-
singleton = Entry::SingletonClass.new(
|
|
717
|
-
@configuration,
|
|
718
|
-
[full_singleton_name],
|
|
719
|
-
attached_ancestor.uri,
|
|
720
|
-
attached_ancestor.location,
|
|
721
|
-
attached_ancestor.name_location,
|
|
722
|
-
nil,
|
|
723
|
-
nil,
|
|
724
|
-
)
|
|
725
|
-
add(singleton, skip_prefix_tree: true)
|
|
726
|
-
end
|
|
727
|
-
|
|
728
|
-
singleton
|
|
729
|
-
end
|
|
730
|
-
|
|
731
|
-
#: [T] (String uri, ?Class[(T & Entry)]? type) -> (Array[Entry] | Array[T])?
|
|
732
|
-
def entries_for(uri, type = nil)
|
|
733
|
-
entries = @uris_to_entries[uri.to_s]
|
|
734
|
-
return entries unless type
|
|
735
|
-
|
|
736
|
-
entries&.grep(type)
|
|
737
|
-
end
|
|
738
|
-
|
|
739
|
-
private
|
|
740
|
-
|
|
741
|
-
# Always returns the linearized ancestors for the attached class, regardless of whether `name` refers to a singleton
|
|
742
|
-
# or attached namespace
|
|
743
|
-
#: (String name) -> Array[String]
|
|
744
|
-
def linearized_attached_ancestors(name)
|
|
745
|
-
name_parts = name.split("::")
|
|
746
|
-
|
|
747
|
-
if name_parts.last&.start_with?("<")
|
|
748
|
-
attached_name = name_parts[0..-2] #: as !nil
|
|
749
|
-
.join("::")
|
|
750
|
-
linearized_ancestors_of(attached_name)
|
|
751
|
-
else
|
|
752
|
-
linearized_ancestors_of(name)
|
|
753
|
-
end
|
|
754
|
-
end
|
|
755
|
-
|
|
756
|
-
# Runs the registered included hooks
|
|
757
|
-
#: (String fully_qualified_name, Array[String] nesting) -> void
|
|
758
|
-
def run_included_hooks(fully_qualified_name, nesting)
|
|
759
|
-
return if @included_hooks.empty?
|
|
760
|
-
|
|
761
|
-
namespaces = self[fully_qualified_name]&.grep(Entry::Namespace)
|
|
762
|
-
return unless namespaces
|
|
763
|
-
|
|
764
|
-
namespaces.each do |namespace|
|
|
765
|
-
namespace.mixin_operations.each do |operation|
|
|
766
|
-
next unless operation.is_a?(Entry::Include)
|
|
767
|
-
|
|
768
|
-
# First we resolve the include name, so that we know the actual module being referred to in the include
|
|
769
|
-
resolved_modules = resolve(operation.module_name, nesting)
|
|
770
|
-
next unless resolved_modules
|
|
771
|
-
|
|
772
|
-
module_name = resolved_modules.first #: as !nil
|
|
773
|
-
.name
|
|
774
|
-
|
|
775
|
-
# Then we grab any hooks registered for that module
|
|
776
|
-
hooks = @included_hooks[module_name]
|
|
777
|
-
next unless hooks
|
|
778
|
-
|
|
779
|
-
# We invoke the hooks with the index and the namespace that included the module
|
|
780
|
-
hooks.each { |hook| hook.call(self, namespace) }
|
|
781
|
-
end
|
|
782
|
-
end
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
# Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
|
|
786
|
-
# linearized ancestors of the mixins
|
|
787
|
-
#: (Array[String] ancestors, Array[Entry::Namespace] namespace_entries, Array[String] nesting) -> void
|
|
788
|
-
def linearize_mixins(ancestors, namespace_entries, nesting)
|
|
789
|
-
mixin_operations = namespace_entries.flat_map(&:mixin_operations)
|
|
790
|
-
main_namespace_index = 0
|
|
791
|
-
|
|
792
|
-
mixin_operations.each do |operation|
|
|
793
|
-
resolved_module = resolve(operation.module_name, nesting)
|
|
794
|
-
next unless resolved_module
|
|
795
|
-
|
|
796
|
-
module_fully_qualified_name = resolved_module.first #: as !nil
|
|
797
|
-
.name
|
|
798
|
-
|
|
799
|
-
case operation
|
|
800
|
-
when Entry::Prepend
|
|
801
|
-
# When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
|
|
802
|
-
# the actual namespace twice. However, it does not check if it has been included because you are allowed to
|
|
803
|
-
# prepend the same module after it has already been included
|
|
804
|
-
linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
|
|
805
|
-
|
|
806
|
-
# When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
|
|
807
|
-
# example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
|
|
808
|
-
# be inserted after `"A`
|
|
809
|
-
prepended_ancestors = ancestors[0...main_namespace_index] #: as !nil
|
|
810
|
-
uniq_prepends = linearized_prepends - prepended_ancestors
|
|
811
|
-
insert_position = linearized_prepends.length - uniq_prepends.length
|
|
812
|
-
|
|
813
|
-
ancestors #: as untyped
|
|
814
|
-
.insert(insert_position, *uniq_prepends)
|
|
815
|
-
|
|
816
|
-
main_namespace_index += linearized_prepends.length
|
|
817
|
-
when Entry::Include
|
|
818
|
-
# When including a module, Ruby will always prevent duplicate entries in case the module has already been
|
|
819
|
-
# prepended or included
|
|
820
|
-
linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
|
|
821
|
-
ancestors #: as untyped
|
|
822
|
-
.insert(main_namespace_index + 1, *(linearized_includes - ancestors))
|
|
823
|
-
end
|
|
824
|
-
end
|
|
825
|
-
end
|
|
826
|
-
|
|
827
|
-
# Linearize the superclass of a given namespace (including modules with the implicit `Module` superclass). This
|
|
828
|
-
# method will mutate the `ancestors` array with the linearized ancestors of the superclass
|
|
829
|
-
#: (Array[String] ancestors, String attached_class_name, String fully_qualified_name, Array[Entry::Namespace] namespace_entries, Array[String] nesting, Integer singleton_levels) -> void
|
|
830
|
-
def linearize_superclass( # rubocop:disable Metrics/ParameterLists
|
|
831
|
-
ancestors,
|
|
832
|
-
attached_class_name,
|
|
833
|
-
fully_qualified_name,
|
|
834
|
-
namespace_entries,
|
|
835
|
-
nesting,
|
|
836
|
-
singleton_levels
|
|
837
|
-
)
|
|
838
|
-
# Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
|
|
839
|
-
# from two different classes in different files, we simply ignore it
|
|
840
|
-
possible_parents = singleton_levels > 0 ? self[attached_class_name] : namespace_entries
|
|
841
|
-
superclass = nil #: Entry::Class?
|
|
842
|
-
|
|
843
|
-
possible_parents&.each do |n|
|
|
844
|
-
# Ignore non class entries
|
|
845
|
-
next unless n.is_a?(Entry::Class)
|
|
846
|
-
|
|
847
|
-
parent_class = n.parent_class
|
|
848
|
-
next unless parent_class
|
|
849
|
-
|
|
850
|
-
# Always set the superclass, but break early if we found one that isn't `::Object` (meaning we found an explicit
|
|
851
|
-
# parent class and not the implicit default). Note that when setting different parents to the same class, which
|
|
852
|
-
# is invalid, we pick whatever is the first one we find
|
|
853
|
-
superclass = n
|
|
854
|
-
break if parent_class != "::Object"
|
|
855
|
-
end
|
|
856
|
-
|
|
857
|
-
if superclass
|
|
858
|
-
# If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
|
|
859
|
-
# error. We need to ensure that this isn't the case
|
|
860
|
-
parent_class = superclass.parent_class #: as !nil
|
|
861
|
-
|
|
862
|
-
resolved_parent_class = resolve(parent_class, nesting)
|
|
863
|
-
parent_class_name = resolved_parent_class&.first&.name
|
|
864
|
-
|
|
865
|
-
if parent_class_name && fully_qualified_name != parent_class_name
|
|
866
|
-
|
|
867
|
-
parent_name_parts = parent_class_name.split("::")
|
|
868
|
-
singleton_levels.times do
|
|
869
|
-
parent_name_parts << "<#{parent_name_parts.last}>"
|
|
870
|
-
end
|
|
871
|
-
|
|
872
|
-
ancestors.concat(linearized_ancestors_of(parent_name_parts.join("::")))
|
|
873
|
-
end
|
|
874
|
-
|
|
875
|
-
# When computing the linearization for a class's singleton class, it inherits from the linearized ancestors of
|
|
876
|
-
# the `Class` class
|
|
877
|
-
if parent_class_name&.start_with?("BasicObject") && singleton_levels > 0
|
|
878
|
-
class_class_name_parts = ["Class"]
|
|
879
|
-
|
|
880
|
-
(singleton_levels - 1).times do
|
|
881
|
-
class_class_name_parts << "<#{class_class_name_parts.last}>"
|
|
882
|
-
end
|
|
883
|
-
|
|
884
|
-
ancestors.concat(linearized_ancestors_of(class_class_name_parts.join("::")))
|
|
885
|
-
end
|
|
886
|
-
elsif singleton_levels > 0
|
|
887
|
-
# When computing the linearization for a module's singleton class, it inherits from the linearized ancestors of
|
|
888
|
-
# the `Module` class
|
|
889
|
-
mod = self[attached_class_name]&.find { |n| n.is_a?(Entry::Module) } #: as Entry::Module?
|
|
890
|
-
|
|
891
|
-
if mod
|
|
892
|
-
module_class_name_parts = ["Module"]
|
|
893
|
-
|
|
894
|
-
(singleton_levels - 1).times do
|
|
895
|
-
module_class_name_parts << "<#{module_class_name_parts.last}>"
|
|
896
|
-
end
|
|
897
|
-
|
|
898
|
-
ancestors.concat(linearized_ancestors_of(module_class_name_parts.join("::")))
|
|
899
|
-
end
|
|
900
|
-
end
|
|
901
|
-
end
|
|
902
|
-
|
|
903
|
-
# Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
|
|
904
|
-
# that doesn't exist, then we return the same UnresolvedAlias
|
|
905
|
-
#: (Entry::UnresolvedConstantAlias entry, Array[String] seen_names) -> (Entry::ConstantAlias | Entry::UnresolvedConstantAlias)
|
|
906
|
-
def resolve_alias(entry, seen_names)
|
|
907
|
-
alias_name = entry.name
|
|
908
|
-
return entry if seen_names.include?(alias_name)
|
|
909
|
-
|
|
910
|
-
seen_names << alias_name
|
|
911
|
-
|
|
912
|
-
target = resolve(entry.target, entry.nesting, seen_names)
|
|
913
|
-
return entry unless target
|
|
914
|
-
|
|
915
|
-
# Self referential alias can be unresolved we should bail out from resolving
|
|
916
|
-
return entry if target.first == entry
|
|
917
|
-
|
|
918
|
-
target_name = target.first #: as !nil
|
|
919
|
-
.name
|
|
920
|
-
resolved_alias = Entry::ConstantAlias.new(target_name, entry)
|
|
921
|
-
|
|
922
|
-
# Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
|
|
923
|
-
original_entries = @entries[alias_name] #: as !nil
|
|
924
|
-
original_entries.delete(entry)
|
|
925
|
-
original_entries << resolved_alias
|
|
926
|
-
|
|
927
|
-
@entries_tree.insert(alias_name, original_entries)
|
|
928
|
-
|
|
929
|
-
resolved_alias
|
|
930
|
-
end
|
|
931
|
-
|
|
932
|
-
#: (String name, Array[String] nesting, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
|
|
933
|
-
def lookup_enclosing_scopes(name, nesting, seen_names)
|
|
934
|
-
nesting.length.downto(1) do |i|
|
|
935
|
-
namespace = nesting[0...i] #: as !nil
|
|
936
|
-
.join("::")
|
|
937
|
-
|
|
938
|
-
# If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
|
|
939
|
-
# because the user might be trying to jump to the alias definition.
|
|
940
|
-
#
|
|
941
|
-
# However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
|
|
942
|
-
# the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
|
|
943
|
-
# `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
|
|
944
|
-
# `RubyLsp::Interface` part is an alias, that has to be resolved
|
|
945
|
-
entries = direct_or_aliased_constant("#{namespace}::#{name}", seen_names)
|
|
946
|
-
return entries if entries
|
|
947
|
-
end
|
|
948
|
-
|
|
949
|
-
nil
|
|
950
|
-
end
|
|
951
|
-
|
|
952
|
-
#: (String name, Array[String] nesting, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
|
|
953
|
-
def lookup_ancestor_chain(name, nesting, seen_names)
|
|
954
|
-
*nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
|
|
955
|
-
return if nesting_parts.empty?
|
|
956
|
-
|
|
957
|
-
namespace_entries = resolve(nesting_parts.join("::"), [], seen_names)
|
|
958
|
-
return unless namespace_entries
|
|
959
|
-
|
|
960
|
-
namespace_name = namespace_entries.first #: as !nil
|
|
961
|
-
.name
|
|
962
|
-
ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(namespace_name)
|
|
963
|
-
|
|
964
|
-
ancestors.each do |ancestor_name|
|
|
965
|
-
entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names)
|
|
966
|
-
return entries if entries
|
|
967
|
-
end
|
|
968
|
-
|
|
969
|
-
nil
|
|
970
|
-
rescue NonExistingNamespaceError
|
|
971
|
-
nil
|
|
972
|
-
end
|
|
973
|
-
|
|
974
|
-
#: (String? name, Array[String] nesting) -> Array[Array[(Entry::Namespace | Entry::ConstantAlias | Entry::UnresolvedConstantAlias | Entry::Constant)]]
|
|
975
|
-
def inherited_constant_completion_candidates(name, nesting)
|
|
976
|
-
namespace_entries = if name
|
|
977
|
-
*nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::")
|
|
978
|
-
return [] if nesting_parts.empty?
|
|
979
|
-
|
|
980
|
-
resolve(nesting_parts.join("::"), [])
|
|
981
|
-
else
|
|
982
|
-
resolve(nesting.join("::"), [])
|
|
983
|
-
end
|
|
984
|
-
return [] unless namespace_entries
|
|
985
|
-
|
|
986
|
-
namespace_name = namespace_entries.first #: as !nil
|
|
987
|
-
.name
|
|
988
|
-
ancestors = linearized_ancestors_of(namespace_name)
|
|
989
|
-
candidates = ancestors.flat_map do |ancestor_name|
|
|
990
|
-
@entries_tree.search("#{ancestor_name}::#{constant_name}")
|
|
991
|
-
end
|
|
992
|
-
|
|
993
|
-
# For candidates with the same name, we must only show the first entry in the inheritance chain, since that's the
|
|
994
|
-
# one the user will be referring to in completion
|
|
995
|
-
completion_items = candidates.each_with_object({}) do |entries, hash|
|
|
996
|
-
*parts, short_name = entries.first #: as !nil
|
|
997
|
-
.name.split("::")
|
|
998
|
-
namespace_name = parts.join("::")
|
|
999
|
-
ancestor_index = ancestors.index(namespace_name)
|
|
1000
|
-
existing_entry, existing_entry_index = hash[short_name]
|
|
1001
|
-
|
|
1002
|
-
next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
|
|
1003
|
-
|
|
1004
|
-
hash[short_name] = [entries, ancestor_index]
|
|
1005
|
-
end
|
|
1006
|
-
|
|
1007
|
-
completion_items.values.map!(&:first)
|
|
1008
|
-
rescue NonExistingNamespaceError
|
|
1009
|
-
[]
|
|
1010
|
-
end
|
|
1011
|
-
|
|
1012
|
-
# Removes redundancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo`
|
|
1013
|
-
# inside of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up
|
|
1014
|
-
# with `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and
|
|
1015
|
-
# the nesting
|
|
1016
|
-
#: (String name, Array[String] nesting) -> String
|
|
1017
|
-
def build_non_redundant_full_name(name, nesting)
|
|
1018
|
-
# If there's no nesting, then we can just return the name as is
|
|
1019
|
-
return name if nesting.empty?
|
|
1020
|
-
|
|
1021
|
-
# If the name is not qualified, we can just concatenate the nesting and the name
|
|
1022
|
-
return "#{nesting.join("::")}::#{name}" unless name.include?("::")
|
|
1023
|
-
|
|
1024
|
-
name_parts = name.split("::")
|
|
1025
|
-
first_redundant_part = nesting.index(name_parts[0])
|
|
1026
|
-
|
|
1027
|
-
# If there are no redundant parts between the name and the nesting, then the full name is both combined
|
|
1028
|
-
return "#{nesting.join("::")}::#{name}" unless first_redundant_part
|
|
1029
|
-
|
|
1030
|
-
# Otherwise, push all of the leading parts of the nesting that aren't redundant into the name. For example, if we
|
|
1031
|
-
# have a reference to `Foo::Bar` inside the `[Namespace, Foo]` nesting, then only the `Foo` part is redundant, but
|
|
1032
|
-
# we still need to include the `Namespace` part
|
|
1033
|
-
name_parts.unshift(*nesting[0...first_redundant_part])
|
|
1034
|
-
name_parts.join("::")
|
|
1035
|
-
end
|
|
1036
|
-
|
|
1037
|
-
# Tries to return direct entry from index then non seen canonicalized alias or nil
|
|
1038
|
-
#: (String full_name, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
|
|
1039
|
-
def direct_or_aliased_constant(full_name, seen_names)
|
|
1040
|
-
if (entries = @entries[full_name])
|
|
1041
|
-
return entries.map do |e|
|
|
1042
|
-
e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
|
|
1043
|
-
end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
|
|
1044
|
-
end
|
|
1045
|
-
|
|
1046
|
-
aliased = follow_aliased_namespace(full_name, seen_names)
|
|
1047
|
-
return if full_name == aliased || seen_names.include?(aliased)
|
|
1048
|
-
|
|
1049
|
-
@entries[aliased]&.map do |e|
|
|
1050
|
-
e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
|
|
1051
|
-
end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
|
|
1052
|
-
end
|
|
1053
|
-
|
|
1054
|
-
# Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to
|
|
1055
|
-
# identify the target or the same unresolved alias entry if we couldn't
|
|
1056
|
-
#: (Entry::UnresolvedMethodAlias entry, String receiver_name, Array[String] seen_names) -> (Entry::MethodAlias | Entry::UnresolvedMethodAlias)
|
|
1057
|
-
def resolve_method_alias(entry, receiver_name, seen_names)
|
|
1058
|
-
new_name = entry.new_name
|
|
1059
|
-
return entry if new_name == entry.old_name
|
|
1060
|
-
return entry if seen_names.include?(new_name)
|
|
1061
|
-
|
|
1062
|
-
seen_names << new_name
|
|
1063
|
-
|
|
1064
|
-
target_method_entries = resolve_method(entry.old_name, receiver_name, seen_names)
|
|
1065
|
-
return entry unless target_method_entries
|
|
1066
|
-
|
|
1067
|
-
resolved_alias = Entry::MethodAlias.new(
|
|
1068
|
-
target_method_entries.first, #: as !nil
|
|
1069
|
-
entry,
|
|
1070
|
-
)
|
|
1071
|
-
original_entries = @entries[new_name] #: as !nil
|
|
1072
|
-
original_entries.delete(entry)
|
|
1073
|
-
original_entries << resolved_alias
|
|
1074
|
-
resolved_alias
|
|
1075
|
-
end
|
|
1076
|
-
end
|
|
1077
|
-
end
|