ruby-lsp 0.20.1 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +18 -3
  5. data/exe/ruby-lsp-launcher +127 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +56 -2
  7. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +21 -6
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  9. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +5 -5
  10. data/lib/ruby_indexer/test/classes_and_modules_test.rb +2 -2
  11. data/lib/ruby_indexer/test/enhancements_test.rb +51 -19
  12. data/lib/ruby_indexer/test/index_test.rb +2 -2
  13. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  14. data/lib/ruby_indexer/test/method_test.rb +26 -0
  15. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  16. data/lib/ruby_lsp/addon.rb +9 -2
  17. data/lib/ruby_lsp/base_server.rb +14 -5
  18. data/lib/ruby_lsp/client_capabilities.rb +60 -0
  19. data/lib/ruby_lsp/document.rb +1 -1
  20. data/lib/ruby_lsp/global_state.rb +20 -19
  21. data/lib/ruby_lsp/internal.rb +2 -0
  22. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  23. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  24. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  25. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  26. data/lib/ruby_lsp/requests/completion.rb +7 -1
  27. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  28. data/lib/ruby_lsp/requests/definition.rb +26 -11
  29. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  30. data/lib/ruby_lsp/requests/hover.rb +24 -6
  31. data/lib/ruby_lsp/requests/rename.rb +1 -1
  32. data/lib/ruby_lsp/requests/request.rb +1 -1
  33. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +11 -1
  34. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  35. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  36. data/lib/ruby_lsp/server.rb +54 -16
  37. data/lib/ruby_lsp/setup_bundler.rb +108 -22
  38. data/lib/ruby_lsp/utils.rb +8 -0
  39. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58327a9f9a3d85375cbbf81e3e170a948d5c8dfbdcdcac0a1fdef5f685d20c95
4
- data.tar.gz: 45572eb6645bce73d2079bed65f0d92c98921517926b968bba96788769a3aada
3
+ metadata.gz: a22b940469b6910a05ada8b39dbbc2ad918e26e41de0f2f287e79df46b8e46fe
4
+ data.tar.gz: a57e250627b82a42ba286fd9b14338c48fb04b5a2e76c889d28d93ea55f96f90
5
5
  SHA512:
6
- metadata.gz: c506eeff4d24060e3afdb4ad9da319c12b725050c8f25a9348ec7dea9082f8adced7671b7b1eb8f7f09412942139e47a2df5059b5672d2b6a75f0ffc5a59885d
7
- data.tar.gz: d9a89ce24946e5f9cc47b5a48086f6fd977176d31332a0b634950dc00a41fc9a36e4753eae2896e2da0129e92bff4278265eaa970252a5292c783e5bc82258e9
6
+ metadata.gz: 0661e457f068016e397436e76c9d48d8d4e2f718fb47268c6fe57797b1374e4669eb4c01b7a45ca26d8fe39dbc79f308797864e73aa31ba21e013ac3c2c97265
7
+ data.tar.gz: 709e136e94d223cb12fcb8a082d6193023fecc324aa1908f7a9dc2d9c3b612d6935cfe0362e7ba22fdd02ec3e6c1113a2a44e2a0f15230ff4698075a145d5e0d
data/README.md CHANGED
@@ -29,8 +29,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopif
29
29
  be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor
30
30
  Covenant](CODE_OF_CONDUCT.md) code of conduct.
31
31
 
