ruby-lsp 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) 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 +35 -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 +39 -16
  9. data/lib/ruby_indexer/test/configuration_test.rb +2 -0
  10. data/lib/ruby_indexer/test/constant_test.rb +213 -11
  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 +53 -10
  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/setup_bundler.rb +22 -11
  47. data/lib/ruby_lsp/utils.rb +2 -12
  48. metadata +11 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98731986ff16a22d1854fe971bfa188d55212665128ad635aabbf519c1097d9f
4
- data.tar.gz: fe416e79e672448b3d2477909fec47451c6f878020ec92e2178259d933c51cb4
3
+ metadata.gz: 7491255b178e72942f0b98ae97c83020c0a5726162d556fba7a2592a2c8b5132
4
+ data.tar.gz: d9c61449b9d3eb43a53a8c00b704ceab8691bb404dedd3324d3ad055d859974d
5
5
  SHA512:
6
- metadata.gz: ad9a36886ffb6b1370b2530ffdb84a89244e22ec2329ee111d70bea8e90ad7ebe6c92b87dd437e283270cb09a7d564b3471d7ddcdc51df86df54d4bdfd09ada8
7
- data.tar.gz: 9a89e7a763d1a74a320ca8dd544a52ead86a3bd573204ad86585dce7dc7797ac993f3716b98e9e6cdfd9d5b6b7a8f17e9743aa149866945d099e7d9210719a28
6
+ metadata.gz: 88b6b4d22784336e299caa42e09a701bd9c70ac3176c224c00700ad80605dd8ad17d4274eedc95dad4f6c6432fb192c2094ca8b5ca66ce3ab95bf7ba3eb68831
7
+ data.tar.gz: caabe89725f634f87ace44cc85c4d001a8f74dbbb0f2d59f1e2e7a763a3b7896e72a6432abbba7e85e07646ebfc7c812c1126ef8afda520a9f827b58a8cc881d
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.0
1
+ 0.11.0
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,39 @@ 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 { |d| d.to_spec.full_gem_path == Dir.pwd }
183
+ others.concat(this_gem.to_spec.dependencies) if this_gem
184
+
185
+ excluded.each do |dependency|
186
+ next unless dependency.runtime?
187
+
188
+ dependency.to_spec.dependencies.each do |transitive_dependency|
189
+ # If the transitive dependency is included in other groups, skip it
190
+ next if others.any? { |d| d.name == transitive_dependency.name }
191
+
192
+ # If the transitive dependency is included as a transitive dependency of a gem outside of the development
193
+ # group, skip it
194
+ next if others.any? { |d| d.to_spec.dependencies.include?(transitive_dependency) }
195
+
196
+ excluded << transitive_dependency
197
+ end
198
+ rescue Gem::MissingSpecError
199
+ # If a gem is scoped only to some specific platform, then its dependencies may not be installed either, but they
200
+ # are still listed in dependencies. We can't index them because they are not installed for the platform, so we
201
+ # just ignore if they're missing
202
+ end
203
+
204
+ excluded.uniq!
205
+ excluded.map(&:name)
206
+ end
177
207
  end
178
208
  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), @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 }
@@ -11,7 +11,7 @@ module RubyIndexer
11
11
  end
12
12
  RUBY
13
13
 
14
- assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
14
+ assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
15
15
  end
16
16
 
17
17
  def test_class_with_statements
@@ -21,7 +21,7 @@ module RubyIndexer
21
21
  end
22
22
  RUBY
23
23
 
24
- assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:2-2")
24
+ assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:2-3")
25
25
  end
26
26
 
27
27
  def test_colon_colon_class
@@ -30,7 +30,7 @@ module RubyIndexer
30
30
  end
31
31
  RUBY
32
32
 
33
- assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
33
+ assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
34
34
  end
35
35
 
36
36
  def test_colon_colon_class_inside_class
@@ -41,8 +41,8 @@ module RubyIndexer
41
41
  end
42
42
  RUBY
43
43
 
44
- assert_entry("Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:3-2")
45
- assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-4")
44
+ assert_entry("Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:3-3")
45
+ assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-5")
46
46
  end
47
47
 
48
48
  def test_namespaced_class
@@ -51,7 +51,7 @@ module RubyIndexer
51
51
  end
52
52
  RUBY
53
53
 
54
- assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
54
+ assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
55
55
  end
56
56
 
57
57
  def test_dynamically_namespaced_class
@@ -69,7 +69,7 @@ module RubyIndexer
69
69
  end
70
70
  RUBY
71
71
 
72
- assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-2")
72
+ assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-3")
73
73
  end
74
74
 
75
75
  def test_module_with_statements
@@ -79,7 +79,7 @@ module RubyIndexer
79
79
  end
80
80
  RUBY
81
81
 
82
- assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:2-2")
82
+ assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:2-3")
83
83
  end
84
84
 
85
85
  def test_colon_colon_module
@@ -88,7 +88,7 @@ module RubyIndexer
88
88
  end
89
89
  RUBY
90
90
 
91
- assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-2")
91
+ assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-3")
92
92
  end
93
93
 
94
94
  def test_namespaced_module
@@ -97,7 +97,7 @@ module RubyIndexer
97
97
  end
98
98
  RUBY
99
99
 
100
- assert_entry("Foo::Bar", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-2")
100
+ assert_entry("Foo::Bar", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-3")
101
101
  end
102
102
 
103
103
  def test_dynamically_namespaced_module
@@ -124,11 +124,11 @@ module RubyIndexer
124
124
  end
125
125
  RUBY
126
126
 
127
- assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:10-2")
128
- assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-4")
129
- assert_entry("Foo::Baz", Index::Entry::Module, "/fake/path/foo.rb:4-2:9-4")
130
- assert_entry("Foo::Baz::Qux", Index::Entry::Class, "/fake/path/foo.rb:5-4:8-6")
131
- assert_entry("Foo::Baz::Qux::Something", Index::Entry::Class, "/fake/path/foo.rb:6-6:7-8")
127
+ assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:10-3")
128
+ assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-5")
129
+ assert_entry("Foo::Baz", Index::Entry::Module, "/fake/path/foo.rb:4-2:9-5")
130
+ assert_entry("Foo::Baz::Qux", Index::Entry::Class, "/fake/path/foo.rb:5-4:8-7")
131
+ assert_entry("Foo::Baz::Qux::Something", Index::Entry::Class, "/fake/path/foo.rb:6-6:7-9")
132
132
  end
133
133
 
134
134
  def test_deleting_from_index_based_on_file_path
@@ -137,7 +137,7 @@ module RubyIndexer
137
137
  end
138
138
  RUBY
139
139
 
140
- assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
140
+ assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
141
141
 
142
142
  @index.delete(IndexablePath.new(nil, "/fake/path/foo.rb"))
143
143
  refute_entry("Foo")
@@ -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