ruby-lsp 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp-check +1 -1
  5. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +40 -5
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +141 -5
  7. data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +66 -18
  8. data/lib/ruby_indexer/test/classes_and_modules_test.rb +23 -0
  9. data/lib/ruby_indexer/test/configuration_test.rb +2 -0
  10. data/lib/ruby_indexer/test/constant_test.rb +202 -0
  11. data/lib/ruby_indexer/test/index_test.rb +20 -0
  12. data/lib/ruby_lsp/{extension.rb → addon.rb} +27 -25
  13. data/lib/ruby_lsp/check_docs.rb +7 -8
  14. data/lib/ruby_lsp/document.rb +35 -38
  15. data/lib/ruby_lsp/event_emitter.rb +239 -77
  16. data/lib/ruby_lsp/executor.rb +45 -55
  17. data/lib/ruby_lsp/internal.rb +2 -3
  18. data/lib/ruby_lsp/listener.rb +8 -7
  19. data/lib/ruby_lsp/parameter_scope.rb +33 -0
  20. data/lib/ruby_lsp/requests/base_request.rb +3 -3
  21. data/lib/ruby_lsp/requests/code_action_resolve.rb +14 -14
  22. data/lib/ruby_lsp/requests/code_lens.rb +39 -63
  23. data/lib/ruby_lsp/requests/completion.rb +54 -32
  24. data/lib/ruby_lsp/requests/definition.rb +30 -27
  25. data/lib/ruby_lsp/requests/diagnostics.rb +26 -3
  26. data/lib/ruby_lsp/requests/document_highlight.rb +18 -19
  27. data/lib/ruby_lsp/requests/document_link.rb +50 -9
  28. data/lib/ruby_lsp/requests/document_symbol.rb +82 -75
  29. data/lib/ruby_lsp/requests/folding_ranges.rb +199 -222
  30. data/lib/ruby_lsp/requests/formatting.rb +5 -6
  31. data/lib/ruby_lsp/requests/hover.rb +33 -22
  32. data/lib/ruby_lsp/requests/inlay_hints.rb +2 -3
  33. data/lib/ruby_lsp/requests/selection_ranges.rb +65 -40
  34. data/lib/ruby_lsp/requests/semantic_highlighting.rb +187 -145
  35. data/lib/ruby_lsp/requests/show_syntax_tree.rb +3 -4
  36. data/lib/ruby_lsp/requests/support/annotation.rb +18 -17
  37. data/lib/ruby_lsp/requests/support/common.rb +17 -26
  38. data/lib/ruby_lsp/requests/support/dependency_detector.rb +67 -42
  39. data/lib/ruby_lsp/requests/support/highlight_target.rb +64 -45
  40. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +9 -4
  41. data/lib/ruby_lsp/requests/support/selection_range.rb +5 -4
  42. data/lib/ruby_lsp/requests/support/sorbet.rb +2 -57
  43. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +7 -1
  44. data/lib/ruby_lsp/requests/workspace_symbol.rb +4 -1
  45. data/lib/ruby_lsp/server.rb +6 -44
  46. data/lib/ruby_lsp/utils.rb +2 -12
  47. metadata +11 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36eafde3135250941805bf340b782dfab3002839b66ba4722d261edd7fe1356a
4
- data.tar.gz: f8dbfe5eb36489223c21d2e816f91324d0928895632a4344c16ecc756c0cb279
3
+ metadata.gz: b2708af5c4c174d0fafe710fb1f7aaeed7e1aee45cb7aad49f16a53d9f32ad03
4
+ data.tar.gz: eaab5153e4c99eac231bf7376ef368fec589e1d6fe05807c9a4de45394242e42
5
5
  SHA512:
6
- metadata.gz: 24312d8a758e8388c02165d8f0ee4a8f8ccb93bb71efbd3ed3a4eeeadeb750373856cdcd3842f2cb1138ef7067c7c497260adeee347cc37f81e960a1febb7a44
7
- data.tar.gz: c637ef4a50da10f0a7d81a15e9fc7af209e6a056642c93b315e0410fbfe732a8f42cd841ca3d70f66fac79d54474253a8febfe60f487c970632e64fd88ce103b
6
+ metadata.gz: ca5c0ba2801350c6557c31b90626a07a4e358245d2177b65a9670f00d2521d0d9850fc488dd65124a58cbc841a09c656d91595cf436391f80e87a434021e822b
7
+ data.tar.gz: c8cf7c3228b62639d5b8eb3e3b61750237e85bc43837424359bb54b5d794bea37f0b7214ea6229bfb365c189445d48069dae166e8ad91a0b774bbe34fd269473
data/README.md CHANGED
@@ -46,14 +46,14 @@ See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth de
46
46
  For creating rich themes for Ruby using the semantic highlighting information, see the [semantic highlighting
47
47
  documentation](SEMANTIC_HIGHLIGHTING.md).
