ruby-lsp-rails 0.3.31 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,18 +15,9 @@ module RubyLsp
15
15
  # # ^ hovering here will show information about the User model
16
16
  # ```
17
17
  class Hover
18
- extend T::Sig
19
18
  include Requests::Support::Common
20
19
 
21
- sig do
22
- params(
23
- client: RunnerClient,
24
- response_builder: ResponseBuilders::Hover,
25
- node_context: NodeContext,
26
- global_state: GlobalState,
27
- dispatcher: Prism::Dispatcher,
28
- ).void
29
- end
20
+ #: (RunnerClient client, ResponseBuilders::Hover response_builder, NodeContext node_context, GlobalState global_state, Prism::Dispatcher dispatcher) -> void
30
21
  def initialize(client, response_builder, node_context, global_state, dispatcher)
31
22
  @client = client
32
23
  @response_builder = response_builder
@@ -35,26 +26,26 @@ module RubyLsp
35
26
  dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter)
36
27
  end
37
28
 
38
- sig { params(node: Prism::ConstantPathNode).void }
29
+ #: (Prism::ConstantPathNode node) -> void
39
30
  def on_constant_path_node_enter(node)
40
31
  entries = @index.resolve(node.slice, @nesting)
41
32
  return unless entries
42
33
 
43
- name = T.must(entries.first).name
34
+ name = entries.first.name
44
35
  generate_column_content(name)
45
36
  end
46
37
 
47
- sig { params(node: Prism::ConstantReadNode).void }
38
+ #: (Prism::ConstantReadNode node) -> void
48
39
  def on_constant_read_node_enter(node)
49
40
  entries = @index.resolve(node.name.to_s, @nesting)
50
41
  return unless entries
51
42
 
52
- generate_column_content(T.must(entries.first).name)
43
+ generate_column_content(entries.first.name)
53
44
  end
54
45
 
55
46
  private
56
47
 
57
- sig { params(name: String).void }
48
+ #: (String name) -> void
58
49
  def generate_column_content(name)
59
50
  model = @client.model(name)
60
51
  return if model.nil?
@@ -66,20 +57,41 @@ module RubyLsp
66
57
  category: :documentation,
67
58
  ) if schema_file
68
59
 
69
- @response_builder.push(
70
- model[:columns].map do |name, type, default_value, nullable|
71
- primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
72
- suffixes = []
73
- suffixes << "default: #{format_default(default_value, type)}" if default_value
74
- suffixes << "not null" unless nullable || primary_key_suffix
75
- suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
76
- "**#{name}**: #{type}#{primary_key_suffix}#{suffix_string}\n"
77
- end.join("\n"),
78
- category: :documentation,
79
- )
60
+ if model[:columns].any?
61
+ @response_builder.push(
62
+ "### Columns",
63
+ category: :documentation,
64
+ )
65
+ @response_builder.push(
66
+ model[:columns].map do |name, type, default_value, nullable|
67
+ primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
68
+ foreign_key_suffix = " (FK)" if model[:foreign_keys].include?(name)
69
+ suffixes = []
70
+ suffixes << "default: #{format_default(default_value, type)}" if default_value
71
+ suffixes << "not null" unless nullable || primary_key_suffix
72
+ suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
73
+ "- **#{name}**: #{type}#{primary_key_suffix}#{foreign_key_suffix}#{suffix_string}\n"
74
+ end.join("\n"),
75
+ category: :documentation,
76
+ )
77
+ end
78
+
79
+ if model[:indexes].any?
80
+ @response_builder.push(
81
+ "### Indexes",
82
+ category: :documentation,
83
+ )
84
+ @response_builder.push(
85
+ model[:indexes].map do |index|
86
+ uniqueness = index[:unique] ? " (unique)" : ""
87
+ "- **#{index[:name]}** (#{index[:columns].join(",")})#{uniqueness}"
88
+ end.join("\n"),
89
+ category: :documentation,
90
+ )
91
+ end
80
92
  end
