docscribe 1.6.1 → 1.6.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -193
  3. data/exe/docscribe-client +26 -7
  4. data/lib/docscribe/cli/config_builder.rb +37 -2
  5. data/lib/docscribe/cli/coverage.rb +5 -5
  6. data/lib/docscribe/cli/formatters/json.rb +74 -29
  7. data/lib/docscribe/cli/formatters/sarif.rb +20 -3
  8. data/lib/docscribe/cli/options.rb +17 -2
  9. data/lib/docscribe/cli/rbs_gen.rb +4 -4
  10. data/lib/docscribe/cli/run.rb +107 -24
  11. data/lib/docscribe/cli/update_types.rb +61 -17
  12. data/lib/docscribe/cli.rb +19 -13
  13. data/lib/docscribe/config/defaults.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +22 -1
  15. data/lib/docscribe/config/template.rb +3 -0
  16. data/lib/docscribe/config/validation.rb +19 -0
  17. data/lib/docscribe/config.rb +1 -0
  18. data/lib/docscribe/infer/behavior.rb +13 -13
  19. data/lib/docscribe/infer/params.rb +2 -2
  20. data/lib/docscribe/infer/raises.rb +5 -6
  21. data/lib/docscribe/infer/returns.rb +1612 -151
  22. data/lib/docscribe/infer.rb +7 -7
  23. data/lib/docscribe/inline_rewriter/doc_builder.rb +485 -102
  24. data/lib/docscribe/inline_rewriter.rb +263 -97
  25. data/lib/docscribe/plugin/registry.rb +1 -0
  26. data/lib/docscribe/server/base.rb +46 -15
  27. data/lib/docscribe/server/client.rb +20 -11
  28. data/lib/docscribe/server/daemon.rb +200 -23
  29. data/lib/docscribe/server/protocol.rb +4 -4
  30. data/lib/docscribe/types/primitive.rb +160 -0
  31. data/lib/docscribe/types/sorbet/base_provider.rb +33 -1
  32. data/lib/docscribe/types/yard/formatter.rb +35 -6
  33. data/lib/docscribe/types/yard/parser.rb +25 -20
  34. data/lib/docscribe/types/yard/validator.rb +131 -0
  35. data/lib/docscribe/validator/generic_compatibility.rb +698 -0
  36. data/lib/docscribe/validator/type_mismatch_validator.rb +287 -0
  37. data/lib/docscribe/version.rb +1 -1
  38. metadata +8 -3
@@ -132,6 +132,7 @@ module Docscribe
132
132
  @tag_entries.clear
133
133
  @collector_entries.clear
134
134
  @order_seq = 0
135
+ nil
135
136
  end
136
137
  end
137
138
  end
@@ -27,7 +27,7 @@ module Docscribe
27
27
  class << self
28
28
  # Start the server daemon if not running.
29
29
  #
30
- # @param [String, nil] config_path optional config file path
30
+ # @param [String?] config_path optional config file path
31
31
  # @param [Boolean] daemonize redirect stdin/stdout/stderr to /dev/null
32
32
  # @param [Integer] timeout max seconds to wait for readiness
33
33
  # @return [void]
@@ -48,7 +48,7 @@ module Docscribe
48
48
 
49
49
  # Start the server daemon and wait for it to become ready.
50
50
  #
51
- # @param [String, nil] config_path optional config path for socket/pid lookup
51
+ # @param [String?] config_path optional config path for socket/pid lookup
52
52
  # @param [Integer] timeout max seconds to wait for readiness
53
53
  # @param [Boolean] raise_on_timeout
54
54
  # @raise [StandardError]
@@ -75,7 +75,7 @@ module Docscribe
75
75
  # if yes, the daemon is still starting up (don't clean up);
76
76
  # if no, removes stale socket and pid files.
77
77
  #
78
- # @param [String, nil] config_path optional config path for socket lookup
78
+ # @param [String?] config_path optional config path for socket lookup
79
79
  # @raise [Errno::ECONNREFUSED]
80
80
  # @raise [Errno::ENOENT]
81
81
  # @raise [Errno::ENOTSOCK]
@@ -101,7 +101,7 @@ module Docscribe
101
101
  # Handle ECONNREFUSED: check if the pid process is alive.
