ruby-lsp 0.20.1 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +19 -4
  5. data/exe/ruby-lsp-launcher +124 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +233 -59
  8. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +34 -16
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +15 -15
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +4 -4
  12. data/lib/ruby_indexer/test/configuration_test.rb +10 -0
  13. data/lib/ruby_indexer/test/constant_test.rb +8 -8
  14. data/lib/ruby_indexer/test/enhancements_test.rb +169 -41
  15. data/lib/ruby_indexer/test/index_test.rb +41 -2
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  17. data/lib/ruby_indexer/test/method_test.rb +139 -0
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  19. data/lib/ruby_lsp/addon.rb +9 -2
  20. data/lib/ruby_lsp/base_server.rb +14 -5
  21. data/lib/ruby_lsp/client_capabilities.rb +67 -0
  22. data/lib/ruby_lsp/document.rb +1 -1
  23. data/lib/ruby_lsp/global_state.rb +33 -20
  24. data/lib/ruby_lsp/internal.rb +3 -0
  25. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  26. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  27. data/lib/ruby_lsp/listeners/document_highlight.rb +91 -4
  28. data/lib/ruby_lsp/listeners/document_symbol.rb +37 -4
  29. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  30. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  31. data/lib/ruby_lsp/requests/completion.rb +7 -1
  32. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  33. data/lib/ruby_lsp/requests/definition.rb +28 -11
  34. data/lib/ruby_lsp/requests/document_highlight.rb +7 -1
  35. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  36. data/lib/ruby_lsp/requests/hover.rb +26 -6
  37. data/lib/ruby_lsp/requests/rename.rb +1 -1
  38. data/lib/ruby_lsp/requests/request.rb +1 -1
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +12 -1
  40. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  41. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  42. data/lib/ruby_lsp/server.rb +85 -55
  43. data/lib/ruby_lsp/setup_bundler.rb +154 -47
  44. data/lib/ruby_lsp/store.rb +0 -4
  45. data/lib/ruby_lsp/utils.rb +63 -0
  46. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58327a9f9a3d85375cbbf81e3e170a948d5c8dfbdcdcac0a1fdef5f685d20c95
4
- data.tar.gz: 45572eb6645bce73d2079bed65f0d92c98921517926b968bba96788769a3aada
3
+ metadata.gz: efd5671eae595026a17c3114a00de1053865ac828726dd7ac5a66548f6b1ad56
4
+ data.tar.gz: ccf5b7af12a6e1e327cc2d458be11f39a9ac7319ab9049863ed5a74e333e8a6f
5
5
  SHA512:
6
- metadata.gz: c506eeff4d24060e3afdb4ad9da319c12b725050c8f25a9348ec7dea9082f8adced7671b7b1eb8f7f09412942139e47a2df5059b5672d2b6a75f0ffc5a59885d
7
- data.tar.gz: d9a89ce24946e5f9cc47b5a48086f6fd977176d31332a0b634950dc00a41fc9a36e4753eae2896e2da0129e92bff4278265eaa970252a5292c783e5bc82258e9
6
+ metadata.gz: 8fbcdfaac508788a17fb3b7e939ce290e33049a0080ff6260abfaeb609ee76a033234ccc2efa5724587e9c4cbf4552e2cd5d2cb0af1186971b8ed06fec290cb3
7
+ data.tar.gz: 3e37acf250618e60df2addbce7f9acf86df4bf3e771b7b49c4cd8f4ad326ea82e3d41ad80c5ce71e3b0429dab98385df51f4645824cd419e32dac5ea13778760
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.22.1
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
@@ -50,10 +54,21 @@ rescue OptionParser::InvalidOption => e
50
54
  exit(1)
51
55
  end
52
56
 
53
- # When we're running without bundler, then we need to make sure the custom bundle is fully configured and re-execute
57
+ # When we're running without bundler, then we need to make sure the composed bundle is fully configured and re-execute
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,124 @@
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
+ end
78
+
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
+ # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
86
+ # configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
87
+ # paths into the load path manually or we may end up requiring the wrong version of the gem
88
+ require "ruby_lsp/load_sorbet"
89
+ require "ruby_lsp/internal"
90
+
91
+ T::Utils.run_all_sig_blocks
92
+
93
+ if ARGV.include?("--debug")
94
+ if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
95
+ $stderr.puts "Debugging is not supported on Windows"
96
+ else
97
+ begin
98
+ ENV.delete("RUBY_DEBUG_IRB_CONSOLE")
99
+ require "debug/open_nonstop"
100
+ rescue LoadError
101
+ $stderr.puts("You need to install the debug gem to use the --debug flag")
102
+ end
103
+ end
104
+ end
105
+
106
+ # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
107
+ $> = $stderr
108
+
109
+ initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize
110
+
111
+ begin
112
+ RubyLsp::Server.new(
113
+ install_error: install_error,
114
+ setup_error: setup_error,
115
+ initialize_request: initialize_request,
116
+ ).start
117
+ rescue ArgumentError
118
+ # If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
119
+ # and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
120
+ # request manually and then start the main loop
121
+ server = RubyLsp::Server.new
122
+ server.process_message(initialize_request)
123
+ server.start
124
+ end
@@ -109,6 +109,12 @@ module RubyIndexer
109
109
 
