ruby-lsp 0.23.1 → 0.23.5

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp-launcher +18 -9
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +89 -58
  5. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +21 -10
  6. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +36 -0
  7. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  8. data/lib/ruby_indexer/test/index_test.rb +2 -1
  9. data/lib/ruby_indexer/test/instance_variables_test.rb +20 -0
  10. data/lib/ruby_indexer/test/method_test.rb +86 -8
  11. data/lib/ruby_lsp/base_server.rb +11 -21
  12. data/lib/ruby_lsp/document.rb +62 -9
  13. data/lib/ruby_lsp/erb_document.rb +5 -3
  14. data/lib/ruby_lsp/global_state.rb +9 -0
  15. data/lib/ruby_lsp/listeners/definition.rb +7 -2
  16. data/lib/ruby_lsp/rbs_document.rb +2 -2
  17. data/lib/ruby_lsp/requests/code_action_resolve.rb +2 -6
  18. data/lib/ruby_lsp/requests/completion.rb +2 -1
  19. data/lib/ruby_lsp/requests/definition.rb +1 -1
  20. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  21. data/lib/ruby_lsp/requests/hover.rb +1 -1
  22. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  23. data/lib/ruby_lsp/requests/references.rb +1 -1
  24. data/lib/ruby_lsp/requests/rename.rb +3 -3
  25. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -4
  26. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  27. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +3 -3
  28. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -13
  29. data/lib/ruby_lsp/requests/workspace_symbol.rb +2 -2
  30. data/lib/ruby_lsp/ruby_document.rb +75 -6
  31. data/lib/ruby_lsp/server.rb +69 -49
  32. data/lib/ruby_lsp/store.rb +7 -7
  33. data/lib/ruby_lsp/type_inferrer.rb +39 -17
  34. data/lib/ruby_lsp/utils.rb +43 -0
  35. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4a7b6b44dae357ccbc6a9f6494bd892d890bebf1287aa6c989ef4f2aee14e82
4
- data.tar.gz: cbfc554b33a2cf45663ab394e4fb81e27eb2ec4309907943e3fe67d591222d63
3
+ metadata.gz: 924e57270229eb4d0b709b01023d16f414591f8ea40c3b8a51ae6fb6cd09cc4f
4
+ data.tar.gz: 72f4f544baf412f8580fe28287ba5324c7f646a9436797ebea59f34f3712d62e
5
5
  SHA512:
6
- metadata.gz: bee0f714bd2f2865eeefa2de4eeb2f31bbf55d1fb106cb2a19af208bfc8c868815e2fcad86dcd58a119c9494c6a8254b01fcacdbeb02ce4379a8242ad94d2fcb
7
- data.tar.gz: 690d71bce5781f0de3ff170782d76e1d6ef34afd6801525c9ebbb92cfc64c2b9adc25b591710eaf4100c138706a8a358bfe6959d133a0da96e7cdd16279549c7
6
+ metadata.gz: 650cf172b8b1408f5498cb2010c2f94b3ef0a4bc93212ef63e24c2177ef5996455625a1bb64fe51f75322a42625e9ebcf156f95f5412c44839fe4957df4c957f
7
+ data.tar.gz: ce9b866443836810fe5884c2f977d4fd91fe020023ff547a9d60e525715ce388429aa7a901b0acc84876d68cada8f30dac2836494b7403fd8c2e845bea86ce79
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.23.1
1
+ 0.23.5
@@ -7,6 +7,7 @@
7
7
  # !!!!!!!
8
8
 
9
9
  setup_error = nil
10
+ install_error = nil
10
11
 
11
12
  # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
12
13
  # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
@@ -61,27 +62,35 @@ begin
61
62
 
62
63
  require "bundler"
63
64
  Bundler.ui.level = :silent
65
+
66
+ # This Marshal load can only happen after requiring Bundler because it will load a custom error class from Bundler
67
+ # itself. If we try to load before requiring, the class will not be defined and loading will fail
68
+ error_path = File.join(".ruby-lsp", "install_error")
69
+ install_error = if File.exist?(error_path)
70
+ Marshal.load(File.read(error_path))
71
+ end
72
+
64
73
  Bundler.setup
65
74
  $stderr.puts("Composed Bundle set up successfully")
66
75
  end
67
76
  rescue StandardError => e
68
77
  # If installing gems failed for any reason, we don't want to exit the process prematurely. We can still provide most
69
78
  # features in a degraded mode. We simply save the error so that we can report to the user that certain gems might be