81
93
 
82
- sig { params(default_value: String, type: String).returns(String) }
94
+ #: (String default_value, String type) -> String
83
95
  def format_default(default_value, type)
84
96
  case type
85
97
  when "boolean"
@@ -4,13 +4,8 @@
4
4
  module RubyLsp
5
5
  module Rails
6
6
  class IndexingEnhancement < RubyIndexer::Enhancement
7
- extend T::Sig
8
-
9
- sig do
10
- override.params(
11
- call_node: Prism::CallNode,
12
- ).void
13
- end
7
+ # @override
8
+ #: (Prism::CallNode call_node) -> void
14
9
  def on_call_node_enter(call_node)
15
10
  owner = @listener.current_owner
16
11
  return unless owner
@@ -26,11 +21,8 @@ module RubyLsp
26
21
  end
27
22
  end
28
23
 
29
- sig do
30
- override.params(
31
- call_node: Prism::CallNode,
32
- ).void
33
- end
24
+ # @override
25
+ #: (Prism::CallNode call_node) -> void
34
26
  def on_call_node_leave(call_node)
35
27
  if call_node.name == :class_methods && call_node.block
36
28
  @listener.pop_namespace_stack
@@ -39,12 +31,7 @@ module RubyLsp
39
31
 
40
32
  private
41
33
 
42
- sig do
43
- params(
44
- owner: RubyIndexer::Entry::Namespace,
45
- call_node: Prism::CallNode,
46
- ).void
47
- end
34
+ #: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
48
35
  def handle_association(owner, call_node)
49
36
  arguments = call_node.arguments&.arguments
50
37
  return unless arguments
@@ -73,7 +60,7 @@ module RubyLsp
73
60
  @listener.add_method("#{name}=", loc, writer_signatures)
74
61
  end
75
62
 
76
- sig { params(owner: RubyIndexer::Entry::Namespace, call_node: Prism::CallNode).void }
63
+ #: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
77
64
  def handle_concern_extend(owner, call_node)
78
65
  arguments = call_node.arguments&.arguments
79
66
  return unless arguments
@@ -98,7 +85,7 @@ module RubyLsp
98
85
  end
99
86
  end
100
87
 
101
- sig { params(owner: RubyIndexer::Entry::Namespace, call_node: Prism::CallNode).void }
88
+ #: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
102
89
  def handle_class_methods(owner, call_node)
103
90
  return unless call_node.block
104
91
 