110
110
  indexables = T.let([], T::Array[IndexablePath])
111
111
 
112
+ # Handle top level files separately. The path below is an optimization to prevent descending down directories that
113
+ # are going to be excluded anyway, so we need to handle top level scripts separately
114
+ Dir.glob(File.join(@workspace_path, "*.rb"), flags).each do |path|
115
+ indexables << IndexablePath.new(nil, path)
116
+ end
117
+
112
118
  # Add user specified patterns
113
119
  @included_patterns.each do |pattern|
114
120
  load_path_entry = T.let(nil, T.nilable(String))
@@ -18,13 +18,12 @@ module RubyIndexer
18
18
  parse_result: Prism::ParseResult,
19
19
  file_path: String,
20
20
  collect_comments: T::Boolean,
21
- enhancements: T::Array[Enhancement],
22
21
  ).void
23
22
  end
24
- def initialize(index, dispatcher, parse_result, file_path, collect_comments: false, enhancements: [])
23
+ def initialize(index, dispatcher, parse_result, file_path, collect_comments: false)
25
24
  @index = index
26
25
  @file_path = file_path
27
- @enhancements = enhancements
26
+ @enhancements = T.let(Enhancement.all(self), T::Array[Enhancement])
28
27
  @visibility_stack = T.let([Entry::Visibility::PUBLIC], T::Array[Entry::Visibility])
29
28
  @comments_by_line = T.let(
30
29
  parse_result.comments.to_h do |c|
@@ -37,6 +36,7 @@ module RubyIndexer
37
36
  parse_result.code_units_cache(@index.configuration.encoding),
38
37
  T.any(T.proc.params(arg0: Integer).returns(Integer), Prism::CodeUnitsCache),
39
38
  )
39
+ @source_lines = T.let(parse_result.source.lines, T::Array[String])
40
40
 
41
41
  # The nesting stack we're currently inside. Used to determine the fully qualified name of constants, but only
42
42
  # stored by unresolved aliases which need the original nesting to be lazily resolved
@@ -85,15 +85,9 @@ module RubyIndexer
85
85
 
86
86
  sig { params(node: Prism::ClassNode).void }
87
87
  def on_class_node_enter(node)
88
- @visibility_stack.push(Entry::Visibility::PUBLIC)
89
88
  constant_path = node.constant_path
90
- name = constant_path.slice
91
-
92
- comments = collect_comments(node)
93
-
94
89
  superclass = node.superclass
95
-
96
- nesting = actual_nesting(name)
90
+ nesting = actual_nesting(constant_path.slice)
97
91
 
98
92
  parent_class = case superclass
99
93
  when Prism::ConstantReadNode, Prism::ConstantPathNode
@@ -112,53 +106,29 @@ module RubyIndexer
112
106
  end
113
107
  end
114
108
 