70
- # missing, but we respect the LSP life cycle
71
- setup_error = e
72
- $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")
79
+ # missing, but we respect the LSP life cycle.
80
+ #
81
+ # If an install error occurred and one of the gems is not installed, Bundler.setup is guaranteed to fail with
82
+ # `Bundler::GemNotFound`. We don't set `setup_error` here because that would essentially report the same problem twice
83
+ # to telemetry, which is not useful
84
+ unless install_error && e.is_a?(Bundler::GemNotFound)
85
+ setup_error = e
86
+ $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")
87
+ end
73
88
 
74
89
  # If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server
75
90
  # in degraded mode
76
91
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
77
92
  end
78
93
 
79
- error_path = File.join(".ruby-lsp", "install_error")
80
-
81
- install_error = if File.exist?(error_path)
82
- Marshal.load(File.read(error_path))
83
- end
84
-
85
94
  # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
86
95
  # configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
87
96
  # paths into the load path manually or we may end up requiring the wrong version of the gem
@@ -24,7 +24,7 @@ module RubyIndexer
24
24
  @index = index
25
25
  @uri = uri
26
26
  @enhancements = T.let(Enhancement.all(self), T::Array[Enhancement])
27
- @visibility_stack = T.let([Entry::Visibility::PUBLIC], T::Array[Entry::Visibility])
27
+ @visibility_stack = T.let([VisibilityScope.public_scope], T::Array[VisibilityScope])
28
28
  @comments_by_line = T.let(
29
29
  parse_result.comments.to_h do |c|
30
30
  [c.location.start_line, c]
@@ -64,7 +64,6 @@ module RubyIndexer
64
64
  :on_constant_path_or_write_node_enter,
65
65
  :on_constant_path_operator_write_node_enter,
66
66
  :on_constant_path_and_write_node_enter,
67
- :on_constant_or_write_node_enter,
68
67
  :on_constant_write_node_enter,
69
68
  :on_constant_or_write_node_enter,
70
69
  :on_constant_and_write_node_enter,
@@ -138,7 +137,7 @@ module RubyIndexer
138
137
 
139
138
  sig { params(node: Prism::SingletonClassNode).void }
140
139
  def on_singleton_class_node_enter(node)
141
- @visibility_stack.push(Entry::Visibility::PUBLIC)
140
+ @visibility_stack.push(VisibilityScope.public_scope)
142
141
 
143
142
  current_owner = @owner_stack.last
144
143
 
@@ -280,15 +279,14 @@ module RubyIndexer
280
279
  when :include, :prepend, :extend
281
280
  handle_module_operation(node, message)
282
281
  when :public
283
- @visibility_stack.push(Entry::Visibility::PUBLIC)
282
+ @visibility_stack.push(VisibilityScope.public_scope)
284
283
  when :protected
285
- @visibility_stack.push(Entry::Visibility::PROTECTED)
284
+ @visibility_stack.push(VisibilityScope.new(visibility: Entry::Visibility::PROTECTED))
286
285
  when :private
287
- @visibility_stack.push(Entry::Visibility::PRIVATE)
286
+ @visibility_stack.push(VisibilityScope.new(visibility: Entry::Visibility::PRIVATE))
288
287
  when :module_function
289
288
  handle_module_function(node)
290
289
  when :private_class_method
291
- @visibility_stack.push(Entry::Visibility::PRIVATE)
292
290
  handle_private_class_method(node)
293
291
  end
294
292
 
@@ -324,42 +322,61 @@ module RubyIndexer
324
322
 
325
323
  sig { params(node: Prism::DefNode).void }
326
324
  def on_def_node_enter(node)
325
+ owner = @owner_stack.last
326
+ return unless owner
327
+
327
328
  @inside_def = true
328
329
  method_name = node.name.to_s
329
330
  comments = collect_comments(node)
331
+ scope = current_visibility_scope
330
332
 
331
333
  case node.receiver
332
334
  when nil
335
+ location = Location.from_prism_location(node.location, @code_units_cache)
336
+ name_location = Location.from_prism_location(node.name_loc, @code_units_cache)
337
+ signatures = [Entry::Signature.new(list_params(node.parameters))]
338
+
333
339
  @index.add(Entry::Method.new(
334
340
  method_name,
335
341
  @uri,
336
- Location.from_prism_location(node.location, @code_units_cache),
337
- Location.from_prism_location(node.name_loc, @code_units_cache),
342
+ location,
343
+ name_location,
338
344
  comments,
339
- [Entry::Signature.new(list_params(node.parameters))],
340
- current_visibility,
341
- @owner_stack.last,
345
+ signatures,
346
+ scope.visibility,
347
+ owner,
342
348
  ))
343
- when Prism::SelfNode
344
- owner = @owner_stack.last
345
349
 
346
- if owner
350
+ if scope.module_func
347
351
  singleton = @index.existing_or_new_singleton_class(owner.name)
348
352
 
349
353
  @index.add(Entry::Method.new(
350
354
  method_name,
351
355
  @uri,
352
- Location.from_prism_location(node.location, @code_units_cache),
353
- Location.from_prism_location(node.name_loc, @code_units_cache),
356
+ location,
357
+ name_location,
354
358
  comments,
355
- [Entry::Signature.new(list_params(node.parameters))],
356
- current_visibility,
359
+ signatures,
360
+ Entry::Visibility::PUBLIC,
357
361
  singleton,
358
362
  ))
359
-
360
- @owner_stack << singleton
361
- @stack << "<Class:#{@stack.last}>"
362
363
  end
364
+ when Prism::SelfNode
365
+ singleton = @index.existing_or_new_singleton_class(owner.name)
366
+
367
+ @index.add(Entry::Method.new(
368
+ method_name,
369
+ @uri,
370
+ Location.from_prism_location(node.location, @code_units_cache),
371
+ Location.from_prism_location(node.name_loc, @code_units_cache),
372
+ comments,
373
+ [Entry::Signature.new(list_params(node.parameters))],
374
+ scope.visibility,
375
+ singleton,
376
+ ))
377
+
378
+ @owner_stack << singleton
379
+ @stack << "<Class:#{@stack.last}>"
363
380
  end
364
381
  end
365
382
 
@@ -834,6 +851,8 @@ module RubyIndexer
834
851
  return unless receiver.nil? || receiver.is_a?(Prism::SelfNode)
835
852
 
836
853
  comments = collect_comments(node)
854
+ scope = current_visibility_scope
855
+
837
856
  arguments.each do |argument|
838
857
  name, loc = case argument
839
858
  when Prism::SymbolNode
@@ -850,7 +869,7 @@ module RubyIndexer
850
869
  @uri,
851
870
  Location.from_prism_location(loc, @code_units_cache),
852
871
  comments,
853
- current_visibility,
872
+ scope.visibility,
854
873
  @owner_stack.last,
855
874
  ))