@@ -0,0 +1,150 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ class RailsTestStyle < Listeners::TestDiscovery
7
+ BASE_COMMAND = "#{RbConfig.ruby} bin/rails test" #: String
8
+
9
+ class << self
10
+ #: (Array[Hash[Symbol, untyped]]) -> Array[String]
11
+ def resolve_test_commands(items)
12
+ commands = []
13
+ queue = items.dup
14
+
15
+ full_files = []
16
+
17
+ until queue.empty?
18
+ item = T.must(queue.shift)
19
+ tags = Set.new(item[:tags])
20
+ next unless tags.include?("framework:rails")
21
+
22
+ children = item[:children]
23
+ uri = URI(item[:uri])
24
+ path = uri.full_path
25
+ next unless path
26
+
27
+ if tags.include?("test_dir")
28
+ if children.empty?
29
+ full_files.concat(Dir.glob(
30
+ "#{path}/**/{*_test,test_*}.rb",
31
+ File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
32
+ ))
33
+ end
34
+ elsif tags.include?("test_file")
35
+ full_files << path if children.empty?
36
+ elsif tags.include?("test_group")
37
+ commands << "#{BASE_COMMAND} #{path} --name \"/#{Shellwords.escape(item[:id])}(#|::)/\""
38
+ else
39
+ full_files << "#{path}:#{item.dig(:range, :start, :line) + 1}"
40
+ end
41
+
42
+ queue.concat(children)
43
+ end
44
+
45
+ unless full_files.empty?
46
+ commands << "#{BASE_COMMAND} #{full_files.join(" ")}"
47
+ end
48
+
49
+ commands
50
+ end
51
+ end
52
+
53
+ #: (RunnerClient client, ResponseBuilders::TestCollection response_builder, GlobalState global_state, Prism::Dispatcher dispatcher, URI::Generic uri) -> void
54
+ def initialize(client, response_builder, global_state, dispatcher, uri)
55
+ super(response_builder, global_state, dispatcher, uri)
56
+
57
+ dispatcher.register(
58
+ self,
59
+ :on_class_node_enter,
60
+ :on_call_node_enter,
61
+ :on_def_node_enter,
62
+ )
63
+ end
64
+
65
+ #: (Prism::ClassNode node) -> void
66
+ def on_class_node_enter(node)
67
+ with_test_ancestor_tracking(node) do |name, ancestors|
68
+ if declarative_minitest?(ancestors, name)
69
+ test_item = Requests::Support::TestItem.new(
70
+ name,
71
+ name,
72
+ @uri,
73
+ range_from_node(node),
74
+ framework: :rails,
75
+ )
76
+
77
+ @response_builder.add(test_item)
78
+ end
79
+ end
80
+ end
81
+
82
+ #: (Prism::CallNode node) -> void
83
+ def on_call_node_enter(node)
84
+ return unless node.name == :test
85
+ return unless node.block
86
+
87
+ arguments = node.arguments&.arguments
88
+ first_arg = arguments&.first
89
+ return unless first_arg.is_a?(Prism::StringNode)
90
+
91
+ test_name = first_arg.content
92
+ test_name = "<empty test name>" if test_name.empty?
93
+
94
+ # Rails' `test "foo bar"` helper defines a method `def test_foo_bar`. We normalize test names
95
+ # the same way (spaces to underscores, prefix with `test_`) to match the actual method names
96
+ # Rails uses at runtime, ensuring proper test discovery and execution.
97
+ rails_normalized_name = "test_#{test_name.gsub(/\s+/, "_")}"
98
+
99
+ add_test_item(node, rails_normalized_name)
100
+ end
101
+
102
+ #: (Prism::DefNode node) -> void
103
+ def on_def_node_enter(node)
104
+ return if @visibility_stack.last != :public
105
+
106
+ name = node.name.to_s
107
+ return unless name.start_with?("test_")
108
+
109
+ add_test_item(node, name)
110
+ end
111
+
112
+ private
113
+
114
+ #: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
115
+ def declarative_minitest?(attached_ancestors, fully_qualified_name)
116
+ # The declarative test style is present as long as the class extends
117
+ # ActiveSupport::Testing::Declarative
118
+ name_parts = fully_qualified_name.split("::")
119
+ singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>"
120
+ @index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
121
+ rescue RubyIndexer::Index::NonExistingNamespaceError
122
+ false
123
+ end
124
+
125
+ #: (Prism::Node node, String test_name) -> void
126
+ def add_test_item(node, test_name)
127
+ test_item = group_test_item
128
+ return unless test_item
129
+
130
+ test_item.add(Requests::Support::TestItem.new(
131
+ "#{test_item.id}##{test_name}",
132
+ test_name,
133
+ @uri,
134
+ range_from_node(node),
135
+ framework: :rails,
136
+ ))
137
+ end
138
+
139
+ #: -> Requests::Support::TestItem?
140
+ def group_test_item
141
+ current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::")
142
+
143
+ # If we're finding a test method, but for the wrong framework, then the group test item will not have been
144
+ # previously pushed and thus we return early and avoid adding items for a framework this listener is not
145
+ # interested in
146
+ @response_builder[current_group_name]
147
+ end
148
+ end
149
+ end
150
+ end
@@ -8,12 +8,10 @@ module RubyLsp
8
8
  module Rails
