ruby-lsp 0.23.11 → 0.26.4

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +10 -4
  5. data/exe/ruby-lsp-check +0 -4
  6. data/exe/ruby-lsp-launcher +45 -22
  7. data/exe/ruby-lsp-test-exec +6 -0
  8. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +1 -2
  9. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +3 -6
  10. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +82 -116
  11. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +140 -183
  12. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +10 -14
  13. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +107 -236
  14. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +188 -285
  15. data/lib/ruby_indexer/lib/ruby_indexer/location.rb +4 -27
  16. data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +23 -27
  17. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +25 -57
  18. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +58 -68
  19. data/lib/ruby_indexer/lib/ruby_indexer/uri.rb +17 -19
  20. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +7 -11
  21. data/lib/ruby_indexer/test/class_variables_test.rb +14 -14
  22. data/lib/ruby_indexer/test/classes_and_modules_test.rb +65 -40
  23. data/lib/ruby_indexer/test/configuration_test.rb +49 -9
  24. data/lib/ruby_indexer/test/constant_test.rb +34 -34
  25. data/lib/ruby_indexer/test/enhancements_test.rb +1 -1
  26. data/lib/ruby_indexer/test/index_test.rb +225 -135
  27. data/lib/ruby_indexer/test/instance_variables_test.rb +61 -37
  28. data/lib/ruby_indexer/test/method_test.rb +166 -123
  29. data/lib/ruby_indexer/test/prefix_tree_test.rb +21 -21
  30. data/lib/ruby_indexer/test/rbs_indexer_test.rb +70 -75
  31. data/lib/ruby_indexer/test/reference_finder_test.rb +79 -14
  32. data/lib/ruby_indexer/test/test_case.rb +9 -3
  33. data/lib/ruby_indexer/test/uri_test.rb +15 -2
  34. data/lib/ruby_lsp/addon.rb +88 -86
  35. data/lib/ruby_lsp/base_server.rb +79 -65
  36. data/lib/ruby_lsp/client_capabilities.rb +16 -13
  37. data/lib/ruby_lsp/document.rb +205 -104
  38. data/lib/ruby_lsp/erb_document.rb +45 -47
  39. data/lib/ruby_lsp/global_state.rb +73 -57
  40. data/lib/ruby_lsp/internal.rb +8 -3
  41. data/lib/ruby_lsp/listeners/code_lens.rb +82 -89
  42. data/lib/ruby_lsp/listeners/completion.rb +81 -76
  43. data/lib/ruby_lsp/listeners/definition.rb +44 -58
  44. data/lib/ruby_lsp/listeners/document_highlight.rb +149 -151
  45. data/lib/ruby_lsp/listeners/document_link.rb +94 -82
  46. data/lib/ruby_lsp/listeners/document_symbol.rb +38 -52
  47. data/lib/ruby_lsp/listeners/folding_ranges.rb +40 -43
  48. data/lib/ruby_lsp/listeners/hover.rb +107 -115
  49. data/lib/ruby_lsp/listeners/inlay_hints.rb +8 -13
  50. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +54 -56
  51. data/lib/ruby_lsp/listeners/signature_help.rb +12 -27
  52. data/lib/ruby_lsp/listeners/spec_style.rb +231 -0
  53. data/lib/ruby_lsp/listeners/test_discovery.rb +107 -0
  54. data/lib/ruby_lsp/listeners/test_style.rb +207 -95
  55. data/lib/ruby_lsp/node_context.rb +12 -39
  56. data/lib/ruby_lsp/rbs_document.rb +10 -11
  57. data/lib/ruby_lsp/requests/code_action_resolve.rb +65 -61
  58. data/lib/ruby_lsp/requests/code_actions.rb +14 -26
  59. data/lib/ruby_lsp/requests/code_lens.rb +31 -21
  60. data/lib/ruby_lsp/requests/completion.rb +8 -21
  61. data/lib/ruby_lsp/requests/completion_resolve.rb +6 -6
  62. data/lib/ruby_lsp/requests/definition.rb +8 -20
  63. data/lib/ruby_lsp/requests/diagnostics.rb +8 -11
  64. data/lib/ruby_lsp/requests/discover_tests.rb +20 -7
  65. data/lib/ruby_lsp/requests/document_highlight.rb +6 -16
  66. data/lib/ruby_lsp/requests/document_link.rb +6 -17
  67. data/lib/ruby_lsp/requests/document_symbol.rb +5 -8
  68. data/lib/ruby_lsp/requests/folding_ranges.rb +7 -15
  69. data/lib/ruby_lsp/requests/formatting.rb +6 -9
  70. data/lib/ruby_lsp/requests/go_to_relevant_file.rb +139 -0
  71. data/lib/ruby_lsp/requests/hover.rb +12 -25
  72. data/lib/ruby_lsp/requests/inlay_hints.rb +8 -19
  73. data/lib/ruby_lsp/requests/on_type_formatting.rb +32 -40
  74. data/lib/ruby_lsp/requests/prepare_rename.rb +5 -10
  75. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +5 -15
  76. data/lib/ruby_lsp/requests/range_formatting.rb +5 -6
  77. data/lib/ruby_lsp/requests/references.rb +17 -57
  78. data/lib/ruby_lsp/requests/rename.rb +27 -51
  79. data/lib/ruby_lsp/requests/request.rb +13 -25
  80. data/lib/ruby_lsp/requests/selection_ranges.rb +7 -7
  81. data/lib/ruby_lsp/requests/semantic_highlighting.rb +16 -35
  82. data/lib/ruby_lsp/requests/show_syntax_tree.rb +7 -8
  83. data/lib/ruby_lsp/requests/signature_help.rb +9 -27
  84. data/lib/ruby_lsp/requests/support/annotation.rb +4 -10
  85. data/lib/ruby_lsp/requests/support/common.rb +23 -61
  86. data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
  87. data/lib/ruby_lsp/requests/support/package_url.rb +414 -0
  88. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +27 -35
  89. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +13 -16
  90. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +34 -36
  91. data/lib/ruby_lsp/requests/support/selection_range.rb +1 -3
  92. data/lib/ruby_lsp/requests/support/sorbet.rb +29 -38
  93. data/lib/ruby_lsp/requests/support/source_uri.rb +20 -32
  94. data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +12 -19
  95. data/lib/ruby_lsp/requests/support/test_item.rb +16 -14
  96. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +5 -6
  97. data/lib/ruby_lsp/requests/workspace_symbol.rb +24 -16
  98. data/lib/ruby_lsp/response_builders/collection_response_builder.rb +6 -9
  99. data/lib/ruby_lsp/response_builders/document_symbol.rb +15 -21
  100. data/lib/ruby_lsp/response_builders/hover.rb +12 -18
  101. data/lib/ruby_lsp/response_builders/response_builder.rb +6 -7
  102. data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +62 -91
  103. data/lib/ruby_lsp/response_builders/signature_help.rb +6 -8
  104. data/lib/ruby_lsp/response_builders/test_collection.rb +35 -13
  105. data/lib/ruby_lsp/ruby_document.rb +32 -98
  106. data/lib/ruby_lsp/scope.rb +7 -11
  107. data/lib/ruby_lsp/scripts/compose_bundle.rb +6 -4
  108. data/lib/ruby_lsp/server.rb +305 -198
  109. data/lib/ruby_lsp/setup_bundler.rb +131 -82
  110. data/lib/ruby_lsp/static_docs.rb +12 -7
  111. data/lib/ruby_lsp/store.rb +21 -49
  112. data/lib/ruby_lsp/test_helper.rb +3 -16
  113. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +241 -0
  114. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +145 -0
  115. data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +92 -0
  116. data/lib/ruby_lsp/type_inferrer.rb +13 -14
  117. data/lib/ruby_lsp/utils.rb +138 -93
  118. data/static_docs/break.md +103 -0
  119. metadata +15 -20
  120. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -5,31 +5,22 @@ module RubyLsp
