ruby-lsp 0.10.1 → 0.11.1

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