ruby-lsp 0.17.17 → 0.18.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -110
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +5 -4
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +14 -6
  6. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +157 -27
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +5 -4
  8. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +10 -10
  10. data/lib/ruby_indexer/test/constant_test.rb +4 -4
  11. data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
  12. data/lib/ruby_indexer/test/method_test.rb +257 -2
  13. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  14. data/lib/ruby_lsp/base_server.rb +21 -1
  15. data/lib/ruby_lsp/document.rb +5 -3
  16. data/lib/ruby_lsp/erb_document.rb +29 -10
  17. data/lib/ruby_lsp/global_state.rb +3 -1
  18. data/lib/ruby_lsp/listeners/code_lens.rb +34 -5
  19. data/lib/ruby_lsp/listeners/signature_help.rb +55 -24
  20. data/lib/ruby_lsp/rbs_document.rb +5 -4
  21. data/lib/ruby_lsp/requests/code_action_resolve.rb +0 -15
  22. data/lib/ruby_lsp/requests/code_actions.rb +0 -10
  23. data/lib/ruby_lsp/requests/code_lens.rb +1 -11
  24. data/lib/ruby_lsp/requests/completion.rb +3 -20
  25. data/lib/ruby_lsp/requests/completion_resolve.rb +0 -8
  26. data/lib/ruby_lsp/requests/definition.rb +6 -20
  27. data/lib/ruby_lsp/requests/diagnostics.rb +0 -10
  28. data/lib/ruby_lsp/requests/document_highlight.rb +7 -14
  29. data/lib/ruby_lsp/requests/document_link.rb +0 -10
  30. data/lib/ruby_lsp/requests/document_symbol.rb +0 -17
  31. data/lib/ruby_lsp/requests/folding_ranges.rb +0 -10
  32. data/lib/ruby_lsp/requests/formatting.rb +0 -16
  33. data/lib/ruby_lsp/requests/hover.rb +9 -9
  34. data/lib/ruby_lsp/requests/inlay_hints.rb +0 -30
  35. data/lib/ruby_lsp/requests/on_type_formatting.rb +0 -10
  36. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +0 -11
  37. data/lib/ruby_lsp/requests/request.rb +17 -1
  38. data/lib/ruby_lsp/requests/selection_ranges.rb +0 -10
  39. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -23
  40. data/lib/ruby_lsp/requests/show_syntax_tree.rb +0 -11
  41. data/lib/ruby_lsp/requests/signature_help.rb +5 -20
  42. data/lib/ruby_lsp/requests/support/common.rb +1 -1
  43. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +2 -0
  44. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +0 -11
  45. data/lib/ruby_lsp/requests/workspace_symbol.rb +0 -12
  46. data/lib/ruby_lsp/ruby_document.rb +4 -3
  47. data/lib/ruby_lsp/server.rb +23 -8
  48. data/lib/ruby_lsp/setup_bundler.rb +31 -13
  49. data/lib/ruby_lsp/type_inferrer.rb +6 -2
  50. data/lib/ruby_lsp/utils.rb +11 -1
  51. metadata +3 -4
  52. data/lib/ruby_lsp/check_docs.rb +0 -130
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4b4c9204834ddf87c84009003c5eef5872211eaef6ed64022a93a2749e1849e
4
- data.tar.gz: 0d770cf2f67f90fb974a9fca08d9fb4dea1a73d668984ff96e5cc5785d7c5766
3
+ metadata.gz: 407c26b9ad43a3992fc5b007456dba7c370f4ba6bf398a68bebdfaaafc58ba81
4
+ data.tar.gz: b890b6762b8d1b4fe43632d6d74e3b0fc1e83168f8d51ccbd2b659df389bff42
5
5
  SHA512:
6
- metadata.gz: 3534227d67761a6738c5e2ae4a06a08964a0836c4a30c9d306917efdd93aae1285d6ed03992e88486a44e5d3f7e7271997e56bbb12ab908599687c6a1e622cb4
7
- data.tar.gz: 5345bcf362b6206a0be392eebe5db2175fe9d06b1a38f113fe11d979e5fdcc48851cb47e5f016ae49726ffc73432e81ef801522588cee79ebb36fe545e02d965
6
+ metadata.gz: 75a2a425311f1b07969fea0849b7b214babd8b455d1a93a4c1883a75df7f26ec9d7c7e2748a7499a404313eb93ec3391a19d22102bc0658d9eb296f4ceaf48a4
7
+ data.tar.gz: 536fd4d95c63b493f83a1620307d21f8aa2c96b71bc3782975f55173bc5c7027be5399928a19508bb2ed08f222ae86d075d721e30b334dae7fa097caf71fb558
data/README.md CHANGED
@@ -15,119 +15,13 @@ experience to Ruby developers using modern standards for cross-editor features,
15
15
  Want to discuss Ruby developer experience? Consider joining the public
16
16
  [Ruby DX Slack workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q).
17
17
 
18
- ## Features
18
+ ## Getting Started
19
19
 
20
- ![Ruby LSP demo](vscode/extras/ruby_lsp_demo.gif)
20
+ For VS Code users, you can start by installing the [Ruby LSP extension](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) from the VS Code marketplace.
21
21
 