5
5
  module Listeners
6
6
  class SemanticHighlighting
7
7
  include Requests::Support::Common
8
- extend T::Sig
9
-
10
- SPECIAL_RUBY_METHODS = T.let(
11
- [
12
- Module.instance_methods(false),
13
- Kernel.instance_methods(false),
14
- Kernel.methods(false),
15
- Bundler::Dsl.instance_methods(false),
16
- Module.private_instance_methods(false),
17
- ].flatten.map(&:to_s).freeze,
18
- T::Array[String],
19
- )
20
-
21
- sig do
22
- params(
23
- dispatcher: Prism::Dispatcher,
24
- response_builder: ResponseBuilders::SemanticHighlighting,
25
- ).void
26
- end
8
+
9
+ SPECIAL_RUBY_METHODS = [
10
+ Module.instance_methods(false),
11
+ Kernel.instance_methods(false),
12
+ Kernel.methods(false),
13
+ Bundler::Dsl.instance_methods(false),
14
+ Module.private_instance_methods(false),
15
+ ].flatten.map(&:to_s).freeze #: Array[String]
16
+
17
+ #: (Prism::Dispatcher dispatcher, ResponseBuilders::SemanticHighlighting response_builder) -> void
27
18
  def initialize(dispatcher, response_builder)
28
19
  @response_builder = response_builder