102
102
  # Cleans up only if the process is dead.
103
103
  #
104
- # @param [String, nil] config_path
104
+ # @param [String?] config_path
105
105
  # @return [Boolean] false (not running)
106
106
  def handle_stale_socket?(config_path)
107
107
  pid = read_pid(config_path)
@@ -122,9 +122,9 @@ module Docscribe
122
122
  false
123
123
  end
124
124
 
125
- # @param [String, nil] config_path
125
+ # @param [String?] config_path
126
126
  # @raise [StandardError]
127
- # @return [Integer, nil]
127
+ # @return [Integer?]
128
128
  # @return [nil] if StandardError
129
129
  def read_pid(config_path = nil)
130
130
  File.read(pid_path(config_path)).to_i if File.exist?(pid_path(config_path))
@@ -134,20 +134,21 @@ module Docscribe
134
134
 
135
135
  # Remove stale socket and pid files.
136
136
  #
137
- # @param [String, nil] config_path
137
+ # @param [String?] config_path
138
138
  # @return [void]
139
139
  def clean_socket_files(config_path)
140
140
  FileUtils.rm_f(socket_path(config_path))
141
141
  FileUtils.rm_f(pid_path(config_path))
142
142
  end
143
143
 
144
- # @param [String, nil] config_path
144
+ # @param [String?] config_path
145
145
  # @return [String]
146
146
  def pid_path(config_path = nil)
147
147
  "#{socket_path(config_path)}.pid"
148
148
  end
149
149
 
150
- ENV_FILES = %w[Gemfile.lock rbs_collection.lock.yaml].freeze
150
+ ENV_FILES = %w[Gemfile.lock rbs_collection.lock.yaml docscribe.yml].freeze
151
+ SIG_RBS_GLOB = 'sig/**/*.rbs'
151
152
 
152
153
  # @param [String] config_path
153
154
  # @return [String]
@@ -180,7 +181,7 @@ module Docscribe
180
181
  # Environment files (Gemfile.lock, rbs_collection.lock.yaml) are also
181
182
  # included so daemon is invalidated when gems or RBS types change.
182
183
  #
183
- # @param [String, nil] config_path optional config path to differentiate
184
+ # @param [String?] config_path optional config path to differentiate
184
185
  # @return [String]
185
186
  def socket_path(config_path = nil)
186
187
  seed = +Dir.pwd
@@ -195,19 +196,49 @@ module Docscribe
195
196
 
196
197
  # Hash of environment files that affect analysis results.
197
198
  # When any of these change, the daemon is invalidated (new socket path).
199
+ # Includes Gemfile.lock, rbs_collection.lock.yaml, docscribe.yml and all sig/**/*.rbs.
198
200
  #
199
201
  # @return [String]
200
202
  def env_hash
201
- parts = ENV_FILES.map do |file|
202
- path = File.join(Dir.pwd, file)
203
- File.exist?(path) ? File.mtime(path).to_f.to_s : '0'
204
- end
203
+ parts = ENV_FILES.map { |file| env_file_mtime(file) }
204
+ parts.concat(sig_env_parts)
205
205
  Digest::MD5.hexdigest(parts.join(':'))
206
206
  end
207
207
 
208
+ # Hash of RBS signature files for cache invalidation inside daemon.
209
+ # Used by Daemon#rewrite_file to detect sig changes without requiring a new socket.
210
+ #
211
+ # @return [String]
212
+ def sig_hash
213
+ files = sig_files
214
+ parts = files.map { |p| "#{p}:#{File.mtime(p).to_f}" }
215
+ parts << "count:#{files.size}"
216
+ Digest::MD5.hexdigest(parts.join('|'))
217
+ end
218
+
219
+ # @param [String] file
220
+ # @return [String]
221
+ def env_file_mtime(file)
222
+ path = File.join(Dir.pwd, file)
223
+ File.exist?(path) ? File.mtime(path).to_f.to_s : '0'
224
+ end
225
+
226
+ # @return [Array<String>]
227
+ def sig_env_parts
228
+ files = sig_files
229
+ mtimes = files.map { |p| File.mtime(p).to_f.to_s }
230
+ mtimes << files.size.to_s
231
+ mtimes
232
+ end
233
+
234
+ # @return [Array<String>]
235
+ def sig_files
236
+ Dir.glob(File.join(Dir.pwd, SIG_RBS_GLOB)).sort
237
+ end
238
+
208
239
  public :read_pid, :pid_path, :socket_path