22
- The Ruby LSP features include
22
+ For other editors, please refer to the [EDITORS](https://shopify.github.io/ruby-lsp/editors.html) guide.
23
23
 
24
- - Semantic highlighting
25
- - Symbol search and code outline
26
- - RuboCop errors and warnings (diagnostics)
27
- - Format on save (with RuboCop or Syntax Tree)
28
- - Format on type
29
- - Debugging support
30
- - Running and debugging tests through VS Code's UI
31
- - Go to definition for classes, modules, constants and required files
32
- - Showing documentation on hover for classes, modules and constants
33
- - Completion for classes, modules, constants and require paths
34
- - Fuzzy search classes, modules and constants anywhere in the project and its dependencies (workspace symbol)
35
-
36
- As of July 2024, Ruby LSP has received significant enhancements to its code navigation features. For an in-depth look at these improvements, including video demonstrations, check out this [article](https://railsatscale.com/2024-07-18-mastering-ruby-code-navigation-major-enhancements-in-ruby-lsp-2024/). Despite these advancements, we plan to continue enhancing its code navigation support even further. You can follow our progress on this [GitHub issue](https://github.com/Shopify/ruby-lsp/issues/899).
37
-
38
- See complete information about features [here](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
39
-
40
- If you experience issues, please see the [troubleshooting
41
- guide](https://github.com/Shopify/ruby-lsp/blob/main/TROUBLESHOOTING.md).
42
-
43
- ## Usage
44
-
45
- ### With VS Code
46
-
47
- If using VS Code, all you have to do is install the [Ruby LSP
48
- extension](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) to get the extra features in the
49
- editor. Do not install the `ruby-lsp` gem manually.
50
-
51
- For more information on using and configuring the extension, see [vscode/README.md](vscode/README.md).
52
-
53
- ### With other editors
54
-
55
- See [editors](EDITORS.md) for community instructions on setting up the Ruby LSP, which current includes Emacs, Neovim, Sublime Text, and Zed.
56
-
57
- The gem can be installed by doing
58
- ```shell
59
- gem install ruby-lsp
60
- ```
61
-
62
- and the language server can be launched running `ruby-lsp` (without bundle exec in order to properly hook into your
63
- project's dependencies).
64
-
65
- ### Documentation
66
-
67
- See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
68
- [supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
69
-
70
- For creating rich themes for Ruby using the semantic highlighting information, see the [semantic highlighting
71
- documentation](SEMANTIC_HIGHLIGHTING.md).
72
-
73
- ### Configuring code indexing
74
-
75
- By default, the Ruby LSP indexes all Ruby files defined in the current project and all of its dependencies, including
76
- default gems, except for
77
-
78
- - Gems that only appear under the `:development` group
79
- - All Ruby files under `test/**/*.rb`
80
-
81
- This behaviour can be overridden and tuned. Learn how to configure it [for VS Code](vscode/README.md#Indexing-Configuration) or [for other editors](EDITORS.md#Indexing-Configuration).
82
-
83
- Note that indexing-dependent behavior, such as definition, hover, completion or workspace symbol will be impacted by
84
- the configuration changes.
85
-
86
- The older approach of using a `.index.yml` file has been deprecated and will be removed in a future release.
87
-
88
- ```yaml
89
- # Exclude files based on a given pattern. Often used to exclude test files or fixtures
90
- excluded_patterns:
91
- - "**/spec/**/*.rb"
92
-
93
- # Include files based on a given pattern. Can be used to index Ruby files that use different extensions
94
- included_patterns:
95
- - "**/bin/*"
96
-
97
- # Exclude gems by name. If a gem is never referenced in the project's code and is only used as a tool, excluding it will
98
- # speed up indexing and reduce the amount of results in features like definition or completion
99
- excluded_gems:
100
- - rubocop
101
- - pathname
102
-
103
- # Include gems by name. Normally used to include development gems that are excluded by default
104
- included_gems:
105
- - prism
106
- ```
107
-
108
- ### Addons
109
-
110
- The Ruby LSP provides an addon system that allows other gems to enhance the base functionality with more editor
111
- features. This is the mechanism that powers addons like
112
-
113
- - [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails)
114
- - [Ruby LSP RSpec](https://github.com/st0012/ruby-lsp-rspec)
115
- - [Ruby LSP rubyfmt](https://github.com/jscharf/ruby-lsp-rubyfmt)
116
-
117
- Additionally, some tools may include a Ruby LSP addon directly, like
118
-
119
- - [Standard Ruby (from v1.39.1)](https://github.com/standardrb/standard/wiki/IDE:-vscode#using-ruby-lsp)
120
-
121
- Other community driven addons can be found in [rubygems](https://rubygems.org/search?query=name%3A+ruby-lsp) by
122
- searching for the `ruby-lsp` prefix.
123
-
124
- For instructions on how to create addons, see the [addons documentation](ADDONS.md).
125
-
126
- ## Learn More
127
-
128
- * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
129
- * [Remote Ruby: Ruby Language Server with Vinicius Stock](https://remoteruby.com/221)
130
- * [RubyKaigi 2023: Code indexing - How language servers understand our code](https://www.youtube.com/watch?v=ks3tQojSJLU) ([Vinicius Stock](https://github.com/vinistock))
24
+ To learn more about Ruby LSP, please refer to the official [documentation](https://shopify.github.io/ruby-lsp) for [supported features](https://shopify.github.io/ruby-lsp#features).
131
25
 
132
26
  ## Contributing
133
27
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.17
1
+ 0.18.0
data/exe/ruby-lsp CHANGED
@@ -64,15 +64,12 @@ if ENV["BUNDLE_GEMFILE"].nil?
64
64
  require_relative "../lib/ruby_lsp/setup_bundler"
65
65
 
66
66
  begin
67
- bundle_gemfile, bundle_path, bundle_app_config = RubyLsp::SetupBundler.new(Dir.pwd, **options).setup!
67
+ env = RubyLsp::SetupBundler.new(Dir.pwd, **options).setup!
68
68
  rescue RubyLsp::SetupBundler::BundleNotLocked
69
69
  warn("Project contains a Gemfile, but no Gemfile.lock. Run `bundle install` to lock gems and restart the server")
70
70
  exit(78)
71
71
  end
72
72
 
73
- env = { "BUNDLE_GEMFILE" => bundle_gemfile }
74
- env["BUNDLE_PATH"] = bundle_path if bundle_path
75
- env["BUNDLE_APP_CONFIG"] = bundle_app_config if bundle_app_config
76
73
  exit exec(env, "bundle exec ruby-lsp #{original_args.join(" ")}")
77
74
  end
78
75
 
@@ -135,4 +132,8 @@ if options[:doctor]
135
132
  return
136
133
  end
137
134
 
135
+ # Ensure all output goes out stderr by default to allow puts/p/pp to work
136
+ # without specifying output device.
137
+ $> = $stderr
138
+
138
139
  RubyLsp::Server.new.start
@@ -17,10 +17,11 @@ module RubyIndexer
17
17
  dispatcher: Prism::Dispatcher,
18
18
  parse_result: Prism::ParseResult,
19
19
  file_path: String,
20
+ collect_comments: T::Boolean,
20
21
  enhancements: T::Array[Enhancement],
21
22
  ).void
22
23
  end
23
- def initialize(index, dispatcher, parse_result, file_path, enhancements: [])
24
+ def initialize(index, dispatcher, parse_result, file_path, collect_comments: false, enhancements: [])
24
25
  @index = index
25
26
  @file_path = file_path
26
27
  @enhancements = enhancements
@@ -40,6 +41,7 @@ module RubyIndexer
40
41
  # A stack of namespace entries that represent where we currently are. Used to properly assign methods to an owner
41
42
  @owner_stack = T.let([], T::Array[Entry::Namespace])
42
43
  @indexing_errors = T.let([], T::Array[String])
44
+ @collect_comments = collect_comments
43
45
 
44
46
  dispatcher.register(
45
47
  self,
@@ -540,9 +542,11 @@ module RubyIndexer
540
542
  )
541
543
  end
542
544
 
543
- sig { params(node: Prism::Node).returns(T::Array[String]) }
545
+ sig { params(node: Prism::Node).returns(T.nilable(String)) }
544
546
  def collect_comments(node)
545
- comments = []
547
+ return unless @collect_comments
548
+
549
+ comments = +""
546
550
 
547
551
  start_line = node.location.start_line - 1
548
552
  start_line -= 1 unless @comments_by_line.key?(start_line)
@@ -551,7 +555,7 @@ module RubyIndexer
551
555
  comment = @comments_by_line[line]
552
556
  break unless comment
553
557
 
554
- comment_content = comment.location.slice.chomp
558
+ comment_content = comment.location.slice
555
559
 
556
560
  # invalid encodings would raise an "invalid byte sequence" exception
557
561
  if !comment_content.valid_encoding? || comment_content.match?(@index.configuration.magic_comment_regex)
@@ -560,9 +564,10 @@ module RubyIndexer
560
564
 
561
565
  comment_content.delete_prefix!("#")
562
566
  comment_content.delete_prefix!(" ")
563
- comments.prepend(comment_content)
567
+ comments.prepend("#{comment_content}\n")
564
568
  end
565
569
 
570
+ comments.chomp!
566
571
  comments
567
572
  end
568
573
 
@@ -685,9 +690,12 @@ module RubyIndexer
685
690
 
686
691
  keyword_rest = parameters_node.keyword_rest
687
692
 
688
- if keyword_rest.is_a?(Prism::KeywordRestParameterNode)
693
+ case keyword_rest
694
+ when Prism::KeywordRestParameterNode
689
695
  keyword_rest_name = parameter_name(keyword_rest) || Entry::KeywordRestParameter::DEFAULT_NAME
690
696
  parameters << Entry::KeywordRestParameter.new(name: keyword_rest_name)
697
+ when Prism::ForwardingParameterNode
698
+ parameters << Entry::ForwardingParameter.new
691
699
  end
692
700
 
693
701
  parameters_node.posts.each do |post|
@@ -24,9 +24,6 @@ module RubyIndexer
24
24
 
25
25
  alias_method :name_location, :location
26
26
 
27
- sig { returns(T::Array[String]) }
28
- attr_reader :comments
29
-
30
27
  sig { returns(Visibility) }
31
28
  attr_accessor :visibility
32
29
 
@@ -35,7 +32,7 @@ module RubyIndexer
35
32
  name: String,
36
33
  file_path: String,
37
34
  location: T.any(Prism::Location, RubyIndexer::Location),
38
- comments: T::Array[String],
35
+ comments: T.nilable(String),
39
36
  ).void
40
37
  end
41
38
  def initialize(name, file_path, location, comments)
@@ -79,6 +76,37 @@ module RubyIndexer
79
76
  File.basename(@file_path)
80
77
  end
81
78
 
79
+ sig { returns(String) }
80
+ def comments
81
+ @comments ||= begin
82
+ # Parse only the comments based on the file path, which is much faster than parsing the entire file
83
+ parsed_comments = Prism.parse_file_comments(@file_path)
84
+
85
+ # Group comments based on whether they belong to a single block of comments
86
+ grouped = parsed_comments.slice_when do |left, right|
87
+ left.location.start_line + 1 != right.location.start_line
88
+ end
89
+
90
+ # Find the group that is either immediately or two lines above the current entry
91
+ correct_group = grouped.find do |group|
92
+ comment_end_line = group.last.location.start_line
93
+ (comment_end_line - 1..comment_end_line).cover?(@location.start_line - 1)
94
+ end
95
+
96
+ # If we found something, we join the comments together. Otherwise, the entry has no documentation and we don't
97
+ # want to accidentally re-parse it, so we set it to an empty string. If an entry is updated, the entire entry
98
+ # object is dropped, so this will not prevent updates
99
+ if correct_group
100
+ correct_group.filter_map do |comment|
101
+ content = comment.slice
102
+ content if content.valid_encoding?
103
+ end.join("\n")
104
+ else
105
+ ""
106
+ end
107
+ end
108
+ end
109
+
82
110
  class ModuleOperation
83
111
  extend T::Sig
84
112
  extend T::Helpers
@@ -116,7 +144,7 @@ module RubyIndexer
116
144
  file_path: String,
117
145
  location: T.any(Prism::Location, RubyIndexer::Location),
118
146
  name_location: T.any(Prism::Location, Location),
119
- comments: T::Array[String],
147
+ comments: T.nilable(String),
120
148
  ).void
121
149
  end
122
150
  def initialize(nesting, file_path, location, name_location, comments)
@@ -177,7 +205,7 @@ module RubyIndexer
177
205
  file_path: String,
178
206
  location: T.any(Prism::Location, RubyIndexer::Location),
179
207
  name_location: T.any(Prism::Location, Location),
180
- comments: T::Array[String],
208
+ comments: T.nilable(String),
181
209
  parent_class: T.nilable(String),
182
210
  ).void
183
211
  end
@@ -195,7 +223,7 @@ module RubyIndexer
195
223
  class SingletonClass < Class
196
224
  extend T::Sig
197
225
 
198
- sig { params(location: Prism::Location, name_location: Prism::Location, comments: T::Array[String]).void }
226
+ sig { params(location: Prism::Location, name_location: Prism::Location, comments: T.nilable(String)).void }
199
227
  def update_singleton_information(location, name_location, comments)
200
228
  # Create a new RubyIndexer::Location object from the Prism location
201
229
  @location = Location.new(
@@ -210,7 +238,7 @@ module RubyIndexer
210
238
  name_location.start_column,
211
239
  name_location.end_column,
212
240
  )
213
- @comments.concat(comments)
241
+ (@comments ||= +"") << comments if comments
214
242
  end
215
243
  end
216
244
 
@@ -302,6 +330,17 @@ module RubyIndexer
302
330
  end
303
331
  end
304
332
 
333
+ # A forwarding method parameter, e.g. `def foo(...)`
334
+ class ForwardingParameter < Parameter
335
+ extend T::Sig
336
+
337
+ sig { void }
338
+ def initialize
339
+ # You can't name a forwarding parameter, it's always called `...`
340
+ super(name: :"...")
341
+ end
342
+ end
343
+
305
344
  class Member < Entry
306
345
  extend T::Sig
307
346
  extend T::Helpers
@@ -311,17 +350,12 @@ module RubyIndexer
311
350
  sig { returns(T.nilable(Entry::Namespace)) }
312
351
  attr_reader :owner
313
352
 
314
- sig { returns(T::Array[RubyIndexer::Entry::Parameter]) }
315
- def parameters
316
- T.must(signatures.first).parameters
317
- end
318
-
319
353
  sig do
320
354
  params(
321
355
  name: String,
322
356
  file_path: String,
323
357
  location: T.any(Prism::Location, RubyIndexer::Location),
324
- comments: T::Array[String],
358
+ comments: T.nilable(String),
325
359
  visibility: Visibility,
326
360
  owner: T.nilable(Entry::Namespace),
327
361
  ).void
@@ -389,7 +423,7 @@ module RubyIndexer
389
423
  file_path: String,
390
424
  location: T.any(Prism::Location, RubyIndexer::Location),
391
425
  name_location: T.any(Prism::Location, Location),
392
- comments: T::Array[String],
426
+ comments: T.nilable(String),
393
427
  signatures: T::Array[Signature],
394
428
  visibility: Visibility,
395
429
  owner: T.nilable(Entry::Namespace),
@@ -440,7 +474,7 @@ module RubyIndexer
440
474
  name: String,
441
475
  file_path: String,
442
476
  location: T.any(Prism::Location, RubyIndexer::Location),
443
- comments: T::Array[String],
477
+ comments: T.nilable(String),
444
478
  ).void
445
479
  end
446
480
  def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
@@ -477,7 +511,7 @@ module RubyIndexer
477
511
  name: String,
478
512
  file_path: String,
479
513
  location: T.any(Prism::Location, RubyIndexer::Location),
480
- comments: T::Array[String],
514
+ comments: T.nilable(String),
481
515
  owner: T.nilable(Entry::Namespace),
482
516
  ).void
483
517
  end
@@ -506,7 +540,7 @@ module RubyIndexer
506
540
  owner: T.nilable(Entry::Namespace),
507
541
  file_path: String,
508
542
  location: T.any(Prism::Location, RubyIndexer::Location),
509
- comments: T::Array[String],
543
+ comments: T.nilable(String),
510
544
  ).void
511
545
  end
512
546
  def initialize(new_name, old_name, owner, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
@@ -530,10 +564,9 @@ module RubyIndexer
530
564
 
531
565
  sig { params(target: T.any(Member, MethodAlias), unresolved_alias: UnresolvedMethodAlias).void }
532
566
  def initialize(target, unresolved_alias)
533
- full_comments = ["Alias for #{target.name}\n"]
534
- full_comments.concat(unresolved_alias.comments)
535
- full_comments << "\n"
536
- full_comments.concat(target.comments)
567
+ full_comments = +"Alias for #{target.name}\n"
568
+ full_comments << "#{unresolved_alias.comments}\n"
569
+ full_comments << target.comments
537
570
 
538
571
  super(
539
572
  unresolved_alias.new_name,
@@ -546,11 +579,6 @@ module RubyIndexer
546
579
  @owner = T.let(unresolved_alias.owner, T.nilable(Entry::Namespace))
547
580
  end
548
581
 
549
- sig { returns(T::Array[Parameter]) }
550
- def parameters
551
- @target.parameters
552
- end
553
-
554
582
  sig { returns(String) }
555
583
  def decorated_parameters
556
584
  @target.decorated_parameters
@@ -586,6 +614,108 @@ module RubyIndexer
586
614
  def format
587
615
  @parameters.map(&:decorated_name).join(", ")
588
616
  end
617
+
618
+ # Returns `true` if the given call node arguments array matches this method signature. This method will prefer
619
+ # returning `true` for situations that cannot be analyzed statically, like the presence of splats, keyword splats
620
+ # or forwarding arguments.
621
+ #
622
+ # Since this method is used to detect which overload should be displayed in signature help, it will also return
623
+ # `true` if there are missing arguments since the user may not be done typing yet. For example:
624
+ #
625
+ # ```ruby
626
+ # def foo(a, b); end
627
+ # # All of the following are considered matches because the user might be in the middle of typing and we have to
628
+ # # show them the signature
629
+ # foo
630
+ # foo(1)
631
+ # foo(1, 2)
632
+ # ```
633
+ sig { params(arguments: T::Array[Prism::Node]).returns(T::Boolean) }
634
+ def matches?(arguments)
635
+ min_pos = 0
636
+ max_pos = T.let(0, T.any(Integer, Float))
637
+ names = []
638
+ has_forward = T.let(false, T::Boolean)
639
+ has_keyword_rest = T.let(false, T::Boolean)
640
+
641
+ @parameters.each do |param|
642
+ case param
643
+ when RequiredParameter
644
+ min_pos += 1
645
+ max_pos += 1
646
+ when OptionalParameter
647
+ max_pos += 1
648
+ when RestParameter
649
+ max_pos = Float::INFINITY
650
+ when ForwardingParameter
651
+ max_pos = Float::INFINITY
652
+ has_forward = true
653
+ when KeywordParameter, OptionalKeywordParameter
654
+ names << param.name
655
+ when KeywordRestParameter
656
+ has_keyword_rest = true
657
+ end
658
+ end
659
+
660
+ keyword_hash_nodes, positional_args = arguments.partition { |arg| arg.is_a?(Prism::KeywordHashNode) }
661
+ keyword_args = T.cast(keyword_hash_nodes.first, T.nilable(Prism::KeywordHashNode))&.elements
662
+ forwarding_arguments, positionals = positional_args.partition do |arg|
663
+ arg.is_a?(Prism::ForwardingArgumentsNode)
664
+ end
665
+
666
+ return true if has_forward && min_pos == 0
667
+
668
+ # If the only argument passed is a forwarding argument, then anything will match
669
+ (positionals.empty? && forwarding_arguments.any?) ||
670
+ (
671
+ # Check if positional arguments match. This includes required, optional, rest arguments. We also need to
672
+ # verify if there's a trailing forwading argument, like `def foo(a, ...); end`
673
+ positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) &&
674
+ # If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest
675
+ # arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest
676
+ # (**kwargs), then we can't analyze statically because the user could be passing a hash and we don't know
677
+ # what the runtime values inside the hash are.
678
+ #
679
+ # If none of those match, then we verify if the user is passing the expect names for the keyword arguments
680
+ (has_forward || has_keyword_rest || keyword_arguments_match?(keyword_args, names))
681
+ )
682
+ end
683
+
684
+ sig do
685
+ params(
686
+ positional_args: T::Array[Prism::Node],
687
+ forwarding_arguments: T::Array[Prism::Node],
688
+ keyword_args: T.nilable(T::Array[Prism::Node]),
689
+ min_pos: Integer,
690
+ max_pos: T.any(Integer, Float),
691
+ ).returns(T::Boolean)
692
+ end
693
+ def positional_arguments_match?(positional_args, forwarding_arguments, keyword_args, min_pos, max_pos)
694
+ # If the method accepts at least one positional argument and a splat has been passed
695
+ (min_pos > 0 && positional_args.any? { |arg| arg.is_a?(Prism::SplatNode) }) ||
696
+ # If there's at least one positional argument unaccounted for and a keyword splat has been passed
697
+ (min_pos - positional_args.length > 0 && keyword_args&.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }) ||
698
+ # If there's at least one positional argument unaccounted for and a forwarding argument has been passed
699
+ (min_pos - positional_args.length > 0 && forwarding_arguments.any?) ||
700
+ # If the number of positional arguments is within the expected range
701
+ (min_pos > 0 && positional_args.length <= max_pos) ||
702
+ (min_pos == 0 && positional_args.empty?)
703
+ end
704
+
705
+ sig { params(args: T.nilable(T::Array[Prism::Node]), names: T::Array[Symbol]).returns(T::Boolean) }
706
+ def keyword_arguments_match?(args, names)
707
+ return true unless args
708
+ return true if args.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }
709
+
710
+ arg_names = args.filter_map do |arg|
711
+ next unless arg.is_a?(Prism::AssocNode)
712
+
713
+ key = arg.key
714
+ key.value&.to_sym if key.is_a?(Prism::SymbolNode)
715
+ end
716
+
717
+ (arg_names - names).empty?
718
+ end
589
719
  end
590
720
  end
591
721
  end
@@ -312,12 +312,12 @@ module RubyIndexer
312
312
  break unless block.call(progress)
313
313
  end
314
314
 
315
- index_single(path)
315
+ index_single(path, collect_comments: false)
316
316
  end
317
317
  end
318
318
 
319
- sig { params(indexable_path: IndexablePath, source: T.nilable(String)).void }
320
- def index_single(indexable_path, source = nil)
319
+ sig { params(indexable_path: IndexablePath, source: T.nilable(String), collect_comments: T::Boolean).void }
320
+ def index_single(indexable_path, source = nil, collect_comments: true)
321
321
  content = source || File.read(indexable_path.full_path)
322
322
  dispatcher = Prism::Dispatcher.new
323
323
 
@@ -327,6 +327,7 @@ module RubyIndexer
327
327
  dispatcher,
328
328
  result,
329
329
  indexable_path.full_path,
330
+ collect_comments: collect_comments,
330
331
  enhancements: @enhancements,
331
332
  )
332
333
  dispatcher.dispatch(result.value)
@@ -607,7 +608,7 @@ module RubyIndexer
607
608
  attached_ancestor.file_path,
608
609
  attached_ancestor.location,
609
610
  attached_ancestor.name_location,
610
- [],
611
+ nil,
611
612
  nil,
612
613
  )
613
614
  add(singleton, skip_prefix_tree: true)
@@ -272,10 +272,10 @@ module RubyIndexer
272
272
  RBS::AST::Declarations::Constant,
273
273
  RBS::AST::Members::MethodDefinition,
274
274
  RBS::AST::Members::Alias,
275
- )).returns(T::Array[String])
275
+ )).returns(T.nilable(String))
276
276
  end