29
- @special_methods = T.let(nil, T.nilable(T::Array[String]))
30
- @current_scope = T.let(Scope.new, Scope)
31
- @inside_regex_capture = T.let(false, T::Boolean)
32
- @inside_implicit_node = T.let(false, T::Boolean)
20
+ @special_methods = nil #: Array[String]?
21
+ @current_scope = Scope.new #: Scope
22
+ @inside_regex_capture = false #: bool
23
+ @inside_implicit_node = false #: bool
33
24
 
34
25
  dispatcher.register(
35
26
  self,
@@ -62,7 +53,7 @@ module RubyLsp
62
53
  )
63
54
  end
64
55
 
65
- sig { params(node: Prism::CallNode).void }
56
+ #: (Prism::CallNode node) -> void
66
57
  def on_call_node_enter(node)
67
58
  return if @inside_implicit_node
68
59
 
@@ -76,16 +67,22 @@ module RubyLsp
76
67
  return if special_method?(message)
77
68
 
78
69
  if Requests::Support::Sorbet.annotation?(node)
79
- @response_builder.add_token(T.must(node.message_loc), :type)
70
+ @response_builder.add_token(
71
+ node.message_loc, #: as !nil
72
+ :type,
73
+ )
80
74
  elsif !node.receiver && !node.opening_loc
81
75
  # If the node has a receiver, then the syntax is not ambiguous and semantic highlighting is not necessary to
82
76
  # determine that the token is a method call. The only ambiguous case is method calls with implicit self
83
77
  # receiver and no parenthesis, which may be confused with local variables
84
- @response_builder.add_token(T.must(node.message_loc), :method)
78
+ @response_builder.add_token(
79
+ node.message_loc, #: as !nil
80
+ :method,
81
+ )
85
82
  end
86
83
  end
87
84
 
88
- sig { params(node: Prism::MatchWriteNode).void }
85
+ #: (Prism::MatchWriteNode node) -> void
89
86
  def on_match_write_node_enter(node)
90
87
  call = node.call
91
88
 
@@ -95,86 +92,86 @@ module RubyLsp
95
92
  end
96
93
  end
97
94
 
98
- sig { params(node: Prism::MatchWriteNode).void }
95
+ #: (Prism::MatchWriteNode node) -> void
99
96
  def on_match_write_node_leave(node)
100
97
  @inside_regex_capture = true if node.call.message == "=~"
101
98
  end
102
99
 
103
- sig { params(node: Prism::DefNode).void }
100
+ #: (Prism::DefNode node) -> void
104
101
  def on_def_node_enter(node)
105
102
  @current_scope = Scope.new(@current_scope)
106
103
  end
107
104
 
108
- sig { params(node: Prism::DefNode).void }
105
+ #: (Prism::DefNode node) -> void
109
106
  def on_def_node_leave(node)
110
- @current_scope = T.must(@current_scope.parent)
107
+ @current_scope = @current_scope.parent #: as !nil
111
108
  end
112
109
 
113
- sig { params(node: Prism::BlockNode).void }
110
+ #: (Prism::BlockNode node) -> void
114
111
  def on_block_node_enter(node)
115
112
  @current_scope = Scope.new(@current_scope)
116
113
  end
117
114
 
118
- sig { params(node: Prism::BlockNode).void }
115
+ #: (Prism::BlockNode node) -> void
119
116
  def on_block_node_leave(node)
120
- @current_scope = T.must(@current_scope.parent)
117
+ @current_scope = @current_scope.parent #: as !nil
121
118
  end
122
119
 
123
- sig { params(node: Prism::BlockLocalVariableNode).void }
120
+ #: (Prism::BlockLocalVariableNode node) -> void
124
121
  def on_block_local_variable_node_enter(node)
125
122
  @response_builder.add_token(node.location, :variable)
126
123
  end
127
124
 
128
- sig { params(node: Prism::BlockParameterNode).void }
125
+ #: (Prism::BlockParameterNode node) -> void
129
126
  def on_block_parameter_node_enter(node)
130
127
  name = node.name
131
128
  @current_scope.add(name.to_sym, :parameter) if name
132
129
  end
133
130
 
