ruby-lsp 0.20.1 → 0.22.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +19 -4
  5. data/exe/ruby-lsp-launcher +124 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +233 -59
  8. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +34 -16
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +15 -15
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +4 -4
  12. data/lib/ruby_indexer/test/configuration_test.rb +10 -0
  13. data/lib/ruby_indexer/test/constant_test.rb +8 -8
  14. data/lib/ruby_indexer/test/enhancements_test.rb +169 -41
  15. data/lib/ruby_indexer/test/index_test.rb +41 -2
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  17. data/lib/ruby_indexer/test/method_test.rb +139 -0
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  19. data/lib/ruby_lsp/addon.rb +9 -2
  20. data/lib/ruby_lsp/base_server.rb +14 -5
  21. data/lib/ruby_lsp/client_capabilities.rb +67 -0
  22. data/lib/ruby_lsp/document.rb +1 -1
  23. data/lib/ruby_lsp/global_state.rb +33 -20
  24. data/lib/ruby_lsp/internal.rb +3 -0
  25. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  26. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  27. data/lib/ruby_lsp/listeners/document_highlight.rb +91 -4
  28. data/lib/ruby_lsp/listeners/document_symbol.rb +37 -4
  29. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  30. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  31. data/lib/ruby_lsp/requests/completion.rb +7 -1
  32. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  33. data/lib/ruby_lsp/requests/definition.rb +28 -11
  34. data/lib/ruby_lsp/requests/document_highlight.rb +7 -1
  35. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  36. data/lib/ruby_lsp/requests/hover.rb +26 -6
  37. data/lib/ruby_lsp/requests/rename.rb +1 -1
  38. data/lib/ruby_lsp/requests/request.rb +1 -1
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +12 -1
  40. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  41. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  42. data/lib/ruby_lsp/server.rb +85 -55
  43. data/lib/ruby_lsp/setup_bundler.rb +154 -47
  44. data/lib/ruby_lsp/store.rb +0 -4
  45. data/lib/ruby_lsp/utils.rb +63 -0
  46. metadata +8 -3
@@ -646,7 +646,7 @@ module RubyIndexer
646
646
  (positionals.empty? && forwarding_arguments.any?) ||
647
647
  (
648
648
  # Check if positional arguments match. This includes required, optional, rest arguments. We also need to
649
- # verify if there's a trailing forwading argument, like `def foo(a, ...); end`
649
+ # verify if there's a trailing forwarding argument, like `def foo(a, ...); end`
650
650
  positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) &&
651
651
  # If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest
652
652
  # arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest
@@ -7,6 +7,7 @@ module RubyIndexer
7
7
 
8
8
  class UnresolvableAliasError < StandardError; end
9
9
  class NonExistingNamespaceError < StandardError; end
10
+ class IndexNotEmptyError < StandardError; end
10
11
 
11
12
  # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
12
13
  ENTRY_SIMILARITY_THRESHOLD = 0.7
@@ -39,9 +40,6 @@ module RubyIndexer
39
40
  # Holds the linearized ancestors list for every namespace
40
41
  @ancestors = T.let({}, T::Hash[String, T::Array[String]])
41
42
 
42
- # List of classes that are enhancing the index
43
- @enhancements = T.let([], T::Array[Enhancement])
44
-
45
43
  # Map of module name to included hooks that have to be executed when we include the given module
46
44
  @included_hooks = T.let(
47
45
  {},
@@ -51,12 +49,6 @@ module RubyIndexer
51
49
  @configuration = T.let(RubyIndexer::Configuration.new, Configuration)
52
50
  end
53
51
 
54
- # Register an enhancement to the index. Enhancements must conform to the `Enhancement` interface
55
- sig { params(enhancement: Enhancement).void }
56
- def register_enhancement(enhancement)
57
- @enhancements << enhancement
58
- end
59
-
60
52
  # Register an included `hook` that will be executed when `module_name` is included into any namespace
61
53
  sig { params(module_name: String, hook: T.proc.params(index: Index, base: Entry::Namespace).void).void }
62
54
  def register_included_hook(module_name, &hook)
@@ -360,6 +352,15 @@ module RubyIndexer
360
352
  ).void
361
353
  end
362
354
  def index_all(indexable_paths: @configuration.indexables, &block)
