docscribe 1.5.1 → 1.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bbceb4704c077ffbe69c69d769bc5790347bbe3c64fce843069638214e53ee6
4
- data.tar.gz: 22a7dd5cfa26ea46bac3e3a47e5e228cd3b0c6520fb3a3909dff09e66b1099ee
3
+ metadata.gz: 65ef814f594285faee2123170710fc06c47e3cf45471e79d4626fc2cb4885183
4
+ data.tar.gz: '008dd20fd7a7c3a6c9254be9ac8158005a4a84779d070939a7abdad0476f6df9'
5
5
  SHA512:
6
- metadata.gz: 576a4645d81448f7cbceb93584e62710d54885d12d099b601d02d50b014d64cec6747756e7ed647d3dda35d12f734646b20508ab9080793e3ae1c26d027da6ba
7
- data.tar.gz: f69b8cd81d990e88a292d0ada1eae224e521d9d6d6f4ced608a06cb71550bddbeb88723407332ea82b5c7c1c564ce6a6fd56286829d3ccb3077a7ed0ceb08629
6
+ metadata.gz: ff68cdf76163fc3a6e59192895b1b59a79116abb87f4921ea18d48b299a6b8f5e0be53ea090da01d089ed5b7cf954e77cfb78ca278012ced7fc97c39d76dfabc
7
+ data.tar.gz: 9ed8f9e80ceea75d6072077cf08a6a9d2d1c6135dd99ea4478ffca41889169dddc6f329f8f10888d5d1fdeeb82dc84fa046c6a818eaae20fae32639773b5c8e9
data/README.md CHANGED
@@ -648,7 +648,8 @@ Exit code `0` if no placeholders found, `1` if any are detected.
648
648
  ### `docscribe server` — persistent daemon mode
649
649
 
650
650
  > [!NOTE]
651
- > Server mode requires **Ruby 3.1+** (`Process.fork` for background daemon).
651
+ > Server mode requires **Ruby 3.1+** and a platform with `Process.fork` and Unix domain sockets.
652
+ > Not available on **Windows** (no Unix sockets) or **JRuby** (no `Process.fork`).
652
653
 
653
654
  `docscribe server` starts a background daemon that keeps the Ruby runtime loaded and caches parsed ASTs across
654
655
  invocations. Subsequent `docscribe` calls with `--server` communicate with the daemon over a Unix socket instead of
@@ -703,6 +704,25 @@ docscribe-client --ping
703
704
  docscribe-client --shutdown
