ruby-lsp 0.20.1 → 0.22.1

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