277
277
  def comments_to_string(declaration)
278
- Array(declaration.comment&.string)
278
+ declaration.comment&.string
279
279
  end
280
280
  end
281
281
  end
@@ -213,10 +213,10 @@ module RubyIndexer
213
213
  RUBY
214
214
 
215
215
  foo_entry = @index["Foo"].first
216
- assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
216
+ assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
217
217
 
218
218
  bar_entry = @index["Bar"].first
219
- assert_equal("This Bar comment has 1 line padding", bar_entry.comments.join("\n"))
219
+ assert_equal("This Bar comment has 1 line padding", bar_entry.comments)
220
220
  end
221
221
 
222
222
  def test_skips_comments_containing_invalid_encodings
@@ -239,10 +239,10 @@ module RubyIndexer
239
239
  RUBY
240
240
 
241
241
  foo_entry = @index["Foo"].first
242
- assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
242
+ assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
243
243
 
244
244
  bar_entry = @index["Foo::Bar"].first
245
- assert_equal("This is a Bar comment", bar_entry.comments.join("\n"))
245
+ assert_equal("This is a Bar comment", bar_entry.comments)
246
246
  end
247
247
 
248
248
  def test_comments_can_be_attached_to_a_reopened_class
@@ -255,10 +255,10 @@ module RubyIndexer
255
255
  RUBY