48
48
 
49
- ### Extensions
49
+ ### Addons
50
50
 
51
- The Ruby LSP provides a server extension system that allows other gems to enhance the base functionality with more
52
- editor features. This is the mechanism that powers extensions like
51
+ The Ruby LSP provides an addon system that allows other gems to enhance the base functionality with more editor
52
+ features. This is the mechanism that powers addons like
53
53
 
54
54
  - [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails)
55
55
 
56
- For instructions on how to create extensions, see the [server extensions documentation](SERVER_EXTENSIONS.md).
56
+ For instructions on how to create addons, see the [addons documentation](ADDONS.md).
57
57
 
58
58
  ## Learn More
59
59
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.10.1
1
+ 0.11.1
data/exe/ruby-lsp-check CHANGED
@@ -16,7 +16,7 @@ end
16
16
 
17
17
  require_relative "../lib/ruby_lsp/internal"
18
18
 
19
- RubyLsp::Extension.load_extensions
19
+ RubyLsp::Addon.load_addons
20
20
 
21
21
  T::Utils.run_all_sig_blocks
22
22
 
@@ -18,11 +18,7 @@ module RubyIndexer
18
18
 
19
19
  sig { void }
20
20
  def initialize
21
- excluded_gem_names = Bundler.definition.dependencies.filter_map do |dependency|
22
- dependency.name if dependency.groups == [:development]
23
- end
24
-
25
- @excluded_gems = T.let(excluded_gem_names, T::Array[String])
21
+ @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
26
22
  @included_gems = T.let([], T::Array[String])
27
23
  @excluded_patterns = T.let([File.join("**", "*_test.rb")], T::Array[String])
28
24
  path = Bundler.settings["path"]
@@ -174,5 +170,44 @@ module RubyIndexer
174
170
  @included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
175
171
  @excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"]
176
172
  end
173
+
174
+ sig { returns(T::Array[String]) }
175
+ def initial_excluded_gems
176
+ excluded, others = Bundler.definition.dependencies.partition do |dependency|
177
+ dependency.groups == [:development]
178
+ end
179
+
180
+ # When working on a gem, we need to make sure that its gemspec dependencies can't be excluded. This is necessary
181
+ # because Bundler doesn't assign groups to gemspec dependencies
182
+ this_gem = Bundler.definition.dependencies.find do |d|
183
+ d.to_spec.full_gem_path == Dir.pwd
184
+ rescue Gem::MissingSpecError
185
+ false
186
+ end
187
+
188
+ others.concat(this_gem.to_spec.dependencies) if this_gem
189
+
190
+ excluded.each do |dependency|
191
+ next unless dependency.runtime?
192
+
193
+ dependency.to_spec.dependencies.each do |transitive_dependency|
194
+ # If the transitive dependency is included in other groups, skip it
195
+ next if others.any? { |d| d.name == transitive_dependency.name }
196
+
197
+ # If the transitive dependency is included as a transitive dependency of a gem outside of the development
198
+ # group, skip it
199
+ next if others.any? { |d| d.to_spec.dependencies.include?(transitive_dependency) }
200
+
201
+ excluded << transitive_dependency
202
+ end
203
+ rescue Gem::MissingSpecError
204
+ # If a gem is scoped only to some specific platform, then its dependencies may not be installed either, but they
205
+ # are still listed in dependencies. We can't index them because they are not installed for the platform, so we
206
+ # just ignore if they're missing
207
+ end
208
+
209
+ excluded.uniq!
210
+ excluded.map(&:name)
211
+ end
177
212
  end
178
213
  end
@@ -5,6 +5,8 @@ module RubyIndexer
5
5
  class Index
6
6
  extend T::Sig