209
240
 
210
- # @param [String, nil] config_path
241
+ # @param [String?] config_path
211
242
  # @param [Boolean] daemonize
212
243
  # @return [void]
213
244
  def start_daemon_process(config_path:, daemonize:)
@@ -6,8 +6,8 @@ module Docscribe
6
6
  module Server
7
7
  # Client for communicating with a running Docscribe daemon.
8
8
  class Client
9
- # @param [String, nil] socket_path custom socket path (defaults to server default)
10
- # @param [String, nil] config_path optional config path for socket lookup
9
+ # @param [String?] socket_path custom socket path (defaults to server default)
10
+ # @param [String?] config_path optional config path for socket lookup
11
11
  # @return [void]
12
12
  def initialize(socket_path = nil, config_path: nil)
13
13
  @socket_path = socket_path || Server.socket_path(config_path)
@@ -17,8 +17,8 @@ module Docscribe
17
17
  #
18
18
  # @param [String] file path to file to check
19
19
  # @param [Symbol] strategy rewrite strategy (:safe, :aggressive)
20
- # @param [Hash<Symbol, Object>] rest extra JSON-RPC params (e.g. cli_overrides)
21
- # @return [Hash<String, Object>, nil] response hash or nil if server unreachable
20
+ # @param [Object] rest extra JSON-RPC params (e.g. cli_overrides)
21
+ # @return [Hash<String, Object>?] response hash or nil if server unreachable
22
22
  def check(file:, strategy: :safe, **rest)
23
23
  request('check', file: file, strategy: strategy, **rest)
24
24
  end
@@ -27,22 +27,31 @@ module Docscribe
27
27
  #
28
28
  # @param [String] file path to file to fix
29
29
  # @param [Symbol] strategy rewrite strategy (:safe, :aggressive)
30
- # @param [Hash<Symbol, Object>] rest extra JSON-RPC params (e.g. cli_overrides)
31
- # @return [Hash<String, Object>, nil] response hash or nil if server unreachable
30
+ # @param [Object] rest extra JSON-RPC params (e.g. cli_overrides)
31
+ # @return [Hash<String, Object>?] response hash or nil if server unreachable
32
32
  def fix(file:, strategy: :safe, **rest)
33
33
  request('fix', file: file, strategy: strategy, **rest)
34
34
  end
35
35
 
36
36
  # Send a shutdown request to the server.
37
37
  #
38
- # @return [Hash<String, Object>, nil] response hash or nil if server unreachable
38
+ # @return [Hash<String, Object>?] response hash or nil if server unreachable
39
39
  def shutdown
40
40
  request('shutdown')
41
41
  end
42
42
 
43
+ # Send an update_types request to the server.
44
+ #
45
+ # @param [String] dir directory to update (defaults to '.')
46
+ # @param [Object] rest extra JSON-RPC params (e.g. cli_overrides)
47
+ # @return [Hash<String, Object>?] response hash or nil if server unreachable
48
+ def update_types(dir: '.', **rest)
49
+ request('update_types', dir: dir, **rest)
50
+ end
51
+
43
52
  # Ping the server and get version/pid/uptime info.
44
53
  #
45
- # @return [Hash<String, Object>, nil] response hash or nil if server unreachable
54
+ # @return [Hash<String, Object>?] response hash or nil if server unreachable
46
55
  def ping
47
56
  request('ping')
48
57
  end
@@ -53,8 +62,8 @@ module Docscribe
53
62
  #
54
63
  # @private
55
64
  # @param [String] method method name
56
- # @param [Hash<Symbol, Object>] params request parameters
57
- # @return [Hash<String, Object>, nil]
65
+ # @param [Object] params request parameters
66
+ # @return [Hash<String, Object>?]
58
67
  def request(method, **params)
59
68
  connect do |socket|
60
69
  req = Protocol.build_request(method, params)
@@ -72,7 +81,7 @@ module Docscribe
72
81
  # @private