256
256
 
257
257
  first_foo_entry = @index["Foo"][0]
258
- assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
258
+ assert_equal("This is a Foo comment", first_foo_entry.comments)
259
259
 
260
260
  second_foo_entry = @index["Foo"][1]
261
- assert_equal("This is another Foo comment", second_foo_entry.comments.join("\n"))
261
+ assert_equal("This is another Foo comment", second_foo_entry.comments)
262
262
  end
263
263
 
264
264
  def test_comments_removes_the_leading_pound_and_space
@@ -271,10 +271,10 @@ module RubyIndexer
271
271
  RUBY
272
272
 
273
273
  first_foo_entry = @index["Foo"][0]
274
- assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
274
+ assert_equal("This is a Foo comment", first_foo_entry.comments)
275
275
 
276
276
  second_foo_entry = @index["Bar"][0]
277
- assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n"))
277
+ assert_equal("This is a Bar comment", second_foo_entry.comments)
278
278
  end
279
279
 
280
280
  def test_private_class_and_module_indexing
@@ -483,7 +483,7 @@ module RubyIndexer
483
483
 
484
484
  foo = T.must(@index["Foo::<Class:Foo>"].first)
485
485
  assert_equal(4, foo.location.start_line)
486
- assert_equal("Some extra comments", foo.comments.join("\n"))
486
+ assert_equal("Some extra comments", foo.comments)
487
487
  end