134
- sig { params(node: Prism::RequiredKeywordParameterNode).void }
131
+ #: (Prism::RequiredKeywordParameterNode node) -> void
135
132
  def on_required_keyword_parameter_node_enter(node)
136
133
  @current_scope.add(node.name, :parameter)
137
134
  end
138
135
 
139
- sig { params(node: Prism::OptionalKeywordParameterNode).void }
136
+ #: (Prism::OptionalKeywordParameterNode node) -> void
140
137
  def on_optional_keyword_parameter_node_enter(node)
141
138
  @current_scope.add(node.name, :parameter)
142
139
  end
143
140
 
144
- sig { params(node: Prism::KeywordRestParameterNode).void }
141
+ #: (Prism::KeywordRestParameterNode node) -> void
145
142
  def on_keyword_rest_parameter_node_enter(node)
146
143
  name = node.name
147
144
  @current_scope.add(name.to_sym, :parameter) if name
148
145
  end
149
146
 
150
- sig { params(node: Prism::OptionalParameterNode).void }
147
+ #: (Prism::OptionalParameterNode node) -> void
151
148
  def on_optional_parameter_node_enter(node)
152
149
  @current_scope.add(node.name, :parameter)
153
150
  end
154
151
 
155
- sig { params(node: Prism::RequiredParameterNode).void }
152
+ #: (Prism::RequiredParameterNode node) -> void
156
153
  def on_required_parameter_node_enter(node)
157
154
  @current_scope.add(node.name, :parameter)
158
155
  end
159
156
 
160
- sig { params(node: Prism::RestParameterNode).void }
157
+ #: (Prism::RestParameterNode node) -> void
161
158
  def on_rest_parameter_node_enter(node)
162
159
  name = node.name
163
160
  @current_scope.add(name.to_sym, :parameter) if name
164
161
  end
165
162
 
166
- sig { params(node: Prism::SelfNode).void }
163
+ #: (Prism::SelfNode node) -> void
167
164
  def on_self_node_enter(node)
168
165
  @response_builder.add_token(node.location, :variable, [:default_library])
169
166
  end
170
167
 
171
- sig { params(node: Prism::LocalVariableWriteNode).void }
168
+ #: (Prism::LocalVariableWriteNode node) -> void
172
169
  def on_local_variable_write_node_enter(node)
173
170
  local = @current_scope.lookup(node.name)
174
171
  @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
175
172
  end
176
173
 
177
- sig { params(node: Prism::LocalVariableReadNode).void }
174
+ #: (Prism::LocalVariableReadNode node) -> void
178
175
  def on_local_variable_read_node_enter(node)
179
176
  return if @inside_implicit_node
180
177
 
@@ -188,25 +185,25 @@ module RubyLsp
188
185
  @response_builder.add_token(node.location, local&.type || :variable)
189
186
  end
190
187
 
191
- sig { params(node: Prism::LocalVariableAndWriteNode).void }
188
+ #: (Prism::LocalVariableAndWriteNode node) -> void
192
189
  def on_local_variable_and_write_node_enter(node)
193
190
  local = @current_scope.lookup(node.name)
194
191
  @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
195
192
  end
196
193
 
197
- sig { params(node: Prism::LocalVariableOperatorWriteNode).void }
194
+ #: (Prism::LocalVariableOperatorWriteNode node) -> void
198
195
  def on_local_variable_operator_write_node_enter(node)
199
196
  local = @current_scope.lookup(node.name)
200
197
  @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
201
198
  end
202
199
 
203
- sig { params(node: Prism::LocalVariableOrWriteNode).void }
200
+ #: (Prism::LocalVariableOrWriteNode node) -> void
204
201
  def on_local_variable_or_write_node_enter(node)
205
202
  local = @current_scope.lookup(node.name)
206
203
  @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
207
204
  end
208
205
 
209
- sig { params(node: Prism::LocalVariableTargetNode).void }
206
+ #: (Prism::LocalVariableTargetNode node) -> void
210
207
  def on_local_variable_target_node_enter(node)
211
208
  # If we're inside a regex capture, Prism will add LocalVariableTarget nodes for each captured variable.
212
209
  # Unfortunately, if the regex contains a backslash, the location will be incorrect and we'll end up highlighting
@@ -218,7 +215,7 @@ module RubyLsp
218
215
  @response_builder.add_token(node.location, local&.type || :variable)
219
216
  end
220
217
 
221
- sig { params(node: Prism::ClassNode).void }
218
+ #: (Prism::ClassNode node) -> void
222
219
  def on_class_node_enter(node)
