ruby-lsp 0.23.1 → 0.23.5

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