73
82
  # @raise [Errno::ECONNREFUSED]
74
83
  # @raise [Errno::ENOENT]
75
- # @return [T, nil] yield return value or nil on connection error
84
+ # @return [U?] yield return value or nil on connection error
76
85
  def connect
77
86
  socket = UNIXSocket.new(@socket_path)
78
87
  yield socket
@@ -4,6 +4,7 @@ require 'socket'
4
4
  require 'fileutils'
5
5
  require 'time'
6
6
  require 'timeout'
7
+ require 'digest/md5'
7
8
  require_relative '../lru_cache'
8
9
 
9
10
  module Docscribe
@@ -18,19 +19,46 @@ module Docscribe
18
19
  timeout: -32_010,
19
20
  internal: -32_099
20
21
  }.freeze
21
- # @param [String, nil] socket_path custom socket path
22
+
23
+ REQUEST_HANDLERS = {
24
+ 'check' => :handle_check,
25
+ 'fix' => :handle_fix,
26
+ 'check_batch' => :handle_check_batch,
27
+ 'update_types' => :handle_update_types
28
+ }.freeze
29
+
30
+ # @param [String?] socket_path custom socket path
22
31
  # @param [Integer] idle_timeout seconds before automatic shutdown
23
- # @param [String, nil] config_path custom config path
32
+ # @param [String?] config_path custom config path
24
33
  # @return [void]
25
34
  def initialize(socket_path: nil, idle_timeout: IDLE_TIMEOUT, config_path: nil)
35
+ setup_socket_config(socket_path, config_path, idle_timeout)
36
+ setup_runtime_state
37
+ setup_caches
38
+ end
39
+
40
+ # @param [String?] socket_path
41
+ # @param [String?] config_path
42
+ # @param [Integer] idle_timeout
43
+ # @return [void]
44
+ def setup_socket_config(socket_path, config_path, idle_timeout)
26
45
  @socket_path = socket_path || Server.socket_path(config_path)
27
- @idle_timeout = idle_timeout
28
46
  @config_path = config_path
47
+ @idle_timeout = idle_timeout
48
+ end
49
+
50
+ # @return [void]
51
+ def setup_runtime_state
29
52
  @last_request_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
30
53
  @running = false
31
54
  @server = nil
32
- @file_cache = LRUCache.new
33
55
  @started_at = Time.now
56
+ @last_sig_hash = nil
57
+ end
58
+
59
+ # @return [void]
60
+ def setup_caches
61
+ @file_cache = LRUCache.new
34
62
  @cache_mutex = Mutex.new
35
63
  @config_mutex = Mutex.new
36
64
  end
@@ -143,11 +171,29 @@ module Docscribe
143
171
  def handle_request(client, request)
144
172
  method = request['method']
145
173
  params = request['params'] || {}
174
+ dispatch_request(client, request, method, params)
175
+ end
176
+
177
+ # @private
178
+ # @param [UNIXSocket] client
179
+ # @param [Hash<String, Object>] request
180
+ # @param [String] method
181
+ # @param [Hash<String, Object>] params
182
+ # @return [void]
183
+ def dispatch_request(client, request, method, params)
184
+ handler = REQUEST_HANDLERS[method]
185
+ return send(handler, client, request['id'], params) if handler
146
186
 
187
+ dispatch_control_request(client, request, method)
188
+ end
189
+
190
+ # @private
191
+ # @param [UNIXSocket] client
192
+ # @param [Hash<String, Object>] request
193
+ # @param [String] method
194
+ # @return [void]
195
+ def dispatch_control_request(client, request, method)
147
196
  case method
148
- when 'check' then handle_check(client, request['id'], params)
149
- when 'fix' then handle_fix(client, request['id'], params)
150
- when 'check_batch' then handle_check_batch(client, request['id'], params)
151
197
  when 'shutdown' then handle_shutdown(client, request['id'])
152
198
  when 'ping' then handle_ping(client, request['id'])
153
199
  else send_error(client, request['id'], -32_601, "Unknown method: #{method}")
@@ -219,7 +265,7 @@ module Docscribe
219
265
  # @private
220
266
  # @param [String] file
221
267
  # @param [Symbol] strategy