223
220
  constant_path = node.constant_path
224
221
 
@@ -257,7 +254,7 @@ module RubyLsp
257
254
  end
258
255
  end
259
256
 
260
- sig { params(node: Prism::ModuleNode).void }
257
+ #: (Prism::ModuleNode node) -> void
261
258
  def on_module_node_enter(node)
262
259
  constant_path = node.constant_path
263
260
 
@@ -278,12 +275,12 @@ module RubyLsp
278
275
  end
279
276
  end
280
277
 
281
- sig { params(node: Prism::ImplicitNode).void }
278
+ #: (Prism::ImplicitNode node) -> void
282
279
  def on_implicit_node_enter(node)
283
280
  @inside_implicit_node = true
284
281
  end
285
282
 
286
- sig { params(node: Prism::ImplicitNode).void }
283
+ #: (Prism::ImplicitNode node) -> void
287
284
  def on_implicit_node_leave(node)
288
285
  @inside_implicit_node = false
289
286
  end
@@ -292,12 +289,12 @@ module RubyLsp
292
289
 
293
290
  # Textmate provides highlighting for a subset of these special Ruby-specific methods. We want to utilize that
294
291
  # highlighting, so we avoid making a semantic token for it.
295
- sig { params(method_name: String).returns(T::Boolean) }
292
+ #: (String method_name) -> bool
296
293
  def special_method?(method_name)
297
294
  SPECIAL_RUBY_METHODS.include?(method_name)
298
295
  end
299
296
 
300
- sig { params(node: Prism::CallNode).void }
297
+ #: (Prism::CallNode node) -> void
301
298
  def process_regexp_locals(node)
302
299
  receiver = node.receiver
303
300
 
@@ -310,7 +307,8 @@ module RubyLsp
310
307
  # For each capture name we find in the regexp, look for a local in the current_scope
311
308
  Regexp.new(content, Regexp::FIXEDENCODING).names.each do |name|
312
309
  # The +3 is to compensate for the "(?<" part of the capture name
313
- capture_name_offset = T.must(content.index("(?<#{name}>")) + 3
310
+ capture_name_index = content.index("(?<#{name}>") #: as !nil
311
+ capture_name_offset = capture_name_index + 3
314
312
  local_var_loc = loc.copy(start_offset: loc.start_offset + capture_name_offset, length: name.length)
315
313
 
316
314
  local = @current_scope.lookup(name)
@@ -4,31 +4,22 @@
4
4
  module RubyLsp
5
5
  module Listeners
6
6
  class SignatureHelp
7
- extend T::Sig
8
7
  include Requests::Support::Common
9
8
 
10
- sig do
11
- params(
12
- response_builder: ResponseBuilders::SignatureHelp,
13
- global_state: GlobalState,
14
- node_context: NodeContext,
15
- dispatcher: Prism::Dispatcher,
16
- sorbet_level: RubyDocument::SorbetLevel,
17
- ).void
18
- end
9
+ #: (ResponseBuilders::SignatureHelp response_builder, GlobalState global_state, NodeContext node_context, Prism::Dispatcher dispatcher, SorbetLevel sorbet_level) -> void
19
10
  def initialize(response_builder, global_state, node_context, dispatcher, sorbet_level)
20
11
  @sorbet_level = sorbet_level
21
12
  @response_builder = response_builder
22
13
  @global_state = global_state
23
- @index = T.let(global_state.index, RubyIndexer::Index)
24
- @type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
14
+ @index = global_state.index #: RubyIndexer::Index
15
+ @type_inferrer = global_state.type_inferrer #: TypeInferrer
25
16
  @node_context = node_context
26
17
  dispatcher.register(self, :on_call_node_enter)
27
18
  end
28
19
 
29
- sig { params(node: Prism::CallNode).void }
20
+ #: (Prism::CallNode node) -> void
30
21
  def on_call_node_enter(node)
31
- return if sorbet_level_true_or_higher?(@sorbet_level)
22
+ return if @sorbet_level.true_or_higher?
32
23
 
33
24
  message = node.message
34
25
  return unless message
@@ -67,9 +58,7 @@ module RubyLsp
67
58
 
68
59
  private
69
60
 
70
- sig do
71
- params(node: Prism::CallNode, signatures: T::Array[RubyIndexer::Entry::Signature]).returns([Integer, Integer])
72
- end
61
+ #: (Prism::CallNode node, Array[RubyIndexer::Entry::Signature] signatures) -> [Integer, Integer]
73
62
  def determine_active_signature_and_parameter(node, signatures)