704
705
  ```
705
706
 
707
+ Additional thin client commands:
708
+
709
+ ```shell
710
+ # Print the Unix socket path (for IDE plugin integration)
711
+ docscribe-client --socket-path
712
+
713
+ # Show server info: version, pid, uptime, config path
714
+ docscribe-client --info
715
+
716
+ # Auto-start the daemon before running a check or fix
717
+ docscribe-client --ensure --check lib/user.rb
718
+
719
+ # Start the daemon standalone (exits when ready)
720
+ docscribe-client --ensure
721
+
722
+ # Batch check multiple files in one RPC call
723
+ docscribe-client --check-batch lib/a.rb,lib/b.rb,lib/c.rb
724
+ ```
725
+
706
726
  The thin client is used automatically by
707
727
  the [VS Code](https://marketplace.visualstudio.com/items?itemName=unurgunite.docscribe-vscode)
708
728
  and [RubyMine](https://plugins.jetbrains.com/plugin/32349-docscribe) plugins when the daemon is running.
@@ -717,6 +737,41 @@ The daemon's socket path includes a hash of:
717
737
  If any of these files change, the next `docscribe --server` call will start a new daemon automatically. The old daemon
718
738
  is left to idle-timeout on its own.
719
739
 
740
+ **JSON-RPC protocol (semi-stable API):**
741
+
742
+ The daemon speaks JSON-RPC 2.0 over Unix socket. Each request is a JSON line, each response is a JSON line.
743
+
744
+ Methods:
745
+
746
+ | Method | Parameters | Result |
747
+ |--------|-----------|--------|
748
+ | `check` | `file` (string), `strategy` ("safe"/"aggressive"), `cli_overrides` (hash, optional) | `status`, `changed`, `changes` |
749
+ | `fix` | same as `check` | `status`, `changed`, `changes` |
750
+ | `check_batch` | `files` (array of strings), `strategy`, `cli_overrides`, `timeout` (int, optional) | array of per-file results |
751
+ | `ping` | — | `version`, `pid`, `socket_path`, `started_at`, `uptime` |
752
+ | `shutdown` | — | `status` |
753
+
754
+ Error codes (standardized for IDE plugin integration):
755
+
756
+ | Code | Meaning | `error.data` fields |
757
+ |------|---------|-------------------|
758
+ | `-32000` | Gem/dependency not found | `gem` |
759
+ | `-32001` | Syntax error in analyzed file | `file`, `line`, `detail` |
760
+ | `-32002` | Config load failure | — |
761
+ | `-32010` | Timeout | `timeout`, `file` |
762
+ | `-32099` | Internal unhandled error | `backtrace` (first 5 lines) |
763
+
764
+ **Socket path derivation:**
765
+
766
+ The socket path is a deterministic hash of:
767
+ - Working directory (`Dir.pwd`)
768
+ - `Gemfile.lock` mtime and `rbs_collection.lock.yaml` mtime (environment invalidation)
769
+ - Config file path and mtime (when `--config` is used)
770
+
771
+ Result: `/tmp/docscribe-<MD5 hash>.sock`
772
+
773
+ IDE plugins should use `docscribe-client --socket-path` instead of reimplementing this algorithm.
774
+
720
775
  **CLI override handling:**
721
776
 
722
777
  When CLI overrides (`-C`, `--include`, `--exclude`, etc.) change between requests, the daemon resets its effective
data/exe/docscribe-client CHANGED
@@ -60,6 +60,9 @@ end
60
60
  config_path = nil
61
61
  mode = nil
62
62
  file = nil
63
+ ensure_flag = false
64
+ batch_files = nil
65
+ batch_timeout = nil
63
66
 
64
67
  OptionParser.new do |opts|
65
68
  opts.on('-C', '--config <path>', 'Config file path') { |v| config_path = v }
@@ -71,20 +74,65 @@ OptionParser.new do |opts|
71
74
  mode = :fix
72
75
  file = v
73
76
  end
77
+ opts.on('--check-batch <files>', 'Check multiple files (comma-separated)') do |v|
78
+ mode = :check_batch
79
+ batch_files = v
80
+ end
81
+ opts.on('--timeout <seconds>', 'Timeout per file in batch mode') { |v| batch_timeout = v.to_f }
74
82
  opts.on('--shutdown', 'Shutdown the server') { mode = :shutdown }
75
83
  opts.on('--status', 'Show server status') { mode = :status }
76
84
  opts.on('--ping', 'Ping the server') { mode = :ping }
85
+ opts.on('--socket-path', 'Print socket path and exit') { mode = :socket_path }
86
+ opts.on('--info', 'Show server info') { mode = :info }
87
+ opts.on('--ensure', 'Ensure server is running before check/fix') { ensure_flag = true }
77
88
  end.parse!
78
89
 
79
90
  sock = socket_path(config_path)
80
91
 
92
+ if ensure_flag && mode.nil?
93
+ require 'docscribe/server'
94
+ Docscribe::Server.ensure_running!(config_path: config_path)
95
+ exit 0
96
+ end
97
+
98
+ if ensure_flag && %i[check fix].include?(mode)
99
+ begin
100
+ UNIXSocket.new(sock).close
101
+ rescue Errno::ECONNREFUSED, Errno::ENOENT
102
+ require 'docscribe/server'
103
+ Docscribe::Server.ensure_running!(config_path: config_path)
104
+ rescue StandardError => e
105
+ warn "docscribe: #{e.message}"
106
+ exit 1
107
+ end
108
+ end
109
+
81
110
  case mode
111
+ when :socket_path
112
+ puts sock
113
+ when :info
114
+ result = send_request(sock, 'ping')
115
+ if result && result['result']
116
+ info = result['result']
117
+ info['status'] = 'running'
118
+ info['config_path'] = config_path if config_path
119
+ info['cli_overrides'] = {}
120
+ puts JSON.generate(info)
121
+ else
122
+ puts JSON.generate({ status: 'not_running', socket_path: sock })
123
+ end
82
124
  when :check
83
125
  result = send_request(sock, 'check', file: file, strategy: :safe)
84
126
  puts JSON.generate(result || { error: 'Connection failed' })
85
127
  when :fix
86
128
  result = send_request(sock, 'fix', file: file, strategy: :safe)
87
129
  puts JSON.generate(result || { error: 'Connection failed' })
130
+ when :check_batch
131
+ files = batch_files.split(',').map(&:strip)
132
+ params = { files: files, strategy: :safe }
133
+ params[:timeout] = batch_timeout if batch_timeout
134
+ result = send_request(sock, 'check_batch', **params)
135
+ puts JSON.generate(result || { error: 'Connection failed' })
88
136
  when :shutdown
89
137
  result = send_request(sock, 'shutdown')
90
138
  puts JSON.generate(result || { error: 'Connection failed' })
@@ -100,6 +148,6 @@ when :status
100
148
  end
101
149
  puts JSON.generate(alive ? { status: 'running', socket: sock } : { status: 'not_running' })
102
150
  else
103
- warn "Usage: #{$PROGRAM_NAME} --check <file> | --fix <file> | --shutdown | --status | --ping"
151
+ warn "Usage: #{$PROGRAM_NAME} --check <file> | --fix <file> | --check-batch <files> | --shutdown | --status | --ping | --socket-path | --info | --ensure"
104
152
  exit 1
105
153
  end
@@ -167,7 +167,7 @@ module Docscribe
167
167
  # @return [Boolean] if StandardError
168
168
  def generate_for_file(path, options)
169
169
  process_source?(File.read(path), path, options)
170
- rescue Parser::SyntaxError => e # steep:ignore
170
+ rescue Parser::SyntaxError => e
171
171
  warn "Syntax error in #{path}: #{e.message}"
172
172
  false
173
173
  rescue StandardError => e
@@ -169,7 +169,7 @@ module Docscribe
169
169
  return unless ast
170
170
 
171
171
  walk_for_methods(ast, [], methods, path)
172
- rescue Parser::SyntaxError => e # steep:ignore
172
+ rescue Parser::SyntaxError => e
173
173
  warn "Syntax error in #{path}: #{e.message}"
174
174
  rescue StandardError => e
175
175
  warn "Error parsing #{path}: #{e.class}: #{e.message}"
@@ -98,7 +98,7 @@ module Docscribe
98
98
  buffer = Parser::Source::Buffer.new('(param)')
99
99
  buffer.source = src
100
100
  Docscribe::Parsing.parse_buffer(buffer)
101
- rescue Parser::SyntaxError # steep:ignore
101
+ rescue Parser::SyntaxError
102
102
  nil
103
103
  end
104
104
  end
@@ -26,7 +26,7 @@ module Docscribe
26
26
  local_var_types = build_local_variable_types(body)
27
27
  run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
28
28
  local_var_types: local_var_types) || FALLBACK_TYPE
29
- rescue Parser::SyntaxError # steep:ignore
29
+ rescue Parser::SyntaxError
30
30
  FALLBACK_TYPE
31
31
  end
32
32
 
@@ -594,7 +594,7 @@ module Docscribe
594
594
 
595
595
  return_type = rest[1...type_end] #: String
596
596
  desc = rest[(type_end + 1)..]&.strip
597
- [return_type, desc&.empty? ? nil : desc]
597
+ [return_type, desc && desc.empty? ? nil : desc]
598
598
  end
599
599
 
600
600
  # Extract all comment tags from line
@@ -7,6 +7,7 @@ require 'securerandom'
7
7
  require 'digest/md5'
8
8
  require 'tmpdir'
9
9
  require 'time'
10
+ require 'timeout'
10
11
  require_relative 'lru_cache'
11
12
 
12
13
  module Docscribe
@@ -34,11 +35,11 @@ module Docscribe
34
35
  # @param [String?] config_path optional config file path
35
36
  # @param [Boolean] daemonize redirect stdin/stdout/stderr to /dev/null
36
37
  # @param [Integer] timeout max seconds to wait for readiness
37
- # @raise [StandardError]
38
38
  # @return [void]
39
39
  def ensure_running!(config_path: nil, daemonize: false, timeout: 5)
40
40
  return if running?(config_path)
41
- raise 'Server mode is unavailable on this Ruby/platform (Process.fork not supported)' unless Process.respond_to?(:fork)
41
+
42
+ check_platform_support!
42
43
 
43
44
  lock_path = "#{socket_path(config_path)}.lock"
44
45
  File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |lock|
@@ -86,17 +87,18 @@ module Docscribe
86
87
  # @raise [StandardError]
87
88
  # @return [Boolean]
88
89
  # @return [Boolean] if Errno::ECONNREFUSED
89
- # @return [Boolean] if Errno::ENOENT, Errno::ENOTSOCK
90
+ # @return [void, Boolean] if Errno::ENOENT, Errno::ENOTSOCK
90
91
  # @return [Boolean] if StandardError
91
92
  def running?(config_path = nil)
93
+ return false unless defined?(UNIXSocket)
94
+
92
95
  socket = UNIXSocket.new(socket_path(config_path))
93
96
  socket.close
94
97
  true
95
98
  rescue Errno::ECONNREFUSED
96
99
  handle_stale_socket?(config_path)
97
100
  rescue Errno::ENOENT, Errno::ENOTSOCK
98
- clean_socket_files(config_path)
99
- false
101
+ clean_socket_files(config_path) && false
100
102
  rescue StandardError
101
103
  false
102
104
  end
@@ -152,6 +154,29 @@ module Docscribe
152
154
 
153
155
  ENV_FILES = %w[Gemfile.lock rbs_collection.lock.yaml].freeze
154
156
 
157
+ # @param [String] config_path
158
+ # @return [String]
159
+ def config_hash(config_path)
160
+ resolved = File.expand_path(config_path)
161
+ mtime = File.exist?(resolved) ? File.mtime(resolved).to_f : 0.0
162
+ Digest::MD5.hexdigest("#{resolved}:#{mtime}")
163
+ end
164
+
165
+ # Check platform compatibility before starting server.
166
+ #
167
+ # @raise [StandardError]
168
+ # @return [void]
169
+ def check_platform_support!
170
+ unless defined?(UNIXSocket)
171
+ raise 'Server mode requires Unix domain sockets, which are not available on Windows. ' \
172
+ 'Use docscribe directly without --server flag.'
173
+ end
174
+ return if Process.respond_to?(:fork)
175
+
176
+ raise 'Server mode requires Process.fork, which is not available on JRuby. ' \
177
+ 'Use docscribe directly without --server flag.'
178
+ end
179
+
155
180
  # Derive a project-specific socket path from the current working directory.
156
181
  # Uses MD5 (deterministic across processes) instead of String#hash
157
182
  # (which varies per Ruby process due to random seeding).
@@ -173,14 +198,6 @@ module Docscribe
173
198
  "#{SOCKET_DIR}/docscribe-#{Digest::MD5.hexdigest(seed)}.sock"
174
199
  end
175
200
 
176
- # @param [String] config_path
177
- # @return [String]
178
- def config_hash(config_path)
179
- resolved = File.expand_path(config_path)
180
- mtime = File.exist?(resolved) ? File.mtime(resolved).to_f : 0.0
181
- Digest::MD5.hexdigest("#{resolved}:#{mtime}")
182
- end
183
-
184
201
  # Hash of environment files that affect analysis results.
185
202
  # When any of these change, the daemon is invalidated (new socket path).
186
203
  #
@@ -332,6 +349,14 @@ module Docscribe
332
349
 
333
350
  # Daemon process that loads the Ruby runtime once and serves requests.
334
351
  class Daemon
352
+ # Standardized JSON-RPC error codes.
353
+ ERROR_CODES = {
354
+ gem_not_found: -32_000,
355
+ syntax_error: -32_001,
356
+ config_load_failure: -32_002,
357
+ timeout: -32_010,
358
+ internal: -32_099
359
+ }.freeze
335
360
  # @param [String?] socket_path custom socket path
336
361
  # @param [Integer] idle_timeout seconds before automatic shutdown
337
362
  # @param [String?] config_path custom config path
@@ -345,6 +370,8 @@ module Docscribe
345
370
  @server = nil
346
371
  @file_cache = LRUCache.new
347
372
  @started_at = Time.now
373
+ @cache_mutex = Mutex.new
374
+ @config_mutex = Mutex.new
348
375
  end
349
376
 
350
377
  # Start the daemon: load dependencies, bind socket, enter listen loop.
@@ -415,15 +442,16 @@ module Docscribe
415
442
  end
416
443
 
417
444
  # Accept a client connection if one is available.
445
+ # Spawns a thread to handle each client concurrently.
418
446
  #
419
447
  # @private
420
448
  # @return [void]
421
449
  def accept_client
422
- client = @server&.accept if @server&.wait_readable(1)
450
+ client = @server&.accept if @server&.wait_readable(0.1)
423
451
  return unless client
424
452
 
425
- handle_client(client)
426
453
  @last_request_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
454
+ Thread.new(client) { |conn| handle_client(conn) }
427
455
  end
428
456
 
429
457
  # Read a request from a client connection and dispatch it.
@@ -437,7 +465,10 @@ module Docscribe
437
465
  request = Protocol.parse_response(request_line)
438
466
  request ? handle_request(client, request) : send_error(client, nil, -32_700, 'Parse error')
439
467
  rescue StandardError => e
440
- send_error(client, request&.dig('id'), -32_603, "#{e.class}: #{e.message}")
468
+ method_name = request&.dig('method')
469
+ error_params = request&.dig('params') || {}
470
+ code, message, data = classify_error(e, method_name, error_params)
471
+ send_error(client, request&.dig('id'), code, message, data)
441
472
  ensure
442
473
  client.close
443
474
  end
@@ -455,6 +486,7 @@ module Docscribe
455
486
  case method
456
487
  when 'check' then handle_check(client, request['id'], params)
457
488
  when 'fix' then handle_fix(client, request['id'], params)
489
+ when 'check_batch' then handle_check_batch(client, request['id'], params)
458
490
  when 'shutdown' then handle_shutdown(client, request['id'])
459
491
  when 'ping' then handle_ping(client, request['id'])
460
492
  else send_error(client, request['id'], -32_601, "Unknown method: #{method}")
@@ -465,7 +497,9 @@ module Docscribe
465
497
  # @param [UNIXSocket] client
466
498
  # @param [String, Integer] id
467
499
  # @param [Hash<String, Object>] params
500
+ # @raise [StandardError]
468
501
  # @return [void]
502
+ # @return [void] if StandardError
469
503
  def handle_check(client, id, params)
470
504
  file = params['file']
471
505
  strategy = (params['strategy'] || 'safe').to_sym
@@ -475,13 +509,17 @@ module Docscribe
475
509
  src, result = rewrite_file(file, strategy)
476
510
  send_result(client, id, 'status' => result[:output] == src ? 'ok' : 'fail',
477
511
  'changed' => result[:output] != src, 'changes' => result[:changes])
512
+ rescue StandardError => e
513
+ handle_request_error(client, id, e, file)
478
514
  end
479
515
 
480
516
  # @private
481
517
  # @param [UNIXSocket] client
482
518
  # @param [String, Integer] id
483
519
  # @param [Hash<String, Object>] params
520
+ # @raise [StandardError]
484
521
  # @return [void]
522
+ # @return [void] if StandardError
485
523
  def handle_fix(client, id, params)
486
524
  file = params['file']
487
525
  strategy = (params['strategy'] || 'safe').to_sym
@@ -489,18 +527,79 @@ module Docscribe
489
527
 
490
528
  apply_cli_overrides(params['cli_overrides'])
491
529
  src, result = rewrite_file(file, strategy)
492
- File.write(file, result[:output]) if result[:output] != src
493
- send_result(client, id, 'status' => 'ok',
494
- 'changed' => result[:output] != src, 'changes' => result[:changes])
530
+ changed = result[:output] != src
531
+ File.write(file, result[:output]) if changed
532
+ send_result(client, id, 'status' => 'ok', 'changed' => changed, 'changes' => result[:changes])
533
+ rescue StandardError => e
534
+ handle_request_error(client, id, e, file)
535
+ end
536
+
537
+ # @private
538
+ # @param [Object] client
539
+ # @param [Object] id
540
+ # @param [Object] params
541
+ # @return [void]
542
+ def handle_check_batch(client, id, params)
543
+ files = params['files']
544
+ return send_error(client, id, -32_602, 'Missing files parameter') unless files.is_a?(Array) && !files.empty?
545
+
546
+ strategy = (params['strategy'] || 'safe').to_sym
547
+ timeout = params['timeout']
548
+
549
+ apply_cli_overrides(params['cli_overrides'])
550
+
551
+ results = files.map do |file|
552
+ process_file_in_batch(file, strategy, timeout)
553
+ end
554
+
555
+ send_result(client, id, { 'results' => results })
556
+ end
557
+
558
+ # @private
559
+ # @param [String] file
560
+ # @param [Symbol] strategy
561
+ # @param [Integer, Float?] timeout
562
+ # @raise [Timeout::Error]
563
+ # @raise [StandardError]
564
+ # @return [Hash<String, Object>]
565
+ # @return [Hash] if Timeout::Error
566
+ # @return [Hash] if StandardError
567
+ def process_file_in_batch(file, strategy, timeout = nil)
568
+ return { 'file' => file, 'status' => 'error', 'error' => "File not found: #{file}" } unless File.file?(file)
569
+
570
+ block = -> { run_rewrite(file, strategy) }
571
+ timeout ? Timeout.timeout(timeout.to_f, &block) : block.call
572
+ rescue Timeout::Error
573
+ { 'file' => file, 'status' => 'error', 'error' => 'Timeout' }
574
+ rescue StandardError => e
575
+ { 'file' => file, 'status' => 'error', 'error' => "#{e.class}: #{e.message}" }
576
+ end
577
+
578
+ # @private
579
+ # @param [String] file
580
+ # @param [Symbol] strategy
581
+ # @return [Hash<String, Object>]
582
+ def run_rewrite(file, strategy)
583
+ src, result = rewrite_file(file, strategy)
584
+ { 'file' => file, 'status' => result[:output] == src ? 'ok' : 'fail', 'changes' => result[:changes] }
495
585
  end
496
586
 
497
587
  # @private
498
588
  # @param [Hash<String, Object>?] overrides
499
589
  # @return [void]
500
590
  def apply_cli_overrides(overrides)
501
- return reset_effective_config if overrides.nil? || overrides.empty?
502
- return if @applied_overrides == overrides
591
+ @config_mutex.synchronize do
592
+ return reset_effective_config_internal if overrides.nil? || overrides.empty?
593
+ return if @applied_overrides == overrides
503
594
 
595
+ build_effective_config(overrides)
596
+ end
597
+ end
598
+
599
+ # @private
600
+ # @param [Hash<String, Object>] overrides
601
+ # @return [void]
602
+ def build_effective_config(overrides)
504
603
  config = @config or return
505
604
  require 'docscribe/cli/config_builder'
506
605
  opts = overrides.transform_keys(&:to_sym)
@@ -512,6 +611,12 @@ module Docscribe
512
611
  # @private
513
612
  # @return [void]
514
613
  def reset_effective_config
614
+ @config_mutex.synchronize { reset_effective_config_internal }
615
+ end
616
+
617
+ # @private
618
+ # @return [void]
619
+ def reset_effective_config_internal
515
620
  return unless @effective_config
516
621
 
517
622
  @effective_config = nil
@@ -525,12 +630,25 @@ module Docscribe
525
630
  # @raise [StandardError]
526
631
  # @return [(String, Hash<Symbol, Object>)]
527
632
  def rewrite_file(file, strategy)
528
- config = @effective_config || @config or raise 'Docscribe: config not loaded'
529
- key = [file, strategy]
530
- mtime = File.mtime(file)
531
- hit = @file_cache[key]
532
- return [hit[:src], hit[:result]] if hit && hit[:mtime] == mtime
633
+ @cache_mutex.synchronize do
634
+ config = @effective_config || @config or raise 'Docscribe: config not loaded'
635
+ key = [file, strategy]
636
+ mtime = File.mtime(file)
637
+ hit = @file_cache[key]
638
+ return [hit[:src], hit[:result]] if hit && hit[:mtime] == mtime
639
+
640
+ rewrite_and_cache(file, strategy, config, key, mtime)
641
+ end
642
+ end
533
643
 
644
+ # @private
645
+ # @param [String] file
646
+ # @param [Symbol] strategy
647
+ # @param [Object] config
648
+ # @param [Object] key
649
+ # @param [Object] mtime
650
+ # @return [(String, Hash<Symbol, Object>)]
651
+ def rewrite_and_cache(file, strategy, config, key, mtime)
534
652
  src = File.read(file)
535
653
  rbs = config.respond_to?(:core_rbs_provider) ? config.core_rbs_provider : nil
536
654
  result = Docscribe::InlineRewriter.rewrite_with_report(src, strategy: strategy, config: config, core_rbs_provider: rbs, file: file)
@@ -571,21 +689,131 @@ module Docscribe
571
689
  # @private
572
690
  # @param [UNIXSocket] client connected client socket
573
691
  # @param [String, Integer] id request ID
574
- # @param [Hash<String, Object>] result result data
692
+ # @param [Object] result result data
575
693
  # @return [void]
576
694
  def send_result(client, id, result)
577
695
  response = { jsonrpc: '2.0', id: id, result: result }
578
696
  client.write(Protocol.serialize(response))
579
697
  end
580
698
 
699
+ # @private
700
+ # @param [Exception] exception
701
+ # @param [String?] _method_name
702
+ # @param [Hash<String, Object>] params
703
+ # @return [(Integer, String, Object?)]
704
+ def classify_error(exception, _method_name = nil, params = {})
705
+ if exception.is_a?(LoadError) || exception.is_a?(Gem::LoadError)
706
+ classify_gem_error(exception)
707
+ elsif syntax_error?(exception)
708
+ classify_syntax_err(exception, params)
709
+ elsif timeout_error?(exception)
710
+ classify_timeout_err(exception, params)
711
+ else
712
+ classify_internal_err(exception)
713
+ end
714
+ end
715
+
716
+ # @private
717
+ # @param [Exception] exception
718
+ # @return [Boolean]
719
+ def syntax_error?(exception)
720
+ exception.is_a?(Docscribe::ParseError) ||
721
+ (defined?(Parser::SyntaxError) && exception.is_a?(Parser::SyntaxError))
722
+ end
723
+
724
+ # @private
725
+ # @param [Exception] exception
726
+ # @return [Boolean]
727
+ def timeout_error?(exception)
728
+ !!defined?(Timeout::Error) && exception.is_a?(Timeout::Error)
729
+ end
730
+
731
+ # @private
732
+ # @param [Object] exception
733
+ # @return [(Integer, String, Object)]
734
+ def classify_gem_error(exception)
735
+ data = { gem: nil }
736
+ data[:gem] = exception.path if exception.respond_to?(:path) && exception.path
737
+ [ERROR_CODES[:gem_not_found], "#{exception.class}: #{exception.message}", data]
738
+ end
739
+
740
+ # @private
741
+ # @param [Object] exception
742
+ # @param [Hash<String, Object>] params
743
+ # @return [(Integer, String, Object)]
744
+ def classify_syntax_err(exception, params)
745
+ file = (params['file'] if params.is_a?(Hash)).to_s
746
+ line = if exception.respond_to?(:line)
747
+ exception.line
748
+ elsif exception.respond_to?(:diagnostic)
749
+ exception.diagnostic.location.line
750
+ end
751
+ data = { file: file, detail: exception.message, line: line }.compact
752
+ [ERROR_CODES[:syntax_error], "Syntax error in #{file}", data]
753
+ end
754
+
755
+ # @private
756
+ # @param [Object] exception
757
+ # @param [Hash<String, Object>] params
758
+ # @return [(Integer, String, Object)]
759
+ def classify_timeout_err(exception, params)
760
+ file = (params['file'] if params.is_a?(Hash)).to_s
761
+ data = { timeout: @idle_timeout || 30, file: file }
762
+ [ERROR_CODES[:timeout], "#{exception.class}: #{exception.message}", data]
763
+ end
764
+
765
+ # @private
766
+ # @param [Object] exception
767
+ # @return [(Integer, String, Object)]
768
+ def classify_internal_err(exception)
769
+ backtrace = exception.backtrace&.first(5) || []
770
+ data = { backtrace: backtrace }
771
+ [ERROR_CODES[:internal], "#{exception.class}: #{exception.message}", data]
772
+ end
773
+
774
+ # @private
775
+ # @param [UNIXSocket] client
776
+ # @param [String, Integer] id
777
+ # @param [Object] exception
778
+ # @param [String] file
779
+ # @raise [StandardError]
780
+ # @return [void]
781
+ def handle_request_error(client, id, exception, file)
782
+ if exception.is_a?(Docscribe::ParseError) ||
783
+ (defined?(Parser::SyntaxError) && exception.is_a?(Parser::SyntaxError))
784
+ send_syntax_error(client, id, exception, file)
785
+ else
786
+ raise
787
+ end
788
+ end
789
+
790
+ # @private
791
+ # @param [UNIXSocket] client
792
+ # @param [String, Integer] id
793
+ # @param [Object] exception
794
+ # @param [String] file
795
+ # @return [void]
796
+ def send_syntax_error(client, id, exception, file)
797
+ line = if exception.respond_to?(:line)
798
+ exception.line
799
+ elsif exception.respond_to?(:diagnostic)
800
+ exception.diagnostic.location.line
801
+ end
802
+ data = { file: file, detail: exception.message, line: line }.compact
803
+ send_error(client, id, ERROR_CODES[:syntax_error], "Syntax error in #{file}", data)
804
+ end
805
+
581
806
  # @private
582
807
  # @param [UNIXSocket] client
583
808
  # @param [String, Integer, nil] id
584
809
  # @param [Integer] code
585
810
  # @param [String] message
811
+ # @param [Object?] data optional structured error data
586
812
  # @return [void]
587
- def send_error(client, id, code, message)
588
- response = { jsonrpc: '2.0', id: id, error: { code: code, message: message } }
813
+ def send_error(client, id, code, message, data = nil)
814
+ error = { code: code, message: message }
815
+ error[:data] = data if data
816
+ response = { jsonrpc: '2.0', id: id, error: error }
589
817
  client.write(Protocol.serialize(response))
590
818
  end
591
819
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
- VERSION = '1.5.1'
4
+ VERSION = '1.5.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docscribe
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - unurgunite
@@ -230,7 +230,7 @@ metadata:
230
230
  changelog_uri: https://github.com/unurgunite/docscribe/blob/master/CHANGELOG.md
231
231
  rubygems_mfa_required: 'true'
232
232
  post_install_message: |
233
- You installed docscribe 1.5.1. Your future self (and your team) thank you.
233
+ You installed docscribe 1.5.2. Your future self (and your team) thank you.
234
234
 
235
235
  $ docscribe --help
236
236
 
@@ -250,7 +250,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
250
  - !ruby/object:Gem::Version
251
251
  version: '0'
252
252
  requirements: []
253
- rubygems_version: 4.0.13
253
+ rubygems_version: 4.0.15
254
254
  specification_version: 4
255
255
  summary: Auto-generate inline YARD documentation for Ruby by analyzing code AST. Supports
256
256
  RBS and Sorbet type signatures.