32
- If you wish to contribute, see [CONTRIBUTING](CONTRIBUTING.md) for development instructions and check out our pinned
33
- [roadmap issue](https://github.com/Shopify/ruby-lsp/issues) for a list of tasks to get started.
32
+ If you wish to contribute, see [Contributing](https://shopify.github.io/ruby-lsp/contributing.html) for development instructions and check out our
33
+ [Design and roadmap](https://shopify.github.io/ruby-lsp/design-and-roadmap.html) for a list of tasks to get started.
34
34
 
35
35
  ## License
36
36
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.20.1
1
+ 0.21.0
data/exe/ruby-lsp CHANGED
@@ -33,6 +33,10 @@ parser = OptionParser.new do |opts|
33
33
  options[:doctor] = true
34
34
  end
35
35
 
36
+ opts.on("--use-launcher", "[EXPERIMENTAL] Use launcher mechanism to handle missing dependencies gracefully") do
37
+ options[:launcher] = true
38
+ end
39
+
36
40
  opts.on("-h", "--help", "Print this help") do
37
41
  puts opts.help
38
42
  puts
@@ -54,6 +58,17 @@ end
54
58
  # using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
55
59
  # the application's bundle
56
60
  if ENV["BUNDLE_GEMFILE"].nil?
61
+ # Substitute the current process by the launcher. RubyGems activates all dependencies of a gem's executable eagerly,
62
+ # but we can't have that happen because we want to invoke Bundler.setup ourselves with the composed bundle and avoid
63
+ # duplicate spec activation errors. Replacing the process with the launcher executable will clear the activated specs,
64
+ # which gives us the opportunity to control which specs are activated and enter degraded mode if any gems failed to
65
+ # install rather than failing to boot the server completely
66
+ if options[:launcher]
67
+ command = +File.expand_path("ruby-lsp-launcher", __dir__)
68
+ command << " --debug" if options[:debug]
69
+ exit exec(command)
70
+ end
71
+
57
72
  require_relative "../lib/ruby_lsp/setup_bundler"
58
73
 
59
74
  begin
@@ -69,12 +84,12 @@ if ENV["BUNDLE_GEMFILE"].nil?
69
84
  "bundle"
70
85
  end
71
86
 
72
- exit exec(env, "#{base_bundle} exec ruby-lsp #{original_args.join(" ")}")
87
+ exit exec(env, "#{base_bundle} exec ruby-lsp #{original_args.join(" ")}".strip)
73
88
  end
74
89
 
75
- require "ruby_lsp/load_sorbet"
76
-
77
90
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
91
+
92
+ require "ruby_lsp/load_sorbet"
78
93
  require "ruby_lsp/internal"
79
94
 
80
95
  T::Utils.run_all_sig_blocks
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # !!!!!!!
5
+ # No gems can be required in this file until we invoke bundler setup except inside the forked process that sets up the
6
+ # composed bundle
7
+ # !!!!!!!
8
+
9
+ setup_error = nil
10
+
11
+ # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
12
+ # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
13
+ # to ensure that we're setting up the bundle in the right place
14
+ $stdin.binmode
15
+ headers = $stdin.gets("\r\n\r\n")
16
+ content_length = headers[/Content-Length: (\d+)/i, 1].to_i
17
+ raw_initialize = $stdin.read(content_length)
18
+
19
+ # Compose the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process
20
+ # `$LOAD_PATH` and `Gem.loaded_specs`. Windows doesn't support forking, so we need a separate path to support it
21
+ pid = if Gem.win_platform?
22
+ # Since we can't fork on Windows and spawn won't carry over the existing load paths, we need to explicitly pass that
23
+ # down to the child process or else requiring gems during composing the bundle will fail
24
+ load_path = $LOAD_PATH.flat_map do |path|
25
+ ["-I", File.expand_path(path)]
26
+ end
27
+
28
+ Process.spawn(
29
+ Gem.ruby,
30
+ *load_path,
31
+ File.expand_path("../lib/ruby_lsp/scripts/compose_bundle_windows.rb", __dir__),
32
+ raw_initialize,
33
+ )
34
+ else
35
+ fork do
36
+ require_relative "../lib/ruby_lsp/scripts/compose_bundle"
37
+ compose(raw_initialize)
38
+ end
39
+ end
40
+
41
+ begin
42
+ # Wait until the composed Bundle is finished
43
+ Process.wait(pid)
44
+ rescue Errno::ECHILD
45
+ # In theory, the child process can finish before we even get to the wait call, but that is not an error
46
+ end
47
+
48
+ begin
49
+ bundle_env_path = File.join(".ruby-lsp", "bundle_env")
50
+ # We can't require `bundler/setup` because that file prematurely exits the process if setup fails. However, we can't
51
+ # simply require bundler either because the version required might conflict with the one locked in the composed
52
+ # bundle. We need the composed bundle sub-process to inform us of the locked Bundler version, so that we can then
53
+ # activate the right spec and require the exact Bundler version required by the app
54
+ if File.exist?(bundle_env_path)
55
+ env = File.readlines(bundle_env_path).to_h { |line| line.chomp.split("=", 2) }
56
+ ENV.merge!(env)
57
+
58
+ if env["BUNDLER_VERSION"]
59
+ Gem::Specification.find_by_name("bundler", env["BUNDLER_VERSION"]).activate
60
+ end
61
+
62
+ require "bundler"
63
+ Bundler.ui.level = :silent
64
+ Bundler.setup
65
+ $stderr.puts("Composed Bundle set up successfully")
66
+ end
67
+ rescue StandardError => e
68
+ # If installing gems failed for any reason, we don't want to exit the process prematurely. We can still provide most
69
+ # 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}")
73
+
74
+ # If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server
75
+ # in degraded mode
76
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
77
+ ensure
78
+ require "fileutils"
79
+ FileUtils.rm(bundle_env_path) if File.exist?(bundle_env_path)
80
+ end
81
+
82
+ error_path = File.join(".ruby-lsp", "install_error")
83
+
84
+ install_error = if File.exist?(error_path)
85
+ Marshal.load(File.read(error_path))
86
+ end
87
+
88
+ # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
89
+ # configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
90
+ # paths into the load path manually or we may end up requiring the wrong version of the gem
91
+ require "ruby_lsp/load_sorbet"
92
+ require "ruby_lsp/internal"
93
+
94
+ T::Utils.run_all_sig_blocks
95
+
96
+ if ARGV.include?("--debug")
97
+ if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
98
+ $stderr.puts "Debugging is not supported on Windows"
99
+ else
100
+ begin
101
+ ENV.delete("RUBY_DEBUG_IRB_CONSOLE")
102
+ require "debug/open_nonstop"
103
+ rescue LoadError
104
+ $stderr.puts("You need to install the debug gem to use the --debug flag")
105
+ end
106
+ end
107
+ end
108
+
109
+ # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
110
+ $> = $stderr
111
+
112
+ initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize
113
+
114
+ begin
115
+ RubyLsp::Server.new(
116
+ install_error: install_error,
117
+ setup_error: setup_error,
118
+ initialize_request: initialize_request,
119
+ ).start
120
+ rescue ArgumentError
121
+ # If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
122
+ # and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
123
+ # request manually and then start the main loop
124
+ server = RubyLsp::Server.new
125
+ server.process_message(initialize_request)
126
+ server.start
127
+ end
@@ -312,12 +312,16 @@ module RubyIndexer
312
312
  @visibility_stack.push(Entry::Visibility::PROTECTED)
313
313
  when :private
314
314
  @visibility_stack.push(Entry::Visibility::PRIVATE)
315
+ when :module_function
316
+ handle_module_function(node)
315
317
  end
316
318
 
317
319
  @enhancements.each do |enhancement|
318
- enhancement.on_call_node(@index, @owner_stack.last, node, @file_path, @code_units_cache)
320
+ enhancement.on_call_node_enter(@owner_stack.last, node, @file_path, @code_units_cache)
319
321
  rescue StandardError => e
320
- @indexing_errors << "Indexing error in #{@file_path} with '#{enhancement.class.name}' enhancement: #{e.message}"
322
+ @indexing_errors << <<~MSG
323
+ Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message}
324
+ MSG
321
325
  end
322
326
  end
323
327
 
@@ -332,6 +336,14 @@ module RubyIndexer
332
336
  @visibility_stack.pop
333
337
  end
334
338
  end
339
+
340
+ @enhancements.each do |enhancement|
341
+ enhancement.on_call_node_leave(@owner_stack.last, node, @file_path, @code_units_cache)
342
+ rescue StandardError => e
343
+ @indexing_errors << <<~MSG
344
+ Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message}
345
+ MSG
346
+ end
335
347
  end
336
348
 
337
349
  sig { params(node: Prism::DefNode).void }
@@ -751,6 +763,48 @@ module RubyIndexer
751
763
  end
752
764
  end
753
765
 
766
+ sig { params(node: Prism::CallNode).void }
767
+ def handle_module_function(node)
768
+ arguments_node = node.arguments
769
+ return unless arguments_node
770
+
771
+ owner_name = @owner_stack.last&.name
772
+ return unless owner_name
773
+
774
+ arguments_node.arguments.each do |argument|
775
+ method_name = case argument
776
+ when Prism::StringNode
777
+ argument.content
778
+ when Prism::SymbolNode
779
+ argument.value
780
+ end
781
+ next unless method_name
782
+
783
+ entries = @index.resolve_method(method_name, owner_name)
784
+ next unless entries
785
+
786
+ entries.each do |entry|
787
+ entry_owner_name = entry.owner&.name
788
+ next unless entry_owner_name
789
+
790
+ entry.visibility = Entry::Visibility::PRIVATE
791
+
792
+ singleton = @index.existing_or_new_singleton_class(entry_owner_name)
793
+ location = Location.from_prism_location(argument.location, @code_units_cache)
794
+ @index.add(Entry::Method.new(
795
+ method_name,
796
+ @file_path,
797
+ location,
798
+ location,
799
+ collect_comments(node)&.concat(entry.comments),
800
+ entry.signatures,
801
+ Entry::Visibility::PUBLIC,
802
+ singleton,
803
+ ))
804
+ end
805
+ end
806
+ end
807
+
754
808
  sig { returns(Entry::Visibility) }
755
809
  def current_visibility
756
810
  T.must(@visibility_stack.last)
@@ -2,20 +2,35 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyIndexer
5
- module Enhancement
5
+ class Enhancement
6
6
  extend T::Sig
7
7
  extend T::Helpers
8
8
 
9
- interface!
9
+ abstract!
10
10
 
11
- requires_ancestor { Object }
11
+ sig { params(index: Index).void }
12
+ def initialize(index)
13
+ @index = index
14
+ end
12
15
 
13
16
  # The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
14
17
  # register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
15
18
  # `ClassMethods` modules
16
19
  sig do
17
- abstract.params(
18
- index: Index,
20
+ overridable.params(
21
+ owner: T.nilable(Entry::Namespace),
22
+ node: Prism::CallNode,
23
+ file_path: String,
24
+ code_units_cache: T.any(
25
+ T.proc.params(arg0: Integer).returns(Integer),
26
+ Prism::CodeUnitsCache,
27
+ ),
28
+ ).void
29
+ end
30
+ def on_call_node_enter(owner, node, file_path, code_units_cache); end
31
+
32
+ sig do
33
+ overridable.params(
19
34
  owner: T.nilable(Entry::Namespace),
20
35
  node: Prism::CallNode,
21
36
  file_path: String,
@@ -25,6 +40,6 @@ module RubyIndexer
25
40
  ),
26
41
  ).void
27
42
  end
28
- def on_call_node(index, owner, node, file_path, code_units_cache); end
43
+ def on_call_node_leave(owner, node, file_path, code_units_cache); end
29
44
  end
30
45
  end
@@ -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
@@ -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
@@ -6,10 +6,8 @@ require_relative "test_case"
6
6
  module RubyIndexer
7
7
  class EnhancementTest < TestCase
8
8
  def test_enhancing_indexing_included_hook
9
- enhancement_class = Class.new do
10
- include Enhancement
11
-
12
- def on_call_node(index, owner, node, file_path, code_units_cache)
9
+ enhancement_class = Class.new(Enhancement) do
10
+ def on_call_node_enter(owner, node, file_path, code_units_cache)
13
11
  return unless owner
14
12
  return unless node.name == :extend
15
13
 
@@ -24,7 +22,7 @@ module RubyIndexer
24
22
  module_name = node.full_name
25
23
  next unless module_name == "ActiveSupport::Concern"
26
24
 
27
- index.register_included_hook(owner.name) do |index, base|
25
+ @index.register_included_hook(owner.name) do |index, base|
28
26
  class_methods_name = "#{owner.name}::ClassMethods"
29
27
 
30
28
  if index.indexed?(class_methods_name)
@@ -33,7 +31,7 @@ module RubyIndexer
33
31
  end
34
32
  end
35
33
 
36
- index.add(Entry::Method.new(
34
+ @index.add(Entry::Method.new(
37
35
  "new_method",
38
36
  file_path,
39
37
  location,
@@ -50,7 +48,7 @@ module RubyIndexer
50
48
  end
51
49
  end
52
50
 
53
- @index.register_enhancement(enhancement_class.new)
51
+ @index.register_enhancement(enhancement_class.new(@index))
54
52
  index(<<~RUBY)
55
53
  module ActiveSupport
56
54
  module Concern
@@ -98,10 +96,8 @@ module RubyIndexer
98
96
  end
99
97
 
100
98
  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)
99
+ enhancement_class = Class.new(Enhancement) do
100
+ def on_call_node_enter(owner, node, file_path, code_units_cache)
105
101
  return unless owner
106
102
 
107
103
  name = node.name
@@ -115,7 +111,7 @@ module RubyIndexer
115
111
 
116
112
  location = Location.from_prism_location(association_name.location, code_units_cache)
117
113
 
118
- index.add(Entry::Method.new(
114
+ @index.add(Entry::Method.new(
119
115
  T.must(association_name.value),
120
116
  file_path,
121
117
  location,
@@ -128,7 +124,7 @@ module RubyIndexer
128
124
  end
129
125
  end
130
126
 
131
- @index.register_enhancement(enhancement_class.new)
127
+ @index.register_enhancement(enhancement_class.new(@index))
132
128
  index(<<~RUBY)
133
129
  module ActiveSupport
134
130
  module Concern
@@ -160,11 +156,44 @@ module RubyIndexer
160
156
  assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17")
161
157
  end
162
158
 
163
- def test_error_handling_in_enhancement
164
- enhancement_class = Class.new do
165
- include Enhancement
159
+ def test_error_handling_in_on_call_node_enter_enhancement
160
+ enhancement_class = Class.new(Enhancement) do
161
+ def on_call_node_enter(owner, node, file_path, code_units_cache)
162
+ raise "Error"
163
+ end
164
+
165
+ class << self
166
+ def name
167
+ "TestEnhancement"
168
+ end
169
+ end
170
+ end
171
+
172
+ @index.register_enhancement(enhancement_class.new(@index))
173
+
174
+ _stdout, stderr = capture_io do
175
+ index(<<~RUBY)
176
+ module ActiveSupport
177
+ module Concern
178
+ def self.extended(base)
179
+ base.class_eval("def new_method(a); end")
180
+ end
181
+ end
182
+ end
183
+ RUBY
184
+ end
185
+
186
+ assert_match(
187
+ %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node enter enhancement},
188
+ stderr,
189
+ )
190
+ # The module should still be indexed
191
+ assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
192
+ end
166
193
 
167
- def on_call_node(index, owner, node, file_path, code_units_cache)
194
+ def test_error_handling_in_on_call_node_leave_enhancement
195
+ enhancement_class = Class.new(Enhancement) do
196
+ def on_call_node_leave(owner, node, file_path, code_units_cache)
168
197
  raise "Error"
169
198
  end
170
199
 
@@ -175,7 +204,7 @@ module RubyIndexer
175
204
  end
176
205
  end
177
206
 
178
- @index.register_enhancement(enhancement_class.new)
207
+ @index.register_enhancement(enhancement_class.new(@index))
179
208
 
180
209
  _stdout, stderr = capture_io do
181
210
  index(<<~RUBY)
@@ -189,7 +218,10 @@ module RubyIndexer
189
218
  RUBY
190
219
  end
191
220
 
192
- assert_match(%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' enhancement}, stderr)
221
+ assert_match(
222
+ %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node leave enhancement},
223
+ stderr,
224
+ )
193
225
  # The module should still be indexed
194
226
  assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
195
227
  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
@@ -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,32 @@ 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_equal(Entry::Visibility::PRIVATE, first_entry.visibility)
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
+
126
152
  def test_method_with_parameters
127
153
  index(<<~RUBY)
128
154
  class Foo
@@ -100,7 +100,7 @@ module RubyIndexer
100
100
  end
101
101
 
102
102
  def test_location_and_name_location_are_the_same
103
- # NOTE: RBS does not store the name location for classes, modules or methods. This behaviour is not exactly what
103
+ # NOTE: RBS does not store the name location for classes, modules or methods. This behavior is not exactly what
104
104
  # we would like, but for now we assign the same location to both
105
105
 
106
106
  entries = @index["Array"]
@@ -46,7 +46,7 @@ module RubyLsp
46
46
  sig { returns(T::Array[T.class_of(Addon)]) }
47
47
  attr_reader :addon_classes
48
48
 
49
- # Automatically track and instantiate addon classes
49
+ # Automatically track and instantiate add-on classes
50
50
  sig { params(child_class: T.class_of(Addon)).void }
51
51
  def inherited(child_class)
52
52
  addon_classes << child_class
@@ -82,7 +82,7 @@ module RubyLsp
82
82
  e
83
83
  end
84
84
 
85
- # Instantiate all discovered addon classes
85
+ # Instantiate all discovered add-on classes
86
86
  self.addons = addon_classes.map(&:new)
87
87
  self.file_watcher_addons = addons.select { |addon| addon.respond_to?(:workspace_did_change_watched_files) }
88
88
 
@@ -194,6 +194,13 @@ module RubyLsp
194
194
  sig { abstract.returns(String) }
195
195
  def version; end
196
196
 
197
+ # Handle a response from a window/showMessageRequest request. Add-ons must include the addon_name as part of the
198
+ # original request so that the response is delegated to the correct add-on and must override this method to handle
199
+ # the response
200
+ # https://microsoft.github.io/language-server-protocol/specification#window_showMessageRequest
201
+ sig { overridable.params(title: String).void }
202
+ def handle_window_show_message_response(title); end
203
+
197
204
  # Creates a new CodeLens listener. This method is invoked on every CodeLens request
198
205
  sig do
199
206
  overridable.params(