74
63
  arguments_node = node.arguments
75
64
  arguments = arguments_node&.arguments || []
@@ -80,7 +69,11 @@ module RubyLsp
80
69
  signature.matches?(arguments)
81
70
  end || 0
82
71
 
83
- parameter_length = [T.must(signatures[active_sig_index]).parameters.length - 1, 0].max
72
+ parameter_length = [
73
+ signatures[active_sig_index] #: as !nil
74
+ .parameters.length - 1,
75
+ 0,
76
+ ].max
84
77
  active_parameter = (arguments.length - 1).clamp(0, parameter_length)
85
78
 
86
79
  # If there are arguments, then we need to check if there's a trailing comma after the end of the last argument
@@ -93,15 +86,7 @@ module RubyLsp
93
86
  [active_sig_index, active_parameter]
94
87
  end
95
88
 
96
- sig do
97
- params(
98
- signatures: T::Array[RubyIndexer::Entry::Signature],
99
- method_name: String,
100
- methods: T::Array[RubyIndexer::Entry],
101
- title: String,
102
- extra_links: T.nilable(String),
103
- ).returns(T::Array[Interface::SignatureInformation])
104
- end
89
+ #: (Array[RubyIndexer::Entry::Signature] signatures, String method_name, Array[RubyIndexer::Entry] methods, String title, String? extra_links) -> Array[Interface::SignatureInformation]
105
90
  def generate_signatures(signatures, method_name, methods, title, extra_links)
106
91
  signatures.map do |signature|