856
875
  end
@@ -862,7 +881,7 @@ module RubyIndexer
862
881
  @uri,
863
882
  Location.from_prism_location(loc, @code_units_cache),
864
883
  comments,
865
- current_visibility,
884
+ scope.visibility,
866
885
  @owner_stack.last,
867
886
  ))
868
887
  end
@@ -904,11 +923,20 @@ module RubyIndexer
904
923
 
905
924
  sig { params(node: Prism::CallNode).void }
906
925
  def handle_module_function(node)
926
+ # Invoking `module_function` in a class raises
927
+ owner = @owner_stack.last
928
+ return unless owner.is_a?(Entry::Module)
929
+
907
930
  arguments_node = node.arguments
908
- return unless arguments_node
909
931
 
910
- owner_name = @owner_stack.last&.name
911
- return unless owner_name
932
+ # If `module_function` is invoked without arguments, all methods defined after it become singleton methods and the
933
+ # visibility for instance methods changes to private
934
+ unless arguments_node
935
+ @visibility_stack.push(VisibilityScope.module_function_scope)
936
+ return
937
+ end
938
+
939
+ owner_name = owner.name
912
940
 
913
941
  arguments_node.arguments.each do |argument|
914
942
  method_name = case argument
@@ -946,45 +974,48 @@ module RubyIndexer
946
974
 
947
975
  sig { params(node: Prism::CallNode).void }
948
976
  def handle_private_class_method(node)
949
- node.arguments&.arguments&.each do |argument|
950
- string_or_symbol_nodes = case argument
951
- when Prism::StringNode, Prism::SymbolNode
952
- [argument]
953
- when Prism::ArrayNode
954
- argument.elements
955
- else
956
- []
957
- end
977
+ arguments = node.arguments&.arguments
978
+ return unless arguments
958
979
 