222
- # @param [Integer, Float, nil] timeout
268
+ # @param [Integer, Float?] timeout
223
269
  # @raise [Timeout::Error]
224
270
  # @raise [StandardError]
225
271
  # @return [Hash<String, Object>]
@@ -245,13 +291,14 @@ module Docscribe
245
291
  # @return [Hash<String, Object>]
246
292
  def run_rewrite(file, strategy)
247
293
  src, result = rewrite_file(file, strategy)
248
- { 'file' => file, 'status' => result[:output] == src ? 'ok' : 'fail', 'changes' => result[:changes] }
294
+ changes = result[:changes].map { |c| c.transform_keys(&:to_s) }
295
+ { 'file' => file, 'status' => result[:output] == src ? 'ok' : 'fail', 'changes' => changes }
249
296
  end
250
297
 
251
298
  # @private
252
- # @param [Hash<String, Object>, nil] overrides
299
+ # @param [Hash<String, Object>?] overrides
253
300
  # @return [void]
254
- def apply_cli_overrides(overrides)
301
+ def apply_cli_overrides(overrides) # rubocop:disable SortedMethodsByCall/Waterfall
255
302
  @config_mutex.synchronize do
256
303
  return reset_effective_config_internal if overrides.nil? || overrides.empty?
257
304
  return if @applied_overrides == overrides
@@ -266,7 +313,8 @@ module Docscribe
266
313
  def build_effective_config(overrides)
267
314
  config = @config or return
268
315
  require 'docscribe/cli/config_builder'
269
- opts = overrides.transform_keys(&:to_sym)
316
+ require 'docscribe/cli/options'
317
+ opts = Docscribe::CLI::Options::DEFAULT.merge(overrides.transform_keys(&:to_sym))
270
318
  @effective_config = Docscribe::CLI::ConfigBuilder.build(config, opts)
271
319
  @file_cache.clear
272
320
  @applied_overrides = overrides
@@ -292,35 +340,164 @@ module Docscribe
292
340
  # @param [String] file
293
341
  # @param [Symbol] strategy
294
342
  # @raise [StandardError]
295
- # @return [(String, Hash<Symbol, Object>)]
343
+ # @return [(String, Hash<Symbol, String, Array<Hash<Symbol, Object>>>)]
296
344
  def rewrite_file(file, strategy)
297
345
  @cache_mutex.synchronize do
298
346
  config = @effective_config || @config or raise 'Docscribe: config not loaded'
299
347
  key = [file, strategy]
300
348
  mtime = File.mtime(file)
301
- hit = @file_cache[key]
302
- return [hit[:src], hit[:result]] if hit && hit[:mtime] == mtime
349
+ sig_hash = sig_hash_for(config)
350
+ handle_sig_change(sig_hash)
351
+ cached = cached_result(key, mtime, sig_hash)
352
+ return cached if cached
303
353
 
304
354
  rewrite_and_cache(file, strategy, config, key, mtime)
305
355
  end
306
356
  end
307
357
 
358
+ # @private
359
+ # @param [String] sig_hash
360
+ # @return [void]
361
+ def handle_sig_change(sig_hash)
362
+ if @last_sig_hash && @last_sig_hash != sig_hash
363
+ [@config, @effective_config].compact.each { |c| clear_rbs_cache(c) }
364
+ @file_cache.clear
365
+ end
366
+ @last_sig_hash = sig_hash
367
+ end
368
+
369
+ # @private
370
+ # @param [Array<String, Symbol>] key
371
+ # @param [Time] mtime
372
+ # @param [String] sig_hash
373
+ # @return [(String, Hash<Symbol, String, Array<Hash<Symbol, Object>>>)?]
374
+ def cached_result(key, mtime, sig_hash)
375
+ hit = @file_cache[key]
376
+ return nil unless hit && hit[:mtime] == mtime && hit[:sig_hash] == sig_hash
377
+
378
+ [hit[:src], hit[:result]]
379
+ end
380
+
381
+ # Clear memoized RBS providers so next request rebuilds env with fresh sig files.
382
+ #
383
+ # @private
384
+ # @param [Docscribe::Config] config
385
+ # @raise [StandardError]
386
+ # @return [void]
387
+ # @return [nil] if StandardError
388
+ def clear_rbs_cache(config)
389
+ config.instance_variable_set(:@rbs_provider, nil) if config.instance_variable_defined?(:@rbs_provider)
390
+ config.instance_variable_set(:@core_rbs_provider, nil) if config.instance_variable_defined?(:@core_rbs_provider)
391
+ rescue StandardError
392
+ nil
393
+ end
394
+
308
395
  # @private