355
+ # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the
356
+ # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this
357
+ # behavior and can take appropriate action.
358
+ # binding.break
359
+ if @entries.any?
360
+ raise IndexNotEmptyError,
361
+ "The index is not empty. To prevent invalid entries, `index_all` can only be called once."
362
+ end
363
+
363
364
  RBSIndexer.new(self).index_ruby_core
364
365
  # Calculate how many paths are worth 1% of progress
365
366
  progress_step = (indexable_paths.length / 100.0).ceil
@@ -386,7 +387,6 @@ module RubyIndexer
386
387
  result,
387
388
  indexable_path.full_path,
388
389
  collect_comments: collect_comments,
389
- enhancements: @enhancements,
390
390
  )
391
391
  dispatcher.dispatch(result.value)
392
392
 
@@ -784,7 +784,7 @@ module RubyIndexer
784
784
  singleton_levels
785
785
  )
786
786
  # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
787
- # from two diffent classes in different files, we simply ignore it
787
+ # from two different classes in different files, we simply ignore it
788
788
  superclass = T.cast(
789
789
  if singleton_levels > 0
790
790
  self[attached_class_name]&.find { |n| n.is_a?(Entry::Class) && n.parent_class }
@@ -974,10 +974,10 @@ module RubyIndexer
974
974
  []
975
975
  end
976
976
 
977
- # Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
978
- # of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
979
- # `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
980
- # nesting
977
+ # Removes redundancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo`
978
+ # inside of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up
979
+ # with `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and
980
+ # the nesting
981
981
  sig { params(name: String, nesting: T::Array[String]).returns(String) }
982
982
  def build_non_redundant_full_name(name, nesting)
983
983
  # If there's no nesting, then we can just return the name as is
@@ -72,7 +72,7 @@ module RubyIndexer
72
72
  assert_entry("self::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
73
73
  end
74
74
 
75
- def test_dynamically_namespaced_class_doesnt_affect_other_classes
75
+ def test_dynamically_namespaced_class_does_not_affect_other_classes
76
76
  index(<<~RUBY)
77
77
  class Foo
78
78
  class self::Bar
@@ -143,7 +143,7 @@ module RubyIndexer
143
143
  assert_entry("self::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
144
144
  end
145
145
 
146
- def test_dynamically_namespaced_module_doesnt_affect_other_modules
146
+ def test_dynamically_namespaced_module_does_not_affect_other_modules
147
147
  index(<<~RUBY)
148
148
  module Foo
149
149
  class self::Bar
@@ -302,10 +302,10 @@ module RubyIndexer
302
302
  RUBY
303
303
 
304
304
  b_const = @index["A::B"].first
305
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
305
+ assert_predicate(b_const, :private?)
306
306
 
307
307
  c_const = @index["A::C"].first
308
- assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
308
+ assert_predicate(c_const, :private?)
309
309
 
310
310
  d_const = @index["A::D"].first
311
311
  assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
@@ -160,5 +160,15 @@ module RubyIndexer
160
160
  )
161
161
  end
162
162
  end
163
+
164
+ def test_includes_top_level_files
165
+ Dir.mktmpdir do |dir|
166
+ FileUtils.touch(File.join(dir, "find_me.rb"))
167
+ @config.workspace_path = dir
168
+
169
+ indexables = @config.indexables
170
+ assert(indexables.find { |i| File.basename(i.full_path) == "find_me.rb" })
171
+ end
172
+ end
163
173
  end
164
174
  end
@@ -130,13 +130,13 @@ module RubyIndexer
130
130
  RUBY
131
131
 
132
132
  b_const = @index["A::B"].first
133
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
133
+ assert_predicate(b_const, :private?)
134
134
 
135
135
  c_const = @index["A::C"].first
136
- assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
136
+ assert_predicate(c_const, :private?)
137
137
 
138
138
  d_const = @index["A::D"].first
139
- assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
139
+ assert_predicate(d_const, :public?)
140
140
  end
141
141
 
142
142
  def test_marking_constants_as_private_reopening_namespaces
@@ -163,13 +163,13 @@ module RubyIndexer
163
163
  RUBY
164
164
 
165
165
  a_const = @index["A::B::CONST_A"].first
166
- assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
166
+ assert_predicate(a_const, :private?)
167
167
 
168
168
  b_const = @index["A::B::CONST_B"].first
169
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
169
+ assert_predicate(b_const, :private?)
170
170
 
171
171
  c_const = @index["A::B::CONST_C"].first
172
- assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
172
+ assert_predicate(c_const, :private?)
173
173
  end
174
174
 
175
175
  def test_marking_constants_as_private_with_receiver
@@ -187,10 +187,10 @@ module RubyIndexer
187
187
  RUBY
188
188
 
189
189
  a_const = @index["A::B::CONST_A"].first
190
- assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
190
+ assert_predicate(a_const, :private?)
191
191
 
192
192
  b_const = @index["A::B::CONST_B"].first
193
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
193
+ assert_predicate(b_const, :private?)
194
194
  end
195
195
 
196
196
  def test_indexing_constant_aliases
@@ -5,26 +5,28 @@ require_relative "test_case"
5
5
 
6
6
  module RubyIndexer
7
7
  class EnhancementTest < TestCase
8
- def test_enhancing_indexing_included_hook
9
- enhancement_class = Class.new do
10
- include Enhancement
8
+ def teardown
9
+ super
10
+ Enhancement.clear
11
+ end
11
12
 
12
- def on_call_node(index, owner, node, file_path, code_units_cache)
13
+ def test_enhancing_indexing_included_hook
14
+ Class.new(Enhancement) do
15
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
16
+ owner = @listener.current_owner
13
17
  return unless owner
14
- return unless node.name == :extend
18
+ return unless call_node.name == :extend
15
19
 
16
- arguments = node.arguments&.arguments
20
+ arguments = call_node.arguments&.arguments
17
21
  return unless arguments
18
22
 
19
- location = Location.from_prism_location(node.location, code_units_cache)
20
-
21
23
  arguments.each do |node|
22
24
  next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
23
25
 
24
26
  module_name = node.full_name
25
27
  next unless module_name == "ActiveSupport::Concern"
26
28
 
27
- index.register_included_hook(owner.name) do |index, base|
29
+ @listener.register_included_hook do |index, base|
28
30
  class_methods_name = "#{owner.name}::ClassMethods"
29
31
 
30
32
  if index.indexed?(class_methods_name)
@@ -33,16 +35,11 @@ module RubyIndexer
33
35
  end
34
36
  end
35
37
 
36
- index.add(Entry::Method.new(
38
+ @listener.add_method(
37
39
  "new_method",
38
- file_path,
39
- location,
40
- location,
41
- nil,
40
+ call_node.location,
42
41
  [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
43
- Entry::Visibility::PUBLIC,
44
- owner,
45
- ))
42
+ )
46
43
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
47
44
  Prism::ConstantPathNode::MissingNodesInConstantPathError
48
45
  # Do nothing
@@ -50,7 +47,6 @@ module RubyIndexer
50
47
  end
51
48
  end
52
49
 
53
- @index.register_enhancement(enhancement_class.new)
54
50
  index(<<~RUBY)
55
51
  module ActiveSupport
56
52
  module Concern
@@ -98,11 +94,9 @@ module RubyIndexer
98
94
  end
99
95
 
100
96
  def test_enhancing_indexing_configuration_dsl
101
- enhancement_class = Class.new do
102
- include Enhancement
103
-
104
- def on_call_node(index, owner, node, file_path, code_units_cache)
105
- return unless owner
97
+ Class.new(Enhancement) do
98
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
99
+ return unless @listener.current_owner
106
100
 
107
101
  name = node.name
108
102
  return unless name == :has_many
@@ -113,22 +107,14 @@ module RubyIndexer
113
107
  association_name = arguments.first
114
108
  return unless association_name.is_a?(Prism::SymbolNode)
115
109
 
116
- location = Location.from_prism_location(association_name.location, code_units_cache)
117
-
118
- index.add(Entry::Method.new(
110
+ @listener.add_method(
119
111
  T.must(association_name.value),
120
- file_path,
121
- location,
122
- location,
123
- nil,
112
+ association_name.location,
124
113
  [],
125
- Entry::Visibility::PUBLIC,
126
- owner,
127
- ))
114
+ )
128
115
  end
129
116
  end
130
117
 
131
- @index.register_enhancement(enhancement_class.new)
132
118
  index(<<~RUBY)
133
119
  module ActiveSupport
134
120
  module Concern
@@ -160,11 +146,9 @@ module RubyIndexer
160
146
  assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17")
161
147
  end
162
148
 
163
- def test_error_handling_in_enhancement
164
- enhancement_class = Class.new do
165
- include Enhancement
166
-
167
- def on_call_node(index, owner, node, file_path, code_units_cache)
149
+ def test_error_handling_in_on_call_node_enter_enhancement
150
+ Class.new(Enhancement) do
151
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
168
152
  raise "Error"
169
153
  end
170
154
 
@@ -175,7 +159,38 @@ module RubyIndexer
175
159
  end
176
160
  end
177
161
 
178
- @index.register_enhancement(enhancement_class.new)
162
+ _stdout, stderr = capture_io do
163
+ index(<<~RUBY)
164
+ module ActiveSupport
165
+ module Concern
166
+ def self.extended(base)
167
+ base.class_eval("def new_method(a); end")
168
+ end
169
+ end
170
+ end
171
+ RUBY
172
+ end
173
+
174
+ assert_match(
175
+ %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node enter enhancement},
176
+ stderr,
177
+ )
178
+ # The module should still be indexed
179
+ assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
180
+ end
181
+
182
+ def test_error_handling_in_on_call_node_leave_enhancement
183
+ Class.new(Enhancement) do
184
+ def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
185
+ raise "Error"
186
+ end
187
+
188
+ class << self
189
+ def name
190
+ "TestEnhancement"
191
+ end
192
+ end
193
+ end
179
194
 
180
195
  _stdout, stderr = capture_io do
181
196
  index(<<~RUBY)
@@ -189,9 +204,122 @@ module RubyIndexer
189
204
  RUBY
190
205
  end
191
206
 
192
- assert_match(%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' enhancement}, stderr)
207
+ assert_match(
208
+ %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node leave enhancement},
209
+ stderr,
210
+ )
193
211
  # The module should still be indexed
194
212
  assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
195
213
  end
214
+
215
+ def test_advancing_namespace_stack_from_enhancement
216
+ Class.new(Enhancement) do
217
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
218
+ owner = @listener.current_owner
219
+ return unless owner
220
+
221
+ case call_node.name
222
+ when :class_methods
223
+ @listener.add_module("ClassMethods", call_node.location, call_node.location)
224
+ when :extend
225
+ arguments = call_node.arguments&.arguments
226
+ return unless arguments
227
+
228
+ arguments.each do |node|
229
+ next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
230
+
231
+ module_name = node.full_name
232
+ next unless module_name == "ActiveSupport::Concern"
233
+
234
+ @listener.register_included_hook do |index, base|
235
+ class_methods_name = "#{owner.name}::ClassMethods"
236
+
237
+ if index.indexed?(class_methods_name)
238
+ singleton = index.existing_or_new_singleton_class(base.name)
239
+ singleton.mixin_operations << Entry::Include.new(class_methods_name)
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
247
+ return unless call_node.name == :class_methods
248
+
249
+ @listener.pop_namespace_stack
250
+ end
251
+ end
252
+
253
+ index(<<~RUBY)
254
+ module ActiveSupport
255
+ module Concern
256
+ end
257
+ end
258
+
259
+ module MyConcern
260
+ extend ActiveSupport::Concern
261
+
262
+ class_methods do
263
+ def foo; end
264
+ end
265
+ end
266
+
267
+ class User
268
+ include MyConcern
269
+ end
270
+ RUBY
271
+
272
+ assert_equal(
273
+ [
274
+ "User::<Class:User>",
275
+ "MyConcern::ClassMethods",
276
+ "Object::<Class:Object>",
277
+ "BasicObject::<Class:BasicObject>",
278
+ "Class",
279
+ "Module",
280
+ "Object",
281
+ "Kernel",
282
+ "BasicObject",
283
+ ],
284
+ @index.linearized_ancestors_of("User::<Class:User>"),
285
+ )
286
+
287
+ refute_nil(@index.resolve_method("foo", "User::<Class:User>"))
288
+ end
289
+
290
+ def test_creating_anonymous_classes_from_enhancement
291
+ Class.new(Enhancement) do
292
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
293
+ case call_node.name
294
+ when :context
295
+ arguments = call_node.arguments&.arguments
296
+ first_argument = arguments&.first
297
+ return unless first_argument.is_a?(Prism::StringNode)
298
+
299
+ @listener.add_class(
300
+ "<RSpec:#{first_argument.content}>",
301
+ call_node.location,
302
+ first_argument.location,
303
+ )
304
+ when :subject
305
+ @listener.add_method("subject", call_node.location, [])
306
+ end
307
+ end
308
+
309
+ def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
310
+ return unless call_node.name == :context
311
+
312
+ @listener.pop_namespace_stack
313
+ end
314
+ end
315
+
316
+ index(<<~RUBY)
317
+ context "does something" do
318
+ subject { call_whatever }
319
+ end
320
+ RUBY
321
+
322
+ refute_nil(@index.resolve_method("subject", "<RSpec:does something>"))
323
+ end
196
324
  end
197
325
  end
@@ -904,7 +904,7 @@ module RubyIndexer
904
904
  assert_equal(14, entry.location.start_line)
905
905
  end
906
906
 
907
- def test_resolving_inherited_alised_namespace
907
+ def test_resolving_inherited_aliased_namespace
908
908
  index(<<~RUBY)
909
909
  module Bar
910
910
  TARGET = 123
@@ -1490,7 +1490,7 @@ module RubyIndexer
1490
1490
  assert_kind_of(Entry::UnresolvedMethodAlias, entry)
1491
1491
  end
1492
1492
 
1493
- def test_unresolable_method_aliases
1493
+ def test_unresolvable_method_aliases
1494
1494
  index(<<~RUBY)
1495
1495
  class Foo
1496
1496
  alias bar baz
@@ -1672,6 +1672,38 @@ module RubyIndexer
1672
1672
  )
1673
1673
  end
1674
1674
 
1675
+ def test_extend_self
1676
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
1677
+ module Foo
1678
+ def bar
1679
+ end
1680
+
1681
+ extend self
1682
+
1683
+ def baz
1684
+ end
1685
+ end
1686
+ RUBY
1687
+
1688
+ ["bar", "baz"].product(["Foo", "Foo::<Class:Foo>"]).each do |method, receiver|
1689
+ entry = @index.resolve_method(method, receiver)&.first
1690
+ refute_nil(entry)
1691
+ assert_equal(method, T.must(entry).name)
1692
+ end
1693
+
1694
+ assert_equal(
1695
+ [
1696
+ "Foo::<Class:Foo>",
1697
+ "Foo",
1698
+ "Module",
1699
+ "Object",
1700
+ "Kernel",
1701
+ "BasicObject",
1702
+ ],
1703
+ @index.linearized_ancestors_of("Foo::<Class:Foo>"),
1704
+ )
1705
+ end
1706
+
1675
1707
  def test_linearizing_singleton_ancestors
1676
1708
  @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
1677
1709
  module First
@@ -2023,5 +2055,12 @@ module RubyIndexer
2023
2055
  ),
