ruby-lsp 0.20.1 → 0.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +19 -4
  5. data/exe/ruby-lsp-launcher +124 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +233 -59
  8. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +34 -16
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +15 -15
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +4 -4
  12. data/lib/ruby_indexer/test/configuration_test.rb +10 -0
  13. data/lib/ruby_indexer/test/constant_test.rb +8 -8
  14. data/lib/ruby_indexer/test/enhancements_test.rb +169 -41
  15. data/lib/ruby_indexer/test/index_test.rb +41 -2
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  17. data/lib/ruby_indexer/test/method_test.rb +139 -0
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  19. data/lib/ruby_lsp/addon.rb +9 -2
  20. data/lib/ruby_lsp/base_server.rb +14 -5
  21. data/lib/ruby_lsp/client_capabilities.rb +67 -0
  22. data/lib/ruby_lsp/document.rb +1 -1
  23. data/lib/ruby_lsp/global_state.rb +33 -20
  24. data/lib/ruby_lsp/internal.rb +3 -0
  25. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  26. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  27. data/lib/ruby_lsp/listeners/document_highlight.rb +91 -4
  28. data/lib/ruby_lsp/listeners/document_symbol.rb +37 -4
  29. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  30. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  31. data/lib/ruby_lsp/requests/completion.rb +7 -1
  32. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  33. data/lib/ruby_lsp/requests/definition.rb +28 -11
  34. data/lib/ruby_lsp/requests/document_highlight.rb +7 -1
  35. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  36. data/lib/ruby_lsp/requests/hover.rb +26 -6
  37. data/lib/ruby_lsp/requests/rename.rb +1 -1
  38. data/lib/ruby_lsp/requests/request.rb +1 -1
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +12 -1
  40. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  41. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  42. data/lib/ruby_lsp/server.rb +85 -55
  43. data/lib/ruby_lsp/setup_bundler.rb +154 -47
  44. data/lib/ruby_lsp/store.rb +0 -4
  45. data/lib/ruby_lsp/utils.rb +63 -0
  46. metadata +8 -3
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