115
- entry = Entry::Class.new(
109
+ add_class(
116
110
  nesting,
117
- @file_path,
118
- Location.from_prism_location(node.location, @code_units_cache),
119
- Location.from_prism_location(constant_path.location, @code_units_cache),
120
- comments,
121
- parent_class,
111
+ node.location,
112
+ constant_path.location,
113
+ parent_class_name: parent_class,
114
+ comments: collect_comments(node),
122
115
  )
123
-
124
- @owner_stack << entry
125
- @index.add(entry)
126
- @stack << name
127
116
  end
128
117
 
129
118
  sig { params(node: Prism::ClassNode).void }
130
119
  def on_class_node_leave(node)
131
- @stack.pop
132
- @owner_stack.pop
133
- @visibility_stack.pop
120
+ pop_namespace_stack
134
121
  end
135
122
 
136
123
  sig { params(node: Prism::ModuleNode).void }
137
124
  def on_module_node_enter(node)
138
- @visibility_stack.push(Entry::Visibility::PUBLIC)
139
125
  constant_path = node.constant_path
140
- name = constant_path.slice
141
-
142
- comments = collect_comments(node)
143
-
144
- entry = Entry::Module.new(
145
- actual_nesting(name),
146
- @file_path,
147
- Location.from_prism_location(node.location, @code_units_cache),
148
- Location.from_prism_location(constant_path.location, @code_units_cache),
149
- comments,
150
- )
151
-
152
- @owner_stack << entry
153
- @index.add(entry)
154
- @stack << name
126
+ add_module(constant_path.slice, node.location, constant_path.location, comments: collect_comments(node))
155
127
  end
156
128
 
157
129
  sig { params(node: Prism::ModuleNode).void }
158
130
  def on_module_node_leave(node)
159
- @stack.pop
160
- @owner_stack.pop
161
- @visibility_stack.pop
131
+ pop_namespace_stack
162
132
  end
163
133
 
164
134
  sig { params(node: Prism::SingletonClassNode).void }
@@ -200,9 +170,7 @@ module RubyIndexer
200
170
 
201
171
  sig { params(node: Prism::SingletonClassNode).void }
202
172
  def on_singleton_class_node_leave(node)
203
- @stack.pop
204
- @owner_stack.pop
205
- @visibility_stack.pop
173
+ pop_namespace_stack
206
174
  end
207
175
 
208
176
  sig { params(node: Prism::MultiWriteNode).void }
@@ -312,12 +280,19 @@ module RubyIndexer
312
280
  @visibility_stack.push(Entry::Visibility::PROTECTED)
313
281
  when :private
314
282
  @visibility_stack.push(Entry::Visibility::PRIVATE)
283
+ when :module_function
284
+ handle_module_function(node)
285
+ when :private_class_method
286
+ @visibility_stack.push(Entry::Visibility::PRIVATE)
287
+ handle_private_class_method(node)
315
288
  end
316
289
 
317
290
  @enhancements.each do |enhancement|
318
- enhancement.on_call_node(@index, @owner_stack.last, node, @file_path, @code_units_cache)
291
+ enhancement.on_call_node_enter(node)
319
292
  rescue StandardError => e
320
- @indexing_errors << "Indexing error in #{@file_path} with '#{enhancement.class.name}' enhancement: #{e.message}"
293
+ @indexing_errors << <<~MSG
294
+ Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message}
295
+ MSG
321
296
  end
322
297
  end
323
298
 
@@ -325,13 +300,21 @@ module RubyIndexer
325
300
  def on_call_node_leave(node)
326
301
  message = node.name
327
302
  case message
328
- when :public, :protected, :private
303
+ when :public, :protected, :private, :private_class_method
329
304
  # We want to restore the visibility stack when we leave a method definition with a visibility modifier
330
305
  # e.g. `private def foo; end`
331
306
  if node.arguments&.arguments&.first&.is_a?(Prism::DefNode)
332
307
  @visibility_stack.pop
333
308
  end
334
309
  end
310
+
311
+ @enhancements.each do |enhancement|
312
+ enhancement.on_call_node_leave(node)
313
+ rescue StandardError => e
314
+ @indexing_errors << <<~MSG
315
+ Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message}
316
+ MSG
317
+ end
335
318
  end
336
319
 
337
320
  sig { params(node: Prism::DefNode).void }
@@ -451,6 +434,98 @@ module RubyIndexer
451
434
  )
452
435
  end
453
436
 