959
- unless string_or_symbol_nodes.empty?
960
- # pop the visibility off since there isn't a method definition following `private_class_method`
961
- @visibility_stack.pop
962
- end
980
+ # If we're passing a method definition directly to `private_class_method`, push a new private scope. That will be
981
+ # applied when the indexer finds the method definition and then popped on `call_node_leave`
982
+ if arguments.first.is_a?(Prism::DefNode)
983
+ @visibility_stack.push(VisibilityScope.new(visibility: Entry::Visibility::PRIVATE))
984
+ return
985
+ end
963
986
 
964
- string_or_symbol_nodes.each do |string_or_symbol_node|
965
- method_name = case string_or_symbol_node
966
- when Prism::StringNode
967
- string_or_symbol_node.content
968
- when Prism::SymbolNode
969
- string_or_symbol_node.value
970
- end
971
- next unless method_name
987
+ owner_name = @owner_stack.last&.name
988
+ return unless owner_name
972
989
 
973
- owner_name = @owner_stack.last&.name
974
- next unless owner_name
990
+ # private_class_method accepts strings, symbols or arrays of strings and symbols as arguments. Here we build a
991
+ # single list of all of the method names that have to be made private
992
+ arrays, others = T.cast(
993
+ arguments.partition { |argument| argument.is_a?(Prism::ArrayNode) },
994
+ [T::Array[Prism::ArrayNode], T::Array[Prism::Node]],
995
+ )
996
+ arrays.each { |array| others.concat(array.elements) }
975
997
 
976
- entries = @index.resolve_method(method_name, @index.existing_or_new_singleton_class(owner_name).name)
977
- next unless entries
998
+ names = others.filter_map do |argument|
999
+ case argument
1000
+ when Prism::StringNode
1001
+ argument.unescaped
1002
+ when Prism::SymbolNode
1003
+ argument.value
1004
+ end
1005
+ end
978
1006
 
979
- entries.each do |entry|
980
- entry.visibility = Entry::Visibility::PRIVATE
981
- end
1007
+ names.each do |name|
1008
+ entries = @index.resolve_method(name, @index.existing_or_new_singleton_class(owner_name).name)
1009
+ next unless entries
1010
+
1011
+ entries.each do |entry|
1012
+ entry.visibility = Entry::Visibility::PRIVATE
982
1013
  end
983
1014
  end
984
1015
  end
985
1016
 
986
- sig { returns(Entry::Visibility) }
987
- def current_visibility
1017
+ sig { returns(VisibilityScope) }
1018
+ def current_visibility_scope
988
1019
  T.must(@visibility_stack.last)
989
1020
  end
990
1021
 
@@ -1091,7 +1122,7 @@ module RubyIndexer
1091
1122
 
1092
1123
  sig { params(short_name: String, entry: Entry::Namespace).void }
1093
1124
  def advance_namespace_stack(short_name, entry)
1094
- @visibility_stack.push(Entry::Visibility::PUBLIC)
1125
+ @visibility_stack.push(VisibilityScope.public_scope)
1095
1126
  @owner_stack << entry
1096
1127
  @index.add(entry)
1097
1128
  @stack << short_name
@@ -48,6 +48,8 @@ module RubyIndexer
48
48
  )
49
49
 
50
50
  @configuration = T.let(RubyIndexer::Configuration.new, Configuration)
51
+
52
+ @initial_indexing_completed = T.let(false, T::Boolean)
51
53
  end
52
54
 
53
55
  # Register an included `hook` that will be executed when `module_name` is included into any namespace
@@ -56,8 +58,8 @@ module RubyIndexer
56
58
  (@included_hooks[module_name] ||= []) << hook
57
59
  end
58
60
 
59
- sig { params(uri: URI::Generic).void }
60
- def delete(uri)
61
+ sig { params(uri: URI::Generic, skip_require_paths_tree: T::Boolean).void }
62
+ def delete(uri, skip_require_paths_tree: false)
61
63
  key = uri.to_s
62
64
  # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
63
65
  # left, delete the constant from the index.
@@ -80,6 +82,7 @@ module RubyIndexer
80
82
  end
81
83
 
82
84
  @uris_to_entries.delete(key)
85
+ return if skip_require_paths_tree
83
86
 
84
87
  require_path = uri.require_path
85
88
  @require_paths_tree.delete(require_path) if require_path
@@ -357,12 +360,13 @@ module RubyIndexer
357
360
  # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the
358
361
  # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this
359
362
  # behavior and can take appropriate action.
360
- # binding.break
361
- if @entries.any?
363
+ if @initial_indexing_completed
362
364
  raise IndexNotEmptyError,
363
365
  "The index is not empty. To prevent invalid entries, `index_all` can only be called once."
364
366
  end
365
367
 