9
9
  class RunnerClient
10
10
  class << self
11
- extend T::Sig
12
-
13
- sig { params(outgoing_queue: Thread::Queue).returns(RunnerClient) }
14
- def create_client(outgoing_queue)
11
+ #: (Thread::Queue outgoing_queue, RubyLsp::GlobalState global_state) -> RunnerClient
12
+ def create_client(outgoing_queue, global_state)
15
13
  if File.exist?("bin/rails")
16
- new(outgoing_queue)
14
+ new(outgoing_queue, global_state)
17
15
  else
18
16
  unless outgoing_queue.closed?
19
17
  outgoing_queue << RubyLsp::Notification.window_log_message(
@@ -46,13 +44,11 @@ module RubyLsp
46
44
  class MessageError < StandardError; end
47
45
  class EmptyMessageError < MessageError; end
48
46
 
49
- extend T::Sig
50
-
51
- sig { returns(String) }
47
+ #: String
52
48
  attr_reader :rails_root
53
49
 
54
- sig { params(outgoing_queue: Thread::Queue).void }
55
- def initialize(outgoing_queue)
50
+ #: (Thread::Queue outgoing_queue, RubyLsp::GlobalState global_state) -> void
51
+ def initialize(outgoing_queue, global_state)
56
52
  @outgoing_queue = T.let(outgoing_queue, Thread::Queue)
57
53
  @mutex = T.let(Mutex.new, Mutex)
58
54
  # Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
@@ -71,7 +67,15 @@ module RubyLsp
71
67
  log_message("Ruby LSP Rails booting server")
72
68
 
73
69
  stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
74
- Open3.popen3("bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start")
70
+ Open3.popen3(
71
+ "bundle",
72
+ "exec",
73
+ "rails",
74
+ "runner",
75
+ "#{__dir__}/server.rb",
76
+ "start",
77
+ server_relevant_capabilities(global_state),
78
+ )
75
79
  end
76
80
 
77
81
  @stdin = T.let(stdin, IO)
@@ -100,10 +104,16 @@ module RubyLsp
100
104
  end
101
105
  end
102
106
 
103
- @logger_thread = T.let(
107
+ # Responsible for transmitting notifications coming from the server to the outgoing queue, so that we can do
108
+ # things such as showing progress notifications initiated by the server
109
+ @notifier_thread = T.let(
104
110
  Thread.new do
105
- while (content = @stderr.gets("\n"))
106
- log_message(content, type: RubyLsp::Constant::MessageType::LOG)
111
+ until @stderr.closed?
112
+ notification = read_notification
113
+
114
+ unless @outgoing_queue.closed? || !notification
115
+ @outgoing_queue << notification
116
+ end
107
117
  end
108
118
  rescue IOError
109
119
  # The server was shutdown and stderr is already closed
@@ -114,7 +124,7 @@ module RubyLsp
114
124
  raise InitializationError, @stderr.read
115
125
  end
116
126
 
117
- sig { params(server_addon_path: String).void }
127
+ #: (String server_addon_path) -> void
118
128
  def register_server_addon(server_addon_path)
119
129
  send_notification("server_addon/register", server_addon_path: server_addon_path)
120
130
  rescue MessageError
@@ -125,7 +135,7 @@ module RubyLsp
125
135
  nil
126
136
  end
127
137
 
128
- sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
138
+ #: (String name) -> Hash[Symbol, untyped]?
129
139
  def model(name)
130
140
  make_request("model", name: name)
131
141
  rescue MessageError
@@ -136,12 +146,7 @@ module RubyLsp
136
146
  nil
137
147
  end
138
148
 
139
- sig do
140
- params(
141
- model_name: String,
142
- association_name: String,
143
- ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
144
- end
149
+ #: (model_name: String, association_name: String) -> Hash[Symbol, untyped]?
145
150
  def association_target_location(model_name:, association_name:)
146
151
  make_request(
147
152
  "association_target_location",
@@ -156,7 +161,7 @@ module RubyLsp
156
161
  nil
157
162
  end
158
163
 
159
- sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
164
+ #: (String name) -> Hash[Symbol, untyped]?
160
165
  def route_location(name)
161
166
  make_request("route_location", name: name)
162
167
  rescue MessageError
@@ -167,7 +172,7 @@ module RubyLsp
167
172
  nil
168
173
  end
169
174
 
170
- sig { params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
175
+ #: (controller: String, action: String) -> Hash[Symbol, untyped]?
171
176
  def route(controller:, action:)
172
177
  make_request("route_info", controller: controller, action: action)
173
178
  rescue MessageError
@@ -179,7 +184,7 @@ module RubyLsp
179
184
  end
180
185
 
181
186
  # Delegates a notification to a server add-on
182
- sig { params(server_addon_name: String, request_name: String, params: T.untyped).void }
187
+ #: (server_addon_name: String, request_name: String, **untyped params) -> void
183
188
  def delegate_notification(server_addon_name:, request_name:, **params)
184
189
  send_notification(
185
190
  "server_addon/delegate",
@@ -189,7 +194,7 @@ module RubyLsp
189
194
  )
190
195
  end
191
196
 
192
- sig { returns(T.nilable(String)) }
197
+ #: -> String?
193
198
  def pending_migrations_message
194
199
  response = make_request("pending_migrations_message")
195
200
  response[:pending_migrations_message] if response
@@ -201,7 +206,7 @@ module RubyLsp
201
206
  nil
202
207
  end
203
208
 
204
- sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
209
+ #: -> Hash[Symbol, untyped]?
205
210
  def run_migrations
206
211
  make_request("run_migrations")
207
212
  rescue MessageError
@@ -213,13 +218,7 @@ module RubyLsp
213
218
  end
214
219
 
215
220
  # Delegates a request to a server add-on
216
- sig do
217
- params(
218
- server_addon_name: String,
219
- request_name: String,
220
- params: T.untyped,
221
- ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
222
- end
221
+ #: (server_addon_name: String, request_name: String, **untyped params) -> Hash[Symbol, untyped]?
223
222
  def delegate_request(server_addon_name:, request_name:, **params)
224
223
  make_request(
225
224
  "server_addon/delegate",
@@ -231,7 +230,7 @@ module RubyLsp
231
230
  nil
232
231
  end
233
232
 
234
- sig { void }
233
+ #: -> void
235
234
  def trigger_reload
236
235
  log_message("Reloading Rails application")
237
236
  send_notification("reload")
@@ -243,7 +242,7 @@ module RubyLsp
243
242
  nil
244
243
  end
245
244
 
246
- sig { void }
245
+ #: -> void
247
246
  def shutdown
248
247
  log_message("Ruby LSP Rails shutting down server")
249
248
  send_message("shutdown")
@@ -254,29 +253,30 @@ module RubyLsp
254
253
  force_kill
255
254
  end
256
255
 
257
- sig { returns(T::Boolean) }
256
+ #: -> bool
258
257
  def stopped?
259
258
  [@stdin, @stdout, @stderr].all?(&:closed?) && !@wait_thread.alive?
260
259
  end
261
260
 
262
- sig do
263
- params(
264
- request: String,
265
- params: T.untyped,
266
- ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
261
+ #: -> bool
262
+ def connected?
263
+ true
267
264
  end
265
+
266
+ private
267
+
268
+ #: (String request, **untyped params) -> Hash[Symbol, untyped]?
268
269
  def make_request(request, **params)
269
270
  send_message(request, **params)
270
271
  read_response
271
272
  end
272
273
 
273
274
  # Notifications are like messages, but one-way, with no response sent back.
274
- sig { params(request: String, params: T.untyped).void }
275
+ #: (String request, **untyped params) -> void
275
276
  def send_notification(request, **params) = send_message(request, **params)
276
277
 
277
- private
278
-
279
- sig { overridable.params(request: String, params: T.untyped).void }
278
+ # @overridable
279
+ #: (String request, **untyped params) -> void
280
280
  def send_message(request, **params)
281
281
  message = { method: request }
282
282
  message[:params] = params
@@ -289,7 +289,8 @@ module RubyLsp
289
289
  # The server connection died
290
290
  end
291
291
 
292
- sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
292
+ # @overridable
293
+ #: -> Hash[Symbol, untyped]?
293
294
  def read_response
294
295
  raw_response = @mutex.synchronize do
295
296
  content_length = read_content_length
@@ -315,20 +316,20 @@ module RubyLsp
315
316
  nil
316
317
  end
317
318
 
318
- sig { void }
319
+ #: -> void
319
320
  def force_kill
320
321
  # Windows does not support the `TERM` signal, so we're forced to use `KILL` here
321
322
  Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
322
323
  end
323
324
 
324
- sig { params(message: ::String, type: ::Integer).void }
325
+ #: (::String message, ?type: ::Integer) -> void
325
326
  def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
326
327
  return if @outgoing_queue.closed?
327
328
 
328
329
  @outgoing_queue << RubyLsp::Notification.window_log_message(message, type: type)
329
330
  end
330
331
 
331
- sig { returns(T.nilable(Integer)) }
332
+ #: -> Integer?
332
333
  def read_content_length
333
334
  headers = @stdout.gets("\r\n\r\n")
334
335
  return unless headers
@@ -338,43 +339,73 @@ module RubyLsp
338
339
 
339
340
  length.to_i
340
341
  end
342
+
343
+ # Read a server notification from stderr. Only intended to be used by notifier thread
344
+ #: -> Hash[Symbol, untyped]?
345
+ def read_notification
346
+ headers = @stderr.gets("\r\n\r\n")
347
+ return unless headers
348
+
349
+ length = headers[/Content-Length: (\d+)/i, 1]
350
+ return unless length
351
+
352
+ raw_content = @stderr.read(length.to_i)
353
+ return unless raw_content
354
+
355
+ JSON.parse(raw_content, symbolize_names: true)
356
+ end
357
+
358
+ #: (GlobalState global_state) -> String
359
+ def server_relevant_capabilities(global_state)
360
+ {
361
+ supports_progress: global_state.client_capabilities.supports_progress,
362
+ }.to_json
363
+ end
341
364
  end
342
365
 
343
366
  class NullClient < RunnerClient
344
- extend T::Sig
345
-
346
- sig { void }
367
+ #: -> void
347
368
  def initialize # rubocop:disable Lint/MissingSuper
348
369
  end
349
370
 
350
- sig { override.void }
371
+ # @override
372
+ #: -> void
351
373
  def shutdown
352
374
  # no-op
353
375
  end
354
376
 
355
- sig { override.returns(T::Boolean) }
377
+ # @override
378
+ #: -> bool
356
379
  def stopped?
357
380
  true
358
381
  end
359
382
 
360
- sig { override.returns(String) }
383
+ # @override
384
+ #: -> String
361
385
  def rails_root
362
386
  Dir.pwd
363
387
  end
364
388
 
389
+ #: -> bool
390
+ def connected?
391
+ false
392
+ end
393
+
365
394
  private
366
395
 
367
- sig { params(message: ::String, type: ::Integer).void }
396
+ #: (::String message, ?type: ::Integer) -> void
368
397
  def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
369
398
  # no-op
370
399
  end
371
400
 
372
- sig { override.params(request: String, params: T.untyped).void }
401
+ # @override
402
+ #: (String request, **untyped params) -> void
373
403
  def send_message(request, **params)
374
404
  # no-op
375
405
  end
376
406
 
377
- sig { override.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
407
+ # @override
408
+ #: -> Hash[Symbol, untyped]?
378
409
  def read_response
379
410
  # no-op
380
411
  end