437
+ sig do
438
+ params(
439
+ name: String,
440
+ node_location: Prism::Location,
441
+ signatures: T::Array[Entry::Signature],
442
+ visibility: Entry::Visibility,
443
+ comments: T.nilable(String),
444
+ ).void
445
+ end
446
+ def add_method(name, node_location, signatures, visibility: Entry::Visibility::PUBLIC, comments: nil)
447
+ location = Location.from_prism_location(node_location, @code_units_cache)
448
+
449
+ @index.add(Entry::Method.new(
450
+ name,
451
+ @file_path,
452
+ location,
453
+ location,
454
+ comments,
455
+ signatures,
456
+ visibility,
457
+ @owner_stack.last,
458
+ ))
459
+ end
460
+
461
+ sig do
462
+ params(
463
+ name: String,
464
+ full_location: Prism::Location,
465
+ name_location: Prism::Location,
466
+ comments: T.nilable(String),
467
+ ).void
468
+ end
469
+ def add_module(name, full_location, name_location, comments: nil)
470
+ location = Location.from_prism_location(full_location, @code_units_cache)
471
+ name_loc = Location.from_prism_location(name_location, @code_units_cache)
472
+
473
+ entry = Entry::Module.new(
474
+ actual_nesting(name),
475
+ @file_path,
476
+ location,
477
+ name_loc,
478
+ comments,
479
+ )
480
+
481
+ advance_namespace_stack(name, entry)
482
+ end
483
+
484
+ sig do
485
+ params(
486
+ name_or_nesting: T.any(String, T::Array[String]),
487
+ full_location: Prism::Location,
488
+ name_location: Prism::Location,
489
+ parent_class_name: T.nilable(String),
490
+ comments: T.nilable(String),
491
+ ).void
492
+ end
493
+ def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil)
494
+ nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : actual_nesting(name_or_nesting)
495
+ entry = Entry::Class.new(
496
+ nesting,
497
+ @file_path,
498
+ Location.from_prism_location(full_location, @code_units_cache),
499
+ Location.from_prism_location(name_location, @code_units_cache),
500
+ comments,
501
+ parent_class_name,
502
+ )
503
+
504
+ advance_namespace_stack(T.must(nesting.last), entry)
505
+ end
506
+
507
+ sig { params(block: T.proc.params(index: Index, base: Entry::Namespace).void).void }
508
+ def register_included_hook(&block)
509
+ owner = @owner_stack.last
510
+ return unless owner
511
+
512
+ @index.register_included_hook(owner.name) do |index, base|
513
+ block.call(index, base)
514
+ end
515
+ end
516
+
517
+ sig { void }
518
+ def pop_namespace_stack
519
+ @stack.pop
520
+ @owner_stack.pop
521
+ @visibility_stack.pop
522
+ end
523
+
524
+ sig { returns(T.nilable(Entry::Namespace)) }
525
+ def current_owner
526
+ @owner_stack.last
527
+ end
528
+
454
529
  private
455
530
 
456
531
  sig do
@@ -649,8 +724,7 @@ module RubyIndexer
649
724
  comments = +""
650
725
 
651
726
  start_line = node.location.start_line - 1
652
- start_line -= 1 unless @comments_by_line.key?(start_line)
653
-
727
+ start_line -= 1 unless comment_exists_at?(start_line)
654
728
  start_line.downto(1) do |line|
655
729
  comment = @comments_by_line[line]
656
730
  break unless comment
@@ -671,6 +745,11 @@ module RubyIndexer
671
745
  comments
672
746
  end
673
747
 
748
+ sig { params(line: Integer).returns(T::Boolean) }
749
+ def comment_exists_at?(line)
750
+ @comments_by_line.key?(line) || !@source_lines[line - 1].to_s.strip.empty?
751
+ end
752
+
674
753
  sig { params(name: String).returns(String) }
675
754
  def fully_qualify_name(name)
676
755
  if @stack.empty? || name.start_with?("::")
@@ -734,16 +813,22 @@ module RubyIndexer
734
813
  return unless arguments
735
814
 
736
815
  arguments.each do |node|
737
- next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
738
-
739
- case operation
740
- when :include
741
- owner.mixin_operations << Entry::Include.new(node.full_name)
742
- when :prepend
743
- owner.mixin_operations << Entry::Prepend.new(node.full_name)
744
- when :extend
816
+ next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) ||
817
+ (node.is_a?(Prism::SelfNode) && operation == :extend)
818
+
819
+ if node.is_a?(Prism::SelfNode)
745
820
  singleton = @index.existing_or_new_singleton_class(owner.name)
746
- singleton.mixin_operations << Entry::Include.new(node.full_name)
821
+ singleton.mixin_operations << Entry::Include.new(owner.name)
822
+ else
823
+ case operation
824
+ when :include
825
+ owner.mixin_operations << Entry::Include.new(node.full_name)
826
+ when :prepend
827
+ owner.mixin_operations << Entry::Prepend.new(node.full_name)
828
+ when :extend
829
+ singleton = @index.existing_or_new_singleton_class(owner.name)
830
+ singleton.mixin_operations << Entry::Include.new(node.full_name)
831
+ end
747
832
  end
748
833
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
749
834
  Prism::ConstantPathNode::MissingNodesInConstantPathError
@@ -751,6 +836,87 @@ module RubyIndexer
751
836
  end
752
837
  end
753
838
 
