ruby-lsp 0.17.17 → 0.18.0

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