107
92
  Interface::SignatureInformation.new(
@@ -0,0 +1,231 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Listeners
6
+ class SpecStyle < TestDiscovery
7
+ class Group
8
+ #: String
9
+ attr_reader :id
10
+
11
+ #: (String) -> void
12
+ def initialize(id)
13
+ @id = id
14
+ end
15
+ end
16
+
17
+ class ClassGroup < Group; end
18
+ class DescribeGroup < Group; end
19
+
20
+ #: (ResponseBuilders::TestCollection, GlobalState, Prism::Dispatcher, URI::Generic) -> void
21
+ def initialize(response_builder, global_state, dispatcher, uri)
22
+ super(response_builder, global_state, uri)
23
+
24
+ @spec_group_id_stack = [] #: Array[Group?]
25
+
26
+ register_events(
27
+ dispatcher,
28
+ :on_class_node_enter,
29
+ :on_call_node_enter,
30
+ :on_call_node_leave,
31
+ )
32
+ end
33
+
34
+ #: (Prism::ClassNode) -> void
35
+ def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
36
+ with_test_ancestor_tracking(node) do |name, ancestors|
37
+ @spec_group_id_stack << (ancestors.include?("Minitest::Spec") ? ClassGroup.new(name) : nil)
38
+ end
39
+ end
40
+
41
+ #: (Prism::ClassNode) -> void
42
+ def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
43
+ @spec_group_id_stack.pop
44
+ super
45
+ end
46
+
47
+ #: (Prism::ModuleNode) -> void
48
+ def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
49
+ @spec_group_id_stack << nil
50
+ super
51
+ end
52
+
53
+ #: (Prism::ModuleNode) -> void
54
+ def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
55
+ @spec_group_id_stack.pop
56
+ super
57
+ end
58
+
59
+ #: (Prism::CallNode) -> void
60
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
61
+ case node.name
62
+ when :describe
63
+ handle_describe(node)
64
+ when :it, :specify
65
+ handle_example(node) if in_spec_context?
66
+ end
67
+ end
68
+
69
+ #: (Prism::CallNode) -> void
70
+ def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
71
+ return unless node.name == :describe && !node.receiver
72
+
73
+ current_group = @spec_group_id_stack.last
74
+ return unless current_group.is_a?(DescribeGroup)
75
+
76
+ description = extract_description(node)
77
+ return unless description && current_group.id.end_with?(description)
78
+
79
+ @spec_group_id_stack.pop
80
+ end
81
+
82
+ private
83
+
84
+ #: (Prism::CallNode) -> void
85
+ def handle_describe(node)
86
+ # Describes will include the nesting of all classes and all outer describes as part of its ID, unlike classes
87
+ # that ignore describes
88
+ return if node.block.nil?
89
+
90
+ description = extract_description(node)
91
+ return unless description
92
+
93
+ parent = latest_group
94
+ return unless parent
95
+
96
+ id = case parent
97
+ when Requests::Support::TestItem
98
+ "#{parent.id}::#{description}"
99
+ else
100
+ description
101
+ end
102
+
103
+ test_item = Requests::Support::TestItem.new(
104
+ id,
105
+ description,
106
+ @uri,
107
+ range_from_node(node),
108
+ framework: :minitest,
109
+ )
110
+
111
+ parent.add(test_item)
112
+ @response_builder.add_code_lens(test_item)
113
+ @spec_group_id_stack << DescribeGroup.new(id)
114
+ end
115
+
116
+ #: (Prism::CallNode) -> void
117
+ def handle_example(node)
118
+ description = extract_it_description(node)
119
+ line = node.location.start_line - 1
120
+ parent = latest_group
121
+ return unless parent.is_a?(Requests::Support::TestItem)
122
+
123
+ id = "#{parent.id}##{format("test_%04d_%s", line, description)}"
124
+
125
+ test_item = Requests::Support::TestItem.new(
126
+ id,
127
+ description,
128
+ @uri,
129
+ range_from_node(node),
130
+ framework: :minitest,
131
+ )
132
+
133
+ parent.add(test_item)
134
+ @response_builder.add_code_lens(test_item)
135
+ end
136
+
137
+ #: (Prism::CallNode) -> String?
138
+ def extract_description(node)
139
+ arguments = node.arguments&.arguments
140
+ return unless arguments
141
+
142
+ parts = arguments.map { |arg| extract_argument_content(arg) }
143
+ return if parts.empty?
144
+
145
+ parts.join("::")
146
+ end
147
+
148
+ #: (Prism::CallNode) -> String
149
+ def extract_it_description(node)
150
+ # Minitest formats the descriptions into test method names by using the count of examples with the description
151
+ # We are not guaranteed to discover examples in the exact order using static analysis, so we use the line number
152
+ # instead. Note that anonymous examples mixed with meta-programming will not be handled correctly
153
+ first_argument = node.arguments&.arguments&.first
154
+ return "anonymous" unless first_argument
155
+
156
+ extract_argument_content(first_argument) || "anonymous"
157
+ end
158
+
159
+ #: (Prism::Node) -> String?
160
+ def extract_argument_content(arg)
161
+ case arg
162
+ when Prism::StringNode
163
+ arg.content
164
+ when Prism::SymbolNode
165
+ arg.value
166
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
167
+ constant_name(arg)
168
+ else
169
+ arg.slice
170
+ end
171
+ end
172
+
173
+ #: -> (Requests::Support::TestItem | ResponseBuilders::TestCollection)?
174
+ def latest_group
175
+ # If we haven't found anything yet, then return the response builder
176
+ return @response_builder if @spec_group_id_stack.compact.empty?
177
+ # If we found something that isn't a group last, then we're inside a random module or class, but not a spec
178
+ # group
179
+ return unless @spec_group_id_stack.last
180
+
181
+ # Specs using at least one class as a group require special handling
182
+ closest_class_index = @spec_group_id_stack.rindex { |i| i.is_a?(ClassGroup) }
183
+
184
+ if closest_class_index
185
+ first_class_index = @spec_group_id_stack.index { |i| i.is_a?(ClassGroup) } #: as !nil
186
+ first_class = @spec_group_id_stack[first_class_index] #: as !nil
187
+ item = @response_builder[first_class.id] #: as !nil
188
+
189
+ # Descend into child items from the beginning all the way to the latest class group, ignoring describes
190
+ @spec_group_id_stack[first_class_index + 1..closest_class_index] #: as !nil
191
+ .each do |group|
192
+ next unless group.is_a?(ClassGroup)
193
+
194
+ item = item[group.id] #: as !nil
195
+ end
196
+
197
+ # From the class forward, we must take describes into account
198
+ @spec_group_id_stack[closest_class_index + 1..] #: as !nil
199
+ .each do |group|
200
+ next unless group
201
+
202
+ item = item[group.id] #: as !nil
203
+ end
204
+
205
+ return item
206
+ end
207
+
208
+ # Specs only using describes
209
+ first_group_index = @spec_group_id_stack.index { |i| i.is_a?(DescribeGroup) }
210
+ return unless first_group_index
211
+
212
+ first_group = @spec_group_id_stack[first_group_index] #: as !nil
213
+ item = @response_builder[first_group.id] #: as !nil
214
+
215
+ @spec_group_id_stack[first_group_index + 1..] #: as !nil
216
+ .each do |group|
217
+ next unless group.is_a?(DescribeGroup)
218
+
219
+ item = item[group.id] #: as !nil
220
+ end
221
+
222
+ item
223
+ end
224
+
225
+ #: -> bool
226
+ def in_spec_context?
227
+ @nesting.empty? || @spec_group_id_stack.any? { |id| id }
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,107 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Listeners
6
+ # @abstract
7
+ class TestDiscovery
8
+ include Requests::Support::Common
9
+
10
+ DYNAMIC_REFERENCE_MARKER = "<dynamic_reference>"
11
+
12
+ #: (ResponseBuilders::TestCollection response_builder, GlobalState global_state, URI::Generic uri) -> void
13
+ def initialize(response_builder, global_state, uri)
14
+ @response_builder = response_builder
15
+ @uri = uri
16
+ @index = global_state.index #: RubyIndexer::Index
17
+ @visibility_stack = [:public] #: Array[Symbol]
18
+ @nesting = [] #: Array[String]
19
+ end
20
+
21
+ #: (Prism::ModuleNode node) -> void
22
+ def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
23
+ @visibility_stack << :public
24
+
25
+ name = constant_name(node.constant_path)
26
+ name ||= name_with_dynamic_reference(node.constant_path)
27
+
28
+ @nesting << name
29
+ end
30
+
31
+ #: (Prism::ModuleNode node) -> void
32
+ def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
33
+ @visibility_stack.pop
34
+ @nesting.pop
35
+ end
36
+
37
+ #: (Prism::ClassNode node) -> void
38
+ def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
39
+ @visibility_stack.pop
40
+ @nesting.pop
41
+ end
42
+
43
+ private
44
+
45
+ #: (Prism::Dispatcher, *Symbol) -> void
46
+ def register_events(dispatcher, *events)
47
+ unique_events = events.dup.push(
48
+ :on_class_node_leave,
49
+ :on_module_node_enter,
50
+ :on_module_node_leave,
51
+ )
52
+
53
+ unique_events.uniq!
54
+ dispatcher.register(self, *unique_events)
55
+ end
56
+
57
+ #: (String? name) -> String
58
+ def calc_fully_qualified_name(name)
59
+ RubyIndexer::Index.actual_nesting(@nesting, name).join("::")
60
+ end
61
+
62
+ #: (Prism::ClassNode node, String fully_qualified_name) -> Array[String]
63
+ def calc_attached_ancestors(node, fully_qualified_name)
64
+ superclass = node.superclass
65
+
66
+ begin
67
+ ancestors = @index.linearized_ancestors_of(fully_qualified_name)
68
+ # If the project has no bundle and a test class inherits from `Minitest::Test`, the linearized ancestors will
69
+ # not include the parent class because we never indexed it in the first place. Here we add the superclass
70
+ # directly, so that we can support running tests in projects without a bundle
71
+ return ancestors if ancestors.length > 1
72
+
73
+ # If all we found is the class itself, then manually include the parent class
74
+ if ancestors.first == fully_qualified_name && superclass
75
+ return [*ancestors, superclass.slice]
76
+ end
77
+
78
+ ancestors
79
+ rescue RubyIndexer::Index::NonExistingNamespaceError
80
+ # When there are dynamic parts in the constant path, we will not have indexed the namespace. We can still
81
+ # provide test functionality if the class inherits directly from Test::Unit::TestCase or Minitest::Test
82
+ [superclass&.slice].compact
83
+ end
84
+ end
85
+
86
+ #: (Prism::ConstantPathNode | Prism::ConstantReadNode | Prism::ConstantPathTargetNode | Prism::CallNode | Prism::MissingNode node) -> String
87
+ def name_with_dynamic_reference(node)
88
+ slice = node.slice
89
+ slice.gsub(/((?<=::)|^)[a-z]\w*/, DYNAMIC_REFERENCE_MARKER)
90
+ end
91
+
92
+ #: (Prism::ClassNode node) { (String name, Array[String] ancestors) -> void } -> void
93
+ def with_test_ancestor_tracking(node, &block)
94
+ @visibility_stack << :public
95
+ name = constant_name(node.constant_path)
96
+ name ||= name_with_dynamic_reference(node.constant_path)
97
+
98
+ fully_qualified_name = calc_fully_qualified_name(name)
99
+ attached_ancestors = calc_attached_ancestors(node, fully_qualified_name)
100
+
101
+ block.call(fully_qualified_name, attached_ancestors)
102
+
103
+ @nesting << name
104
+ end
105
+ end
106
+ end
107
+ end