7
7
 
8
+ class UnresolvableAliasError < StandardError; end
9
+
8
10
  # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
9
11
  ENTRY_SIMILARITY_THRESHOLD = 0.7
10
12
 
@@ -125,13 +127,29 @@ module RubyIndexer
125
127
  # 3. Baz
126
128
  sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
127
129
  def resolve(name, nesting)
128
- (nesting.length + 1).downto(0).each do |i|
129
- prefix = T.must(nesting[0...i]).join("::")
130
- full_name = prefix.empty? ? name : "#{prefix}::#{name}"
131
- entries = @entries[full_name]
132
- return entries if entries
130
+ if name.start_with?("::")
131
+ name = name.delete_prefix("::")
132
+ results = @entries[name] || @entries[follow_aliased_namespace(name)]
133
+ return results.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if results
133
134
  end
134
135
 
136
+ nesting.length.downto(0).each do |i|
137
+ namespace = T.must(nesting[0...i]).join("::")
138
+ full_name = namespace.empty? ? name : "#{namespace}::#{name}"
139
+
140
+ # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
141
+ # because the user might be trying to jump to the alias definition.
142
+ #
143
+ # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
144
+ # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
145
+ # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
146
+ # `RubyLsp::Interface` part is an alias, that has to be resolved
147
+ entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
148
+ return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries
149
+ end
150
+
151
+ nil
152
+ rescue UnresolvableAliasError
135
153
  nil
136
154
  end
137
155
 
@@ -152,6 +170,68 @@ module RubyIndexer
152
170
  # If `path` is a directory, just ignore it and continue indexing
153
171
  end
154
172
 
173
+ # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
174
+ # it. The idea is that we test the name in parts starting from the complete name to the first namespace. For
175
+ # `Foo::Bar::Baz`, we would test:
176
+ # 1. Is `Foo::Bar::Baz` an alias? Get the target and recursively follow its target
177
+ # 2. Is `Foo::Bar` an alias? Get the target and recursively follow its target
178
+ # 3. Is `Foo` an alias? Get the target and recursively follow its target
179
+ #
180
+ # If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
181
+ # `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
182
+ # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
183
+ sig { params(name: String).returns(String) }
184
+ def follow_aliased_namespace(name)
185
+ parts = name.split("::")
186
+ real_parts = []
187
+
188
+ (parts.length - 1).downto(0).each do |i|
189
+ current_name = T.must(parts[0..i]).join("::")
190
+ entry = @entries[current_name]&.first
191
+
192
+ case entry
193
+ when Entry::Alias
194
+ target = entry.target
195
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
196
+ when Entry::UnresolvedAlias
197
+ resolved = resolve_alias(entry)
198
+
199
+ if resolved.is_a?(Entry::UnresolvedAlias)
200
+ raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
201
+ end
202
+
203
+ target = resolved.target
204
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
205
+ else
206
+ real_parts.unshift(T.must(parts[i]))
207
+ end
208
+ end
209
+
210
+ real_parts.join("::")
211
+ end
212
+
213
+ private
214
+
215
+ # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
216
+ # that doesn't exist, then we return the same UnresolvedAlias
217
+ sig { params(entry: Entry::UnresolvedAlias).returns(T.any(Entry::Alias, Entry::UnresolvedAlias)) }
218
+ def resolve_alias(entry)
219
+ target = resolve(entry.target, entry.nesting)
220
+ return entry unless target
221
+
222
+ target_name = T.must(target.first).name
223
+ resolved_alias = Entry::Alias.new(target_name, entry)
224
+
225
+ # Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
226
+ original_entries = T.must(@entries[entry.name])
227
+ original_entries.delete(entry)
228
+ original_entries << resolved_alias
229
+
230
+ @entries_tree.insert(entry.name, original_entries)
231
+
232
+ resolved_alias
233
+ end
234
+
155
235
  class Entry
156
236
  extend T::Sig
157
237
 
@@ -167,12 +247,16 @@ module RubyIndexer
167
247
  sig { returns(T::Array[String]) }
168
248
  attr_reader :comments
169
249
 
250
+ sig { returns(Symbol) }
251
+ attr_accessor :visibility
252
+
170
253
  sig { params(name: String, file_path: String, location: YARP::Location, comments: T::Array[String]).void }
