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 +4 -4
- data/README.md +56 -1
- data/exe/docscribe-client +49 -1
- data/lib/docscribe/cli/rbs_gen.rb +1 -1
- data/lib/docscribe/cli/sigs.rb +1 -1
- data/lib/docscribe/infer/params.rb +1 -1
- data/lib/docscribe/infer/returns.rb +1 -1
- data/lib/docscribe/inline_rewriter/doc_builder.rb +1 -1
- data/lib/docscribe/server.rb +257 -29
- data/lib/docscribe/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65ef814f594285faee2123170710fc06c47e3cf45471e79d4626fc2cb4885183
|
|
4
|
+
data.tar.gz: '008dd20fd7a7c3a6c9254be9ac8158005a4a84779d070939a7abdad0476f6df9'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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+**
|
|
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
|
|
170
|
+
rescue Parser::SyntaxError => e
|
|
171
171
|
warn "Syntax error in #{path}: #{e.message}"
|
|
172
172
|
false
|
|
173
173
|
rescue StandardError => e
|
data/lib/docscribe/cli/sigs.rb
CHANGED
|
@@ -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
|
|
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}"
|
|
@@ -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
|
|
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
|
|
597
|
+
[return_type, desc && desc.empty? ? nil : desc]
|
|
598
598
|
end
|
|
599
599
|
|
|
600
600
|
# Extract all comment tags from line
|
data/lib/docscribe/server.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
|
data/lib/docscribe/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|