2024
2056
  )
2025
2057
  end
2058
+
2059
+ def test_prevents_multiple_calls_to_index_all
2060
+ # For this test class, `index_all` is already called once in `setup`.
2061
+ assert_raises(Index::IndexNotEmptyError) do
2062
+ @index.index_all
2063
+ end
2064
+ end
2026
2065
  end
2027
2066
  end
@@ -209,7 +209,7 @@ module RubyIndexer
209
209
  end
210
210
  RUBY
211
211
 
212
- # If the surrounding method is beind defined on any dynamic value that isn't `self`, then we attribute the
212
+ # If the surrounding method is being defined on any dynamic value that isn't `self`, then we attribute the
213
213
  # instance variable to the wrong owner since there's no way to understand that statically
214
214
  entry = T.must(@index["@a"]&.first)
215
215
  owner = T.must(entry.owner)
@@ -123,6 +123,145 @@ module RubyIndexer
123
123
  assert_entry("baz", Entry::Method, "/fake/path/foo.rb:9-2:9-14", visibility: Entry::Visibility::PRIVATE)
124
124
  end
125
125
 
126
+ def test_visibility_tracking_with_module_function
127
+ index(<<~RUBY)
128
+ module Test
129
+ def foo; end
130
+ def bar; end
131
+ module_function :foo, "bar"
132
+ end
133
+ RUBY
134
+
135
+ ["foo", "bar"].each do |keyword|
136
+ entries = T.must(@index[keyword])
137
+ # should receive two entries because module_function creates a singleton method
138
+ # for the Test module and a private method for classes include the Test module
139
+ assert_equal(entries.size, 2)
140
+ first_entry, second_entry = *entries
141
+ # The first entry points to the location of the module_function call
142
+ assert_equal("Test", first_entry.owner.name)
143
+ assert_instance_of(Entry::Module, first_entry.owner)
144
+ assert_predicate(first_entry, :private?)
145
+ # The second entry points to the public singleton method
146
+ assert_equal("Test::<Class:Test>", second_entry.owner.name)
147
+ assert_instance_of(Entry::SingletonClass, second_entry.owner)
148
+ assert_equal(Entry::Visibility::PUBLIC, second_entry.visibility)
149
+ end
150
+ end
151
+
152
+ def test_private_class_method_visibility_tracking_string_symbol_arguments
153
+ index(<<~RUBY)
154
+ class Test
155
+ def self.foo
156
+ end
157
+
158
+ def self.bar
159
+ end
160
+
161
+ private_class_method("foo", :bar)
162
+
163
+ def self.baz
164
+ end
165
+ end
166
+ RUBY
167
+
168
+ ["foo", "bar"].each do |keyword|
169
+ entries = T.must(@index[keyword])
170
+ assert_equal(1, entries.size)
171
+ entry = entries.first
172
+ assert_predicate(entry, :private?)
173
+ end
174
+
175
+ entries = T.must(@index["baz"])
176
+ assert_equal(1, entries.size)
177
+ entry = entries.first
178
+ assert_predicate(entry, :public?)
179
+ end
180
+
181
+ def test_private_class_method_visibility_tracking_array_argument
182
+ index(<<~RUBY)
183
+ class Test
184
+ def self.foo
185
+ end
186
+
187
+ def self.bar
188
+ end
189
+
190
+ private_class_method(["foo", :bar])
191
+
192
+ def self.baz
193
+ end
194
+ end
195
+ RUBY
196
+
197
+ ["foo", "bar"].each do |keyword|
198
+ entries = T.must(@index[keyword])
199
+ assert_equal(1, entries.size)
200
+ entry = entries.first
201
+ assert_predicate(entry, :private?)
202
+ end
203
+
204
+ entries = T.must(@index["baz"])
205
+ assert_equal(1, entries.size)
206
+ entry = entries.first
207
+ assert_predicate(entry, :public?)
208
+ end
209
+
210
+ def test_private_class_method_visibility_tracking_method_argument
211
+ index(<<~RUBY)
212
+ class Test
213
+ private_class_method def self.foo
214
+ end
215
+
216
+ def self.bar
217
+ end
218
+ end
219
+ RUBY
220
+
221
+ entries = T.must(@index["foo"])
222
+ assert_equal(1, entries.size)
223
+ entry = entries.first
224
+ assert_predicate(entry, :private?)
225
+
226
+ entries = T.must(@index["bar"])
227
+ assert_equal(1, entries.size)
228
+ entry = entries.first
229
+ assert_predicate(entry, :public?)
230
+ end
231
+
232
+ def test_comments_documentation
233
+ index(<<~RUBY)
234
+ # Documentation for Foo
235
+
236
+ class Foo
237
+ # ####################
238
+ # Documentation for bar
239
+ # ####################
240
+ #
241
+ def bar
242
+ end
243
+
244
+ # test
245
+
246
+ # Documentation for baz
247
+ def baz; end
248
+ def ban; end
249
+ end
250
+ RUBY
251
+
252
+ foo_comment = @index["Foo"].first.comments
253
+ assert_equal("Documentation for Foo", foo_comment)
254
+
255
+ bar_comment = @index["bar"].first.comments
256
+ assert_equal("####################\nDocumentation for bar\n####################\n", bar_comment)
257
+
258
+ baz_comment = @index["baz"].first.comments
259
+ assert_equal("Documentation for baz", baz_comment)
260
+
261
+ ban_comment = @index["ban"].first.comments
262
+ assert_empty(ban_comment)
263
+ end
264
+
126
265
  def test_method_with_parameters
127
266
  index(<<~RUBY)
128
267
  class Foo