171
254
  def initialize(name, file_path, location, comments)
172
255
  @name = name
173
256
  @file_path = file_path
174
257
  @location = location
175
258
  @comments = comments
259
+ @visibility = T.let(:public, Symbol)
176
260
  end
177
261
 
178
262
  sig { returns(String) }
@@ -195,6 +279,58 @@ module RubyIndexer
195
279
 
196
280
  class Constant < Entry
197
281
  end
282
+
283
+ # An UnresolvedAlias points to a constant alias with a right hand side that has not yet been resolved. For
284
+ # example, if we find
285
+ #
286
+ # ```ruby
287
+ # CONST = Foo
288
+ # ```
289
+ # Before we have discovered `Foo`, there's no way to eagerly resolve this alias to the correct target constant.
290
+ # All aliases are inserted as UnresolvedAlias in the index first and then we lazily resolve them to the correct
291
+ # target in [rdoc-ref:Index#resolve]. If the right hand side contains a constant that doesn't exist, then it's not
292
+ # possible to resolve the alias and it will remain an UnresolvedAlias until the right hand side constant exists
293
+ class UnresolvedAlias < Entry
294
+ extend T::Sig
295
+
296
+ sig { returns(String) }
297
+ attr_reader :target
298
+
299
+ sig { returns(T::Array[String]) }
300
+ attr_reader :nesting
301
+
302
+ sig do
303
+ params(
304
+ target: String,
305
+ nesting: T::Array[String],
306
+ name: String,
307
+ file_path: String,
308
+ location: YARP::Location,
309
+ comments: T::Array[String],
310
+ ).void
311
+ end
312
+ def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
313
+ super(name, file_path, location, comments)
314
+
315
+ @target = target
316
+ @nesting = nesting
317
+ end
318
+ end
319
+
320
+ # Alias represents a resolved alias, which points to an existing constant target
321
+ class Alias < Entry
322
+ extend T::Sig
323
+
324
+ sig { returns(String) }
325
+ attr_reader :target
326
+
327
+ sig { params(target: String, unresolved_alias: UnresolvedAlias).void }
328
+ def initialize(target, unresolved_alias)
329
+ super(unresolved_alias.name, unresolved_alias.file_path, unresolved_alias.location, unresolved_alias.comments)
330
+
331
+ @target = target
332
+ end
333
+ end
198
334
  end
199
335
  end
200
336
  end
@@ -36,9 +36,19 @@ module RubyIndexer
36
36
  when YARP::ModuleNode
37
37
  add_index_entry(node, Index::Entry::Module)
38
38
  when YARP::ConstantWriteNode, YARP::ConstantOrWriteNode
39
- add_constant(node)
40
- when YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode
41
- add_constant_with_path(node)
39
+ name = fully_qualify_name(node.name.to_s)
40
+ add_constant(node, name)
41
+ when YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode, YARP::ConstantPathOperatorWriteNode,
42
+ YARP::ConstantPathAndWriteNode
43
+
44
+ # ignore variable constants like `var::FOO` or `self.class::FOO`
45
+ return unless node.target.parent.nil? || node.target.parent.is_a?(YARP::ConstantReadNode)
46
+
47
+ name = fully_qualify_name(node.target.location.slice)
48
+ add_constant(node, name)
49
+ when YARP::CallNode
50
+ message = node.message
51
+ handle_private_constant(node) if message == "private_constant"
42
52
  end
43
53
  end
44
54
 
@@ -50,28 +60,66 @@ module RubyIndexer
50
60
 
51
61
  private
52
62
 
53
- sig do
54
- params(
55
- node: T.any(YARP::ConstantWriteNode, YARP::ConstantOrWriteNode),
56
- ).void
57
- end
58
- def add_constant(node)
59
- comments = collect_comments(node)
60
- @index << Index::Entry::Constant.new(fully_qualify_name(node.name.to_s), @file_path, node.location, comments)
63
+ sig { params(node: YARP::CallNode).void }
64
+ def handle_private_constant(node)
65
+ arguments = node.arguments&.arguments
66
+ return unless arguments
67
+
68
+ first_argument = arguments.first
69
+
70
+ name = case first_argument
71
+ when YARP::StringNode
72
+ first_argument.content
73
+ when YARP::SymbolNode
74
+ first_argument.value
75
+ end
76
+
77
+ return unless name
78
+
79
+ receiver = node.receiver
80
+ name = "#{receiver.slice}::#{name}" if receiver
81
+
82
+ # The private_constant method does not resolve the constant name. It always points to a constant that needs to
83
+ # exist in the current namespace
84
+ entries = @index[fully_qualify_name(name)]
85
+ entries&.each { |entry| entry.visibility = :private }
61
86
  end