488
488
 
489
489
  def test_dynamic_singleton_class_blocks
@@ -501,7 +501,7 @@ module RubyIndexer
501
501
  # That pattern cannot be properly analyzed statically and assuming that it's always a regular singleton simplifies
502
502
  # the implementation considerably.
503
503
  assert_equal(3, singleton.location.start_line)
504
- assert_equal("Some extra comments", singleton.comments.join("\n"))
504
+ assert_equal("Some extra comments", singleton.comments)
505
505
  end
506
506
 
507
507
  def test_namespaces_inside_singleton_blocks
@@ -86,16 +86,16 @@ module RubyIndexer
86
86
  A::BAZ = 1
87
87
  RUBY
88
88
 
89
- foo_comment = @index["FOO"].first.comments.join("\n")
89
+ foo_comment = @index["FOO"].first.comments
90
90
  assert_equal("FOO comment", foo_comment)
91
91
 
92
- a_foo_comment = @index["A::FOO"].first.comments.join("\n")
92
+ a_foo_comment = @index["A::FOO"].first.comments
93
93
  assert_equal("A::FOO comment", a_foo_comment)
94
94
 
95
- bar_comment = @index["BAR"].first.comments.join("\n")
95
+ bar_comment = @index["BAR"].first.comments
96
96
  assert_equal("::BAR comment", bar_comment)
97
97
 
98
- a_baz_comment = @index["A::BAZ"].first.comments.join("\n")
98
+ a_baz_comment = @index["A::BAZ"].first.comments
99
99
  assert_equal("A::BAZ comment", a_baz_comment)
100
100
  end
101
101