309
396
  # @param [String] file
310
397
  # @param [Symbol] strategy
311
- # @param [Docscribe::Config, nil] config effective or base config
312
- # @param [Array<(String, Symbol)>] key cache key
398
+ # @param [Docscribe::Config] config effective or base config
399
+ # @param [Array<String, Symbol>] key cache key
313
400
  # @param [Time] mtime file modification time
314
- # @return [(String, Hash<Symbol, Object>)]
401
+ # @return [(String, Hash<Symbol, String, Array<Hash<Symbol, Object>>>)]
315
402
  def rewrite_and_cache(file, strategy, config, key, mtime)
316
403
  src = File.read(file)
317
404
  rbs = config.respond_to?(:core_rbs_provider) ? config.core_rbs_provider : nil
318
405
  result = Docscribe::InlineRewriter.rewrite_with_report(src, strategy: strategy, config: config,
319
406
  core_rbs_provider: rbs, file: file)
320
- @file_cache[key] = { mtime: mtime, src: src, result: result }
407
+ @file_cache[key] = { mtime: mtime, sig_hash: @last_sig_hash, src: src, result: result }
321
408
  [src, result]
322
409
  end
323
410
 
411
+ # Hash of RBS signature files for cache invalidation.
412
+ # Includes all files under sig_dirs (default: sig/**/*.rbs).
413
+ #
414
+ # @private
415
+ # @param [Docscribe::Config] config effective or base config
416
+ # @raise [StandardError]
417
+ # @return [String]
418
+ # @return [String] if StandardError
419
+ def sig_hash_for(config)
420
+ files = sig_rbs_files(sig_dirs_for(config))
421
+ parts = files.map { |p| "#{p}:#{File.mtime(p).to_f}" if File.file?(p) }.compact
422
+ parts << "count:#{files.size}"
423
+ Digest::MD5.hexdigest(parts.join('|'))
424
+ rescue StandardError
425
+ '0'
426
+ end
427
+
428
+ # @private
429
+ # @param [Array<String>] dirs
430
+ # @return [Array<String>]
431
+ def sig_rbs_files(dirs)
432
+ dirs.flat_map { |dir| Dir.glob(File.join(Dir.pwd, dir, '**', '*.rbs')) }.uniq.sort
433
+ end
434
+
435
+ # Resolve sig dirs from config, falling back to defaults.
436
+ #
437
+ # @private
438
+ # @param [Docscribe::Config] config
439
+ # @raise [StandardError]
440
+ # @return [Array<String>]
441
+ # @return [Array] if StandardError
442
+ def sig_dirs_for(config)
443
+ raw_dirs = config.raw.dig('rbs', 'sig_dirs') if config.respond_to?(:raw)
444
+ dirs = Array(raw_dirs || Docscribe::Config::DEFAULT.dig('rbs', 'sig_dirs')).map(&:to_s) # steep:ignore
445
+ dirs.empty? ? ['sig'] : dirs
446
+ rescue StandardError
447
+ ['sig']
448
+ end
449
+
450
+ # Handle update_types request (used by RubyMine plugin).
451
+ #
452
+ # @private
453
+ # @param [UNIXSocket] client connected client socket
454
+ # @param [String, Integer] id request ID
455
+ # @param [Hash<String, Object>] params request params
456
+ # @raise [StandardError]
457
+ # @return [void]
458
+ # @return [void] if StandardError
459
+ def handle_update_types(client, id, params)
460
+ target = update_types_dir(params)
461
+ apply_cli_overrides(params['cli_overrides'])
462
+ exit_code = run_update_types(target)
463
+ @file_cache.clear
464
+ send_update_types_response(client, id, target, exit_code)
465
+ rescue StandardError => e
466
+ @file_cache.clear
467
+ code, message, data = classify_error(e, 'update_types', params)
468
+ send_error(client, id, code, message, data)
469
+ end
470
+
471
+ # @private
472
+ # @param [Hash<String, Object>] params
473
+ # @return [String]
474
+ def update_types_dir(params)
475
+ params['file'] || params['dir'] || params['directory'] || '.'
476
+ end
477
+
478
+ # @private
479
+ # @param [String] target
480
+ # @return [Integer]
481
+ def run_update_types(target)
482
+ require 'docscribe/cli/update_types'
483
+ Docscribe::CLI::UpdateTypes.run([target])
484
+ end
485
+
486
+ # @private
487
+ # @param [UNIXSocket] client
488
+ # @param [String, Integer] id
489
+ # @param [String] dir
490
+ # @param [Integer] exit_code
491
+ # @return [void]
492
+ def send_update_types_response(client, id, dir, exit_code)
493
+ if exit_code.zero?
494
+ send_result(client, id, 'status' => 'ok', 'dir' => dir, 'exit_code' => exit_code)
495
+ else
496
+ send_error(client, id, ERROR_CODES[:internal], "update_types failed with exit code #{exit_code}",
497
+ { 'dir' => dir, 'exit_code' => exit_code })
498
+ end
499
+ end
500
+
324
501
  # Handle a shutdown request.