62
87
 
63
88
  sig do
64
89
  params(
65
- node: T.any(YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode),
90
+ node: T.any(
91
+ YARP::ConstantWriteNode,
92
+ YARP::ConstantOrWriteNode,
93
+ YARP::ConstantPathWriteNode,
94
+ YARP::ConstantPathOrWriteNode,
95
+ YARP::ConstantPathOperatorWriteNode,
96
+ YARP::ConstantPathAndWriteNode,
97
+ ),
98
+ name: String,
66
99
  ).void
67
100
  end
68
- def add_constant_with_path(node)
69
- # ignore variable constants like `var::FOO` or `self.class::FOO`
70
- return unless node.target.parent.nil? || node.target.parent.is_a?(YARP::ConstantReadNode)
71
-
72
- name = node.target.location.slice
101
+ def add_constant(node, name)
102
+ value = node.value
73
103
  comments = collect_comments(node)
74
- @index << Index::Entry::Constant.new(fully_qualify_name(name), @file_path, node.location, comments)
104
+
105
+ @index << case value
106
+ when YARP::ConstantReadNode, YARP::ConstantPathNode
107
+ Index::Entry::UnresolvedAlias.new(value.slice, @stack.dup, name, @file_path, node.location, comments)
108
+ when YARP::ConstantWriteNode, YARP::ConstantAndWriteNode, YARP::ConstantOrWriteNode,
109
+ YARP::ConstantOperatorWriteNode
110
+
111
+ # If the right hand side is another constant assignment, we need to visit it because that constant has to be
112
+ # indexed too
113
+ visit(value)
114
+ Index::Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments)
115
+ when YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode, YARP::ConstantPathOperatorWriteNode,
116
+ YARP::ConstantPathAndWriteNode
117
+
118
+ visit(value)
119
+ Index::Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments)
120
+ else
121
+ Index::Entry::Constant.new(name, @file_path, node.location, comments)
122
+ end
75
123
  end
76
124
 
77
125
  sig { params(node: T.any(YARP::ClassNode, YARP::ModuleNode), klass: T.class_of(Index::Entry)).void }
@@ -216,5 +216,28 @@ module RubyIndexer
216
216
  second_foo_entry = @index["Bar"][0]
217
217
  assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n"))
218
218
  end
219
+
220
+ def test_private_class_and_module_indexing
221
+ index(<<~RUBY)
222
+ class A
223
+ class B; end
224
+ private_constant(:B)
225
+
226
+ module C; end
227
+ private_constant("C")
228
+
229
+ class D; end
230
+ end
231
+ RUBY
232
+
233
+ b_const = @index["A::B"].first
234
+ assert_equal(:private, b_const.visibility)
235
+
236
+ c_const = @index["A::C"].first
237
+ assert_equal(:private, c_const.visibility)
238
+
239
+ d_const = @index["A::D"].first
240
+ assert_equal(:public, d_const.visibility)
241
+ end
219
242
  end
220
243
  end
@@ -15,6 +15,8 @@ module RubyIndexer
15
15
 
16
16
  assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") })
17
17
  assert(indexables.none? { |indexable| indexable.full_path.include?("minitest-reporters") })
18
+ assert(indexables.none? { |indexable| indexable.full_path.include?("ansi") })
19
+ assert(indexables.any? { |indexable| indexable.full_path.include?("sorbet-runtime") })
18
20
  assert(indexables.none? { |indexable| indexable.full_path == __FILE__ })
19
21
  end
20
22
 
@@ -104,5 +104,207 @@ module RubyIndexer
104
104
 
105
105
  assert_no_entry
106
106
  end