368
+ @initial_indexing_completed = true
369
+
366
370
  RBSIndexer.new(self).index_ruby_core
367
371
  # Calculate how many paths are worth 1% of progress
368
372
  progress_step = (uris.length / 100.0).ceil
@@ -618,17 +622,24 @@ module RubyIndexer
618
622
  end
619
623
 
620
624
  # Synchronizes a change made to the given URI. This method will ensure that new declarations are indexed, removed
621
- # declarations removed and that the ancestor linearization cache is cleared if necessary
622
- sig { params(uri: URI::Generic, source: String).void }
623
- def handle_change(uri, source)
625
+ # declarations removed and that the ancestor linearization cache is cleared if necessary. If a block is passed, the
626
+ # consumer of this API has to handle deleting and inserting/updating entries in the index instead of passing the
627
+ # document's source (used to handle unsaved changes to files)
628
+ sig do
629
+ params(uri: URI::Generic, source: T.nilable(String), block: T.nilable(T.proc.params(index: Index).void)).void
630
+ end
631
+ def handle_change(uri, source = nil, &block)
624
632
  key = uri.to_s
625
633
  original_entries = @uris_to_entries[key]
626
634
 
627
- delete(uri)
628
- index_single(uri, source)
635
+ if block
636
+ block.call(self)
637
+ else
638
+ delete(uri)
639
+ index_single(uri, T.must(source))
640
+ end
629
641
 
630
642
  updated_entries = @uris_to_entries[key]
631
-
632
643
  return unless original_entries && updated_entries
633
644
 
634
645
  # A change in one ancestor may impact several different others, which could be including that ancestor through
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ # Represents the visibility scope in a Ruby namespace. This keeps track of whether methods are in a public, private or
6
+ # protected section, and whether they are module functions.
7
+ class VisibilityScope
8
+ extend T::Sig
9
+
10
+ class << self
11
+ extend T::Sig
12
+
13
+ sig { returns(T.attached_class) }
14
+ def module_function_scope
15
+ new(module_func: true, visibility: Entry::Visibility::PRIVATE)
16
+ end
17
+
18
+ sig { returns(T.attached_class) }
19
+ def public_scope
20
+ new
21
+ end
22
+ end
23
+
24
+ sig { returns(Entry::Visibility) }
25
+ attr_reader :visibility
26
+
27
+ sig { returns(T::Boolean) }
28
+ attr_reader :module_func
29
+
30
+ sig { params(visibility: Entry::Visibility, module_func: T::Boolean).void }
31
+ def initialize(visibility: Entry::Visibility::PUBLIC, module_func: false)
32
+ @visibility = visibility
33
+ @module_func = module_func
34
+ end
35
+ end
36
+ end
@@ -5,6 +5,7 @@ require "yaml"
5
5
  require "did_you_mean"
6
6
 
7
7
  require "ruby_indexer/lib/ruby_indexer/uri"
8
+ require "ruby_indexer/lib/ruby_indexer/visibility_scope"
8
9
  require "ruby_indexer/lib/ruby_indexer/declaration_listener"
9
10
  require "ruby_indexer/lib/ruby_indexer/reference_finder"
10
11
  require "ruby_indexer/lib/ruby_indexer/enhancement"
@@ -2061,7 +2061,8 @@ module RubyIndexer
2061
2061
  end
2062
2062
 
2063
2063
  def test_prevents_multiple_calls_to_index_all
2064
- # For this test class, `index_all` is already called once in `setup`.
2064
+ @index.index_all
2065
+
2065
2066
  assert_raises(Index::IndexNotEmptyError) do
2066
2067
  @index.index_all
2067
2068
  end
@@ -216,5 +216,25 @@ module RubyIndexer
216
216
  assert_instance_of(Entry::Class, owner)
217
217
  assert_equal("Foo", owner.name)
218
218
  end
219
+
220
+ def test_module_function_does_not_impact_instance_variables
221
+ # One possible way of implementing `module_function` would be to push a fake singleton class to the stack, so that
222
+ # methods are inserted into it. However, that would be incorrect because it would then bind instance variables to
223
+ # the wrong type. This test is here to prevent that from happening.
224
+ index(<<~RUBY)
225
+ module Foo
226
+ module_function
227
+
228
+ def something; end
229
+
230
+ @a = 123
231
+ end
232
+ RUBY
233
+
234
+ entry = T.must(@index["@a"]&.first)
235
+ owner = T.must(entry.owner)
236
+ assert_instance_of(Entry::SingletonClass, owner)
237
+ assert_equal("Foo::<Class:Foo>", owner.name)
238
+ end
219
239
  end