839
+ sig { params(node: Prism::CallNode).void }
840
+ def handle_module_function(node)
841
+ arguments_node = node.arguments
842
+ return unless arguments_node
843
+
844
+ owner_name = @owner_stack.last&.name
845
+ return unless owner_name
846
+
847
+ arguments_node.arguments.each do |argument|
848
+ method_name = case argument
849
+ when Prism::StringNode
850
+ argument.content
851
+ when Prism::SymbolNode
852
+ argument.value
853
+ end
854
+ next unless method_name
855
+
856
+ entries = @index.resolve_method(method_name, owner_name)
857
+ next unless entries
858
+
859
+ entries.each do |entry|
860
+ entry_owner_name = entry.owner&.name
861
+ next unless entry_owner_name
862
+
863
+ entry.visibility = Entry::Visibility::PRIVATE
864
+
865
+ singleton = @index.existing_or_new_singleton_class(entry_owner_name)
866
+ location = Location.from_prism_location(argument.location, @code_units_cache)
867
+ @index.add(Entry::Method.new(
868
+ method_name,
869
+ @file_path,
870
+ location,
871
+ location,
872
+ collect_comments(node)&.concat(entry.comments),
873
+ entry.signatures,
874
+ Entry::Visibility::PUBLIC,
875
+ singleton,
876
+ ))
877
+ end
878
+ end
879
+ end
880
+
881
+ sig { params(node: Prism::CallNode).void }
882
+ def handle_private_class_method(node)
883
+ node.arguments&.arguments&.each do |argument|
884
+ string_or_symbol_nodes = case argument
885
+ when Prism::StringNode, Prism::SymbolNode
886
+ [argument]
887
+ when Prism::ArrayNode
888
+ argument.elements
889
+ else
890
+ []
891
+ end
892
+
893
+ unless string_or_symbol_nodes.empty?
894
+ # pop the visibility off since there isn't a method definition following `private_class_method`
895
+ @visibility_stack.pop
896
+ end
897
+
898
+ string_or_symbol_nodes.each do |string_or_symbol_node|
899
+ method_name = case string_or_symbol_node
900
+ when Prism::StringNode
901
+ string_or_symbol_node.content
902
+ when Prism::SymbolNode
903
+ string_or_symbol_node.value
904
+ end
905
+ next unless method_name
906
+
907
+ owner_name = @owner_stack.last&.name
908
+ next unless owner_name
909
+
910
+ entries = @index.resolve_method(method_name, @index.existing_or_new_singleton_class(owner_name).name)
911
+ next unless entries
912
+
913
+ entries.each do |entry|
914
+ entry.visibility = Entry::Visibility::PRIVATE
915
+ end
916
+ end
917
+ end
918
+ end
919
+
754
920
  sig { returns(Entry::Visibility) }
755
921
  def current_visibility
756
922
  T.must(@visibility_stack.last)
@@ -856,5 +1022,13 @@ module RubyIndexer
856
1022
 
857
1023
  corrected_nesting
858
1024
  end
1025
+
1026
+ sig { params(short_name: String, entry: Entry::Namespace).void }
1027
+ def advance_namespace_stack(short_name, entry)
1028
+ @visibility_stack.push(Entry::Visibility::PUBLIC)
1029
+ @owner_stack << entry
1030
+ @index.add(entry)
1031
+ @stack << short_name
1032
+ end
859
1033
  end
860
1034
  end
@@ -2,29 +2,47 @@
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
+ @enhancements = T.let([], T::Array[T::Class[Enhancement]])
12
+
13
+ class << self
14
+ extend T::Sig
15
+
16
+ sig { params(child: T::Class[Enhancement]).void }
17
+ def inherited(child)
18
+ @enhancements << child
19
+ super
20
+ end
21
+
22
+ sig { params(listener: DeclarationListener).returns(T::Array[Enhancement]) }
23
+ def all(listener)
24
+ @enhancements.map { |enhancement| enhancement.new(listener) }
25
+ end
26
+
27
+ # Only available for testing purposes
28
+ sig { void }
29
+ def clear
30
+ @enhancements.clear
31
+ end
32
+ end
33
+
34
+ sig { params(listener: DeclarationListener).void }
35
+ def initialize(listener)
36
+ @listener = listener
37
+ end
12
38
 
13
39
  # The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
14
40
  # register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
15
41
  # `ClassMethods` modules
16
- sig do
17
- abstract.params(
18
- index: Index,
19
- owner: T.nilable(Entry::Namespace),
20
- node: Prism::CallNode,
21
- file_path: String,
22
- code_units_cache: T.any(
23
- T.proc.params(arg0: Integer).returns(Integer),
24
- Prism::CodeUnitsCache,
25
- ),
26
- ).void
27
- end
28
- def on_call_node(index, owner, node, file_path, code_units_cache); end
42
+ sig { overridable.params(node: Prism::CallNode).void }
43
+ def on_call_node_enter(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
44
+
45
+ sig { overridable.params(node: Prism::CallNode).void }
46
+ def on_call_node_leave(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
29
47
  end
30
48
  end