325
502
  #
326
503
  # @private
@@ -363,9 +540,9 @@ module Docscribe
363
540
 
364
541
  # @private
365
542
  # @param [Exception] exception
366
- # @param [String, nil] _method_name
543
+ # @param [String?] _method_name
367
544
  # @param [Hash<String, Object>] params
368
- # @return [(Integer, String, Object)]
545
+ # @return [(Integer, String, Hash<Symbol, Object, nil>, nil)]
369
546
  def classify_error(exception, _method_name = nil, params = {})
370
547
  if exception.is_a?(LoadError) || exception.is_a?(Gem::LoadError)
371
548
  classify_gem_error(exception)
@@ -395,7 +572,7 @@ module Docscribe
395
572
 
396
573
  # @private
397
574
  # @param [Exception] exception
398
- # @return [(Integer, String, Hash<Symbol, String>)]
575
+ # @return [(Integer, String, Hash<Symbol, String, nil>)]
399
576
  def classify_gem_error(exception)
400
577
  data = { gem: nil }
401
578
  data[:gem] = exception.path if exception.respond_to?(:path) && exception.path
@@ -473,7 +650,7 @@ module Docscribe
473
650
  # @param [String, Integer, nil] id
474
651
  # @param [Integer] code
475
652
  # @param [String] message
476
- # @param [Hash<Symbol, Object>, nil] data optional structured error data
653
+ # @param [Hash<Symbol, Object>?] data optional structured error data
477
654
  # @return [void]
478
655
  def send_error(client, id, code, message, data = nil)
479
656
  error = { code: code, message: message }
@@ -13,8 +13,8 @@ module Docscribe
13
13
  #
14
14
  # @note module_function: defines #build_request (visibility: private)
15
15
  # @param [String] method method name
16
- # @param [Hash<Symbol, Object>] params request parameters
17
- # @return [Hash<Symbol, Object>]
16
+ # @param [Hash<Symbol, T>] params request parameters
17
+ # @return [Hash<Symbol, String, Hash<Symbol, T>>]
18
18
  def build_request(method, params = {})
19
19
  {
20
20
  jsonrpc: '2.0',
@@ -29,7 +29,7 @@ module Docscribe
29
29
  # @note module_function: defines #parse_response (visibility: private)
30
30
  # @param [String] line raw JSON line
31
31
  # @raise [JSON::ParserError]
32
- # @return [Hash<String, Object>, nil]
32
+ # @return [Hash<String, Object>?]
33
33
  # @return [nil] if JSON::ParserError
34
34
  def parse_response(line)
35
35
  JSON.parse(line)
@@ -40,7 +40,7 @@ module Docscribe
40
40
  # Serialize a hash to a JSON line.
41
41
  #
42
42
  # @note module_function: defines #serialize (visibility: private)
43
- # @param [Hash<Object, Object>] hash
43
+ # @param [Object] hash
44
44
  # @return [String]
45
45
  def serialize(hash)
46
46
  "#{JSON.generate(hash)}\n"