107
+
108
+ def test_private_constant_indexing
109
+ index(<<~RUBY)
110
+ class A
111
+ B = 1
112
+ private_constant(:B)
113
+
114
+ C = 2
115
+ private_constant("C")
116
+
117
+ D = 1
118
+ end
119
+ RUBY
120
+
121
+ b_const = @index["A::B"].first
122
+ assert_equal(:private, b_const.visibility)
123
+
124
+ c_const = @index["A::C"].first
125
+ assert_equal(:private, c_const.visibility)
126
+
127
+ d_const = @index["A::D"].first
128
+ assert_equal(:public, d_const.visibility)
129
+ end
130
+
131
+ def test_marking_constants_as_private_reopening_namespaces
132
+ index(<<~RUBY)
133
+ module A
134
+ module B
135
+ CONST_A = 1
136
+ private_constant(:CONST_A)
137
+
138
+ CONST_B = 2
139
+ CONST_C = 3
140
+ end
141
+
142
+ module B
143
+ private_constant(:CONST_B)
144
+ end
145
+ end
146
+
147
+ module A
148
+ module B
149
+ private_constant(:CONST_C)
150
+ end
151
+ end
152
+ RUBY
153
+
154
+ a_const = @index["A::B::CONST_A"].first
155
+ assert_equal(:private, a_const.visibility)
156
+
157
+ b_const = @index["A::B::CONST_B"].first
158
+ assert_equal(:private, b_const.visibility)
159
+
160
+ c_const = @index["A::B::CONST_C"].first
161
+ assert_equal(:private, c_const.visibility)
162
+ end
163
+
164
+ def test_marking_constants_as_private_with_receiver
165
+ index(<<~RUBY)
166
+ module A
167
+ module B
168
+ CONST_A = 1
169
+ CONST_B = 2
170
+ end
171
+
172
+ B.private_constant(:CONST_A)
173
+ end
174
+
175
+ A::B.private_constant(:CONST_B)
176
+ RUBY
177
+
178
+ a_const = @index["A::B::CONST_A"].first
179
+ assert_equal(:private, a_const.visibility)
180
+
181
+ b_const = @index["A::B::CONST_B"].first
182
+ assert_equal(:private, b_const.visibility)
183
+ end
184
+
185
+ def test_indexing_constant_aliases
186
+ index(<<~RUBY)
187
+ module A
188
+ module B
189
+ module C
190
+ end
191
+ end
192
+
193
+ FIRST = B::C
194
+ end
195
+
196
+ SECOND = A::FIRST
197
+ RUBY
198
+
199
+ unresolve_entry = @index["A::FIRST"].first
200
+ assert_instance_of(Index::Entry::UnresolvedAlias, unresolve_entry)
201
+ assert_equal(["A"], unresolve_entry.nesting)
202
+ assert_equal("B::C", unresolve_entry.target)
203
+
204
+ resolved_entry = @index.resolve("A::FIRST", []).first
205
+ assert_instance_of(Index::Entry::Alias, resolved_entry)
206
+ assert_equal("A::B::C", resolved_entry.target)
207
+ end
208
+
209
+ def test_aliasing_namespaces
210
+ index(<<~RUBY)
211
+ module A
212
+ module B
213
+ module C
214
+ end
215
+ end
216
+
217
+ ALIAS = B
218
+ end
219
+
220
+ module Other
221
+ ONE_MORE = A::ALIAS
222
+ end
223
+ RUBY
224
+
225
+ unresolve_entry = @index["A::ALIAS"].first
226
+ assert_instance_of(Index::Entry::UnresolvedAlias, unresolve_entry)
227
+ assert_equal(["A"], unresolve_entry.nesting)
228
+ assert_equal("B", unresolve_entry.target)
229
+
230
+ resolved_entry = @index.resolve("ALIAS", ["A"]).first
231
+ assert_instance_of(Index::Entry::Alias, resolved_entry)
232
+ assert_equal("A::B", resolved_entry.target)
233
+
234
+ resolved_entry = @index.resolve("ALIAS::C", ["A"]).first
235
+ assert_instance_of(Index::Entry::Module, resolved_entry)
236
+ assert_equal("A::B::C", resolved_entry.name)
237
+
238
+ unresolve_entry = @index["Other::ONE_MORE"].first
239
+ assert_instance_of(Index::Entry::UnresolvedAlias, unresolve_entry)
240
+ assert_equal(["Other"], unresolve_entry.nesting)
241
+ assert_equal("A::ALIAS", unresolve_entry.target)
242
+
243
+ resolved_entry = @index.resolve("Other::ONE_MORE::C", []).first
244
+ assert_instance_of(Index::Entry::Module, resolved_entry)
245
+ end
246
+
247
+ def test_indexing_same_line_constant_aliases
248
+ index(<<~RUBY)
249
+ module A
250
+ B = C = 1
251
+ D = E ||= 1
252
+ F = G::H &&= 1
253
+ I::J = K::L = M = 1
254
+ end
255
+ RUBY
256
+
257
+ # B and C
258
+ unresolve_entry = @index["A::B"].first
259
+ assert_instance_of(Index::Entry::UnresolvedAlias, unresolve_entry)
260
+ assert_equal(["A"], unresolve_entry.nesting)
261
+ assert_equal("C", unresolve_entry.target)
262
+
263
+ resolved_entry = @index.resolve("A::B", []).first
264
+ assert_instance_of(Index::Entry::Alias, resolved_entry)
265
+ assert_equal("A::C", resolved_entry.target)
266
+
267
+ constant = @index["A::C"].first
268
+ assert_instance_of(Index::Entry::Constant, constant)
269
+
270
+ # D and E
271
+ unresolve_entry = @index["A::D"].first
272
+ assert_instance_of(Index::Entry::UnresolvedAlias, unresolve_entry)
273
+ assert_equal(["A"], unresolve_entry.nesting)
274
+ assert_equal("E", unresolve_entry.target)
275
+
276
+ resolved_entry = @index.resolve("A::D", []).first
277
+ assert_instance_of(Index::Entry::Alias, resolved_entry)
278
+ assert_equal("A::E", resolved_entry.target)
279
+
280
+ # F and G::H
281
+ unresolve_entry = @index["A::F"].first
282
+ assert_instance_of(Index::Entry::UnresolvedAlias, unresolve_entry)
283
+ assert_equal(["A"], unresolve_entry.nesting)
284
+ assert_equal("G::H", unresolve_entry.target)
285
+
286
+ resolved_entry = @index.resolve("A::F", []).first
287
+ assert_instance_of(Index::Entry::Alias, resolved_entry)
288
+ assert_equal("A::G::H", resolved_entry.target)
289
+
290
+ # I::J, K::L and M
291
+ unresolve_entry = @index["A::I::J"].first
292
+ assert_instance_of(Index::Entry::UnresolvedAlias, unresolve_entry)
293
+ assert_equal(["A"], unresolve_entry.nesting)
294
+ assert_equal("K::L", unresolve_entry.target)
295
+
296
+ resolved_entry = @index.resolve("A::I::J", []).first
297
+ assert_instance_of(Index::Entry::Alias, resolved_entry)
298
+ assert_equal("A::K::L", resolved_entry.target)
299
+
300
+ # When we are resolving A::I::J, we invoke `resolve("K::L", ["A"])`, which recursively resolves A::K::L too.
301
+ # Therefore, both A::I::J and A::K::L point to A::M by the end of the previous resolve invocation
302
+ resolved_entry = @index["A::K::L"].first
303
+ assert_instance_of(Index::Entry::Alias, resolved_entry)
304
+ assert_equal("A::M", resolved_entry.target)
305
+
306
+ constant = @index["A::M"].first
307
+ assert_instance_of(Index::Entry::Constant, constant)
308
+ end
107
309
  end
108
310
  end
@@ -158,5 +158,25 @@ module RubyIndexer
158
158
  results = @index.prefix_search("Ba", ["Foo"]).map { |entries| entries.map(&:name) }
159
159
  assert_equal([["Foo::Bar", "Foo::Bar"], ["Foo::Baz"]], results)
160
160
  end
161
+
162
+ def test_resolve_normalizes_top_level_names
163
+ @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY)
164
+ class Bar; end
165
+
166
+ module Foo
167
+ class Bar; end
168
+ end
169
+ RUBY
170
+
171
+ entries = @index.resolve("::Foo::Bar", [])
172
+ refute_nil(entries)
173
+
174
+ assert_equal("Foo::Bar", entries.first.name)
175
+
176
+ entries = @index.resolve("::Bar", ["Foo"])
177
+ refute_nil(entries)
178
+
179
+ assert_equal("Bar", entries.first.name)
180
+ end
161
181
  end
162
182
  end