220
240
  end
@@ -88,19 +88,21 @@ module RubyIndexer
88
88
 
89
89
  def test_visibility_tracking
90
90
  index(<<~RUBY)
91
- private def foo
92
- end
91
+ class Foo
92
+ private def foo
93
+ end
93
94
 
94
- def bar; end
95
+ def bar; end
95
96
 
96
- protected
97
+ protected
97
98
 
98
- def baz; end
99
+ def baz; end
100
+ end
99
101
  RUBY
100
102
 
101
- assert_entry("foo", Entry::Method, "/fake/path/foo.rb:0-8:1-3", visibility: Entry::Visibility::PRIVATE)
102
- assert_entry("bar", Entry::Method, "/fake/path/foo.rb:3-0:3-12", visibility: Entry::Visibility::PUBLIC)
103
- assert_entry("baz", Entry::Method, "/fake/path/foo.rb:7-0:7-12", visibility: Entry::Visibility::PROTECTED)
103
+ assert_entry("foo", Entry::Method, "/fake/path/foo.rb:1-10:2-5", visibility: Entry::Visibility::PRIVATE)
104
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:4-2:4-14", visibility: Entry::Visibility::PUBLIC)
105
+ assert_entry("baz", Entry::Method, "/fake/path/foo.rb:8-2:8-14", visibility: Entry::Visibility::PROTECTED)
104
106
  end
105
107
 
106
108
  def test_visibility_tracking_with_nested_class_or_modules
@@ -846,6 +848,82 @@ module RubyIndexer
846
848
  assert_signature_matches(entry, "baz(1)")
847
849
  end
848
850
 
851
+ def test_module_function_with_no_arguments
852
+ index(<<~RUBY)
853
+ module Foo
854
+ def bar; end
855
+
856
+ module_function
857
+
858
+ def baz; end
859
+ attr_reader :attribute
860
+
861
+ public
862
+
863
+ def qux; end
864
+ end
865
+ RUBY
866
+
867
+ entry = T.must(@index["bar"].first)
868
+ assert_predicate(entry, :public?)
869
+ assert_equal("Foo", T.must(entry.owner).name)
870
+
871
+ instance_baz, singleton_baz = T.must(@index["baz"])
872
+ assert_predicate(instance_baz, :private?)
873
+ assert_equal("Foo", T.must(instance_baz.owner).name)
874
+
875
+ assert_predicate(singleton_baz, :public?)
876
+ assert_equal("Foo::<Class:Foo>", T.must(singleton_baz.owner).name)
877
+
878
+ # After invoking `public`, the state of `module_function` is reset
879
+ instance_qux, singleton_qux = T.must(@index["qux"])
880
+ assert_nil(singleton_qux)
881
+ assert_predicate(instance_qux, :public?)
882
+ assert_equal("Foo", T.must(instance_baz.owner).name)
883
+
884
+ # Attributes are not turned into class methods, they do become private
885
+ instance_attribute, singleton_attribute = @index["attribute"]
886
+ assert_nil(singleton_attribute)
887
+ assert_equal("Foo", T.must(instance_attribute.owner).name)
888
+ assert_predicate(instance_attribute, :private?)
889
+ end
890
+
891
+ def test_module_function_does_nothing_in_classes
892
+ # Invoking `module_function` in a class raises an error. We simply ignore it
893
+ index(<<~RUBY)
894
+ class Foo
895
+ def bar; end
896
+
897
+ module_function
898
+
899
+ def baz; end
900
+ end
901
+ RUBY
902
+
903
+ entry = T.must(@index["bar"].first)
904
+ assert_predicate(entry, :public?)
905
+ assert_equal("Foo", T.must(entry.owner).name)
906
+
907
+ entry = T.must(@index["baz"].first)
908
+ assert_predicate(entry, :public?)
909
+ assert_equal("Foo", T.must(entry.owner).name)
910
+ end
911
+
912
+ def test_making_several_class_methods_private
913
+ index(<<~RUBY)
914
+ class Foo
915
+ def self.bar; end
916
+ def self.baz; end
917
+ def self.qux; end
918
+
919
+ private_class_method :bar, :baz, :qux
920
+
921
+ def initialize
922
+ end
923
+ end
924
+ RUBY
925
+ end
926
+
849
927
  private
850
928
 
851
929
  sig { params(entry: Entry::Method, call_string: String).void }