docscribe 1.6.0 → 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.
- checksums.yaml +4 -4
- data/README.md +76 -193
- data/exe/docscribe-client +26 -7
- data/lib/docscribe/cli/config_builder.rb +37 -2
- data/lib/docscribe/cli/coverage.rb +5 -5
- data/lib/docscribe/cli/formatters/json.rb +74 -29
- data/lib/docscribe/cli/formatters/sarif.rb +20 -3
- data/lib/docscribe/cli/options.rb +17 -2
- data/lib/docscribe/cli/rbs_gen.rb +4 -4
- data/lib/docscribe/cli/run.rb +107 -24
- data/lib/docscribe/cli/update_types.rb +61 -17
- data/lib/docscribe/cli.rb +19 -13
- data/lib/docscribe/config/defaults.rb +1 -0
- data/lib/docscribe/config/rbs.rb +22 -1
- data/lib/docscribe/config/template.rb +3 -0
- data/lib/docscribe/config/validation.rb +19 -0
- data/lib/docscribe/config.rb +1 -0
- data/lib/docscribe/infer/behavior.rb +13 -13
- data/lib/docscribe/infer/params.rb +2 -2
- data/lib/docscribe/infer/raises.rb +5 -6
- data/lib/docscribe/infer/returns.rb +1612 -151
- data/lib/docscribe/infer.rb +7 -7
- data/lib/docscribe/inline_rewriter/doc_builder.rb +485 -102
- data/lib/docscribe/inline_rewriter.rb +263 -97
- data/lib/docscribe/plugin/registry.rb +1 -0
- data/lib/docscribe/server/base.rb +255 -0
- data/lib/docscribe/server/client.rb +95 -0
- data/lib/docscribe/server/daemon.rb +678 -0
- data/lib/docscribe/server/protocol.rb +50 -0
- data/lib/docscribe/server.rb +4 -835
- data/lib/docscribe/types/primitive.rb +160 -0
- data/lib/docscribe/types/sorbet/base_provider.rb +33 -1
- data/lib/docscribe/types/yard/formatter.rb +35 -6
- data/lib/docscribe/types/yard/parser.rb +25 -20
- data/lib/docscribe/types/yard/validator.rb +131 -0
- data/lib/docscribe/validator/generic_compatibility.rb +698 -0
- data/lib/docscribe/validator/type_mismatch_validator.rb +287 -0
- data/lib/docscribe/version.rb +1 -1
- metadata +12 -3
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'timeout'
|
|
7
|
+
require 'digest/md5'
|
|
8
|
+
require_relative '../lru_cache'
|
|
9
|
+
|
|
10
|
+
module Docscribe
|
|
11
|
+
module Server
|
|
12
|
+
# Daemon process that loads the Ruby runtime once and serves requests.
|
|
13
|
+
class Daemon
|
|
14
|
+
# Standardized JSON-RPC error codes.
|
|
15
|
+
ERROR_CODES = {
|
|
16
|
+
gem_not_found: -32_000,
|
|
17
|
+
syntax_error: -32_001,
|
|
18
|
+
config_load_failure: -32_002,
|
|
19
|
+
timeout: -32_010,
|
|
20
|
+
internal: -32_099
|
|
21
|
+
}.freeze
|
|
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
|
|
31
|
+
# @param [Integer] idle_timeout seconds before automatic shutdown
|
|
32
|
+
# @param [String?] config_path custom config path
|
|
33
|
+
# @return [void]
|
|
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)
|
|
45
|
+
@socket_path = socket_path || Server.socket_path(config_path)
|
|
46
|
+
@config_path = config_path
|
|
47
|
+
@idle_timeout = idle_timeout
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [void]
|
|
51
|
+
def setup_runtime_state
|
|
52
|
+
@last_request_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
53
|
+
@running = false
|
|
54
|
+
@server = nil
|
|
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
|
|
62
|
+
@cache_mutex = Mutex.new
|
|
63
|
+
@config_mutex = Mutex.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Start the daemon: load dependencies, bind socket, enter listen loop.
|
|
67
|
+
#
|
|
68
|
+
# @return [void]
|
|
69
|
+
def start
|
|
70
|
+
load_dependencies
|
|
71
|
+
setup_socket
|
|
72
|
+
@running = true
|
|
73
|
+
$PROGRAM_NAME = "docscribe server (#{Dir.pwd})"
|
|
74
|
+
write_pid
|
|
75
|
+
listen_loop
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Load the full Docscribe runtime and build cached config.
|
|
81
|
+
#
|
|
82
|
+
# @private
|
|
83
|
+
# @return [void]
|
|
84
|
+
def load_dependencies
|
|
85
|
+
require 'docscribe'
|
|
86
|
+
@config = Docscribe::Config.load(@config_path)
|
|
87
|
+
@config&.load_plugins!
|
|
88
|
+
@core_rbs_provider = @config&.core_rbs_provider
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Create and bind the Unix domain socket.
|
|
92
|
+
#
|
|
93
|
+
# @private
|
|
94
|
+
# @return [void]
|
|
95
|
+
def setup_socket
|
|
96
|
+
FileUtils.rm_f(@socket_path)
|
|
97
|
+
FileUtils.mkdir_p(File.dirname(@socket_path))
|
|
98
|
+
@server = UNIXServer.new(@socket_path)
|
|
99
|
+
File.chmod(0o600, @socket_path)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @private
|
|
103
|
+
# @return [void]
|
|
104
|
+
def write_pid
|
|
105
|
+
File.write("#{@socket_path}.pid", Process.pid)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Main accept loop with idle timeout check.
|
|
109
|
+
#
|
|
110
|
+
# @private
|
|
111
|
+
# @raise [Interrupt]
|
|
112
|
+
# @return [void]
|
|
113
|
+
def listen_loop
|
|
114
|
+
while @running
|
|
115
|
+
check_idle_timeout
|
|
116
|
+
accept_client
|
|
117
|
+
end
|
|
118
|
+
rescue Interrupt
|
|
119
|
+
@running = false
|
|
120
|
+
ensure
|
|
121
|
+
cleanup
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check whether the idle timeout has been exceeded.
|
|
125
|
+
#
|
|
126
|
+
# @private
|
|
127
|
+
# @return [void]
|
|
128
|
+
def check_idle_timeout
|
|
129
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_request_time
|
|
130
|
+
@running = false if elapsed > @idle_timeout
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Accept a client connection if one is available.
|
|
134
|
+
# Spawns a thread to handle each client concurrently.
|
|
135
|
+
#
|
|
136
|
+
# @private
|
|
137
|
+
# @return [void]
|
|
138
|
+
def accept_client
|
|
139
|
+
client = @server&.accept if @server&.wait_readable(0.1)
|
|
140
|
+
return unless client
|
|
141
|
+
|
|
142
|
+
@last_request_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
143
|
+
Thread.new(client) { |conn| handle_client(conn) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Read a request from a client connection and dispatch it.
|
|
147
|
+
#
|
|
148
|
+
# @private
|
|
149
|
+
# @param [UNIXSocket] client connected client socket
|
|
150
|
+
# @raise [StandardError]
|
|
151
|
+
# @return [void]
|
|
152
|
+
def handle_client(client)
|
|
153
|
+
request_line = client.gets or return
|
|
154
|
+
request = Protocol.parse_response(request_line)
|
|
155
|
+
request ? handle_request(client, request) : send_error(client, nil, -32_700, 'Parse error')
|
|
156
|
+
rescue StandardError => e
|
|
157
|
+
method_name = request&.dig('method')
|
|
158
|
+
error_params = request&.dig('params') || {}
|
|
159
|
+
code, message, data = classify_error(e, method_name, error_params)
|
|
160
|
+
send_error(client, request&.dig('id'), code, message, data)
|
|
161
|
+
ensure
|
|
162
|
+
client.close
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Dispatch a parsed request to the appropriate handler.
|
|
166
|
+
#
|
|
167
|
+
# @private
|
|
168
|
+
# @param [UNIXSocket] client connected client socket
|
|
169
|
+
# @param [Hash<String, Object>] request parsed JSON-RPC request
|
|
170
|
+
# @return [void]
|
|
171
|
+
def handle_request(client, request)
|
|
172
|
+
method = request['method']
|
|
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
|
|
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)
|
|
196
|
+
case method
|
|
197
|
+
when 'shutdown' then handle_shutdown(client, request['id'])
|
|
198
|
+
when 'ping' then handle_ping(client, request['id'])
|
|
199
|
+
else send_error(client, request['id'], -32_601, "Unknown method: #{method}")
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @private
|
|
204
|
+
# @param [UNIXSocket] client
|
|
205
|
+
# @param [String, Integer] id
|
|
206
|
+
# @param [Hash<String, Object>] params
|
|
207
|
+
# @raise [StandardError]
|
|
208
|
+
# @return [void]
|
|
209
|
+
# @return [void] if StandardError
|
|
210
|
+
def handle_check(client, id, params)
|
|
211
|
+
file = params['file']
|
|
212
|
+
strategy = (params['strategy'] || 'safe').to_sym
|
|
213
|
+
return send_error(client, id, -32_602, "File not found: #{file}") unless file && File.file?(file)
|
|
214
|
+
|
|
215
|
+
apply_cli_overrides(params['cli_overrides'])
|
|
216
|
+
src, result = rewrite_file(file, strategy)
|
|
217
|
+
send_result(client, id, 'status' => result[:output] == src ? 'ok' : 'fail',
|
|
218
|
+
'changed' => result[:output] != src, 'changes' => result[:changes])
|
|
219
|
+
rescue StandardError => e
|
|
220
|
+
handle_request_error(client, id, e, file)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @private
|
|
224
|
+
# @param [UNIXSocket] client
|
|
225
|
+
# @param [String, Integer] id
|
|
226
|
+
# @param [Hash<String, Object>] params
|
|
227
|
+
# @raise [StandardError]
|
|
228
|
+
# @return [void]
|
|
229
|
+
# @return [void] if StandardError
|
|
230
|
+
def handle_fix(client, id, params)
|
|
231
|
+
file = params['file']
|
|
232
|
+
strategy = (params['strategy'] || 'safe').to_sym
|
|
233
|
+
return send_error(client, id, -32_602, "File not found: #{file}") unless file && File.file?(file)
|
|
234
|
+
|
|
235
|
+
apply_cli_overrides(params['cli_overrides'])
|
|
236
|
+
src, result = rewrite_file(file, strategy)
|
|
237
|
+
changed = result[:output] != src
|
|
238
|
+
File.write(file, result[:output]) if changed
|
|
239
|
+
send_result(client, id, 'status' => 'ok', 'changed' => changed, 'changes' => result[:changes])
|
|
240
|
+
rescue StandardError => e
|
|
241
|
+
handle_request_error(client, id, e, file)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @private
|
|
245
|
+
# @param [UNIXSocket] client
|
|
246
|
+
# @param [String, Integer] id
|
|
247
|
+
# @param [Hash<String, Object>] params
|
|
248
|
+
# @return [void]
|
|
249
|
+
def handle_check_batch(client, id, params)
|
|
250
|
+
files = params['files']
|
|
251
|
+
return send_error(client, id, -32_602, 'Missing files parameter') unless files.is_a?(Array) && !files.empty?
|
|
252
|
+
|
|
253
|
+
strategy = (params['strategy'] || 'safe').to_sym
|
|
254
|
+
timeout = params['timeout']
|
|
255
|
+
|
|
256
|
+
apply_cli_overrides(params['cli_overrides'])
|
|
257
|
+
|
|
258
|
+
results = files.map do |file|
|
|
259
|
+
process_file_in_batch(file, strategy, timeout)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
send_result(client, id, { 'results' => results })
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @private
|
|
266
|
+
# @param [String] file
|
|
267
|
+
# @param [Symbol] strategy
|
|
268
|
+
# @param [Integer, Float?] timeout
|
|
269
|
+
# @raise [Timeout::Error]
|
|
270
|
+
# @raise [StandardError]
|
|
271
|
+
# @return [Hash<String, Object>]
|
|
272
|
+
# @return [Hash] if Timeout::Error
|
|
273
|
+
# @return [Hash] if StandardError
|
|
274
|
+
def process_file_in_batch(file, strategy, timeout = nil)
|
|
275
|
+
return { 'file' => file, 'status' => 'error', 'error' => "File not found: #{file}" } unless File.file?(file)
|
|
276
|
+
|
|
277
|
+
if timeout
|
|
278
|
+
Timeout.timeout(timeout.to_f) { run_rewrite(file, strategy) }
|
|
279
|
+
else
|
|
280
|
+
run_rewrite(file, strategy)
|
|
281
|
+
end
|
|
282
|
+
rescue Timeout::Error
|
|
283
|
+
{ 'file' => file, 'status' => 'error', 'error' => 'Timeout' }
|
|
284
|
+
rescue StandardError => e
|
|
285
|
+
{ 'file' => file, 'status' => 'error', 'error' => "#{e.class}: #{e.message}" }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @private
|
|
289
|
+
# @param [String] file
|
|
290
|
+
# @param [Symbol] strategy
|
|
291
|
+
# @return [Hash<String, Object>]
|
|
292
|
+
def run_rewrite(file, strategy)
|
|
293
|
+
src, result = rewrite_file(file, strategy)
|
|
294
|
+
changes = result[:changes].map { |c| c.transform_keys(&:to_s) }
|
|
295
|
+
{ 'file' => file, 'status' => result[:output] == src ? 'ok' : 'fail', 'changes' => changes }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# @private
|
|
299
|
+
# @param [Hash<String, Object>?] overrides
|
|
300
|
+
# @return [void]
|
|
301
|
+
def apply_cli_overrides(overrides) # rubocop:disable SortedMethodsByCall/Waterfall
|
|
302
|
+
@config_mutex.synchronize do
|
|
303
|
+
return reset_effective_config_internal if overrides.nil? || overrides.empty?
|
|
304
|
+
return if @applied_overrides == overrides
|
|
305
|
+
|
|
306
|
+
build_effective_config(overrides)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# @private
|
|
311
|
+
# @param [Hash<String, Object>] overrides
|
|
312
|
+
# @return [void]
|
|
313
|
+
def build_effective_config(overrides)
|
|
314
|
+
config = @config or return
|
|
315
|
+
require 'docscribe/cli/config_builder'
|
|
316
|
+
require 'docscribe/cli/options'
|
|
317
|
+
opts = Docscribe::CLI::Options::DEFAULT.merge(overrides.transform_keys(&:to_sym))
|
|
318
|
+
@effective_config = Docscribe::CLI::ConfigBuilder.build(config, opts)
|
|
319
|
+
@file_cache.clear
|
|
320
|
+
@applied_overrides = overrides
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# @private
|
|
324
|
+
# @return [void]
|
|
325
|
+
def reset_effective_config
|
|
326
|
+
@config_mutex.synchronize { reset_effective_config_internal }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# @private
|
|
330
|
+
# @return [void]
|
|
331
|
+
def reset_effective_config_internal
|
|
332
|
+
return unless @effective_config
|
|
333
|
+
|
|
334
|
+
@effective_config = nil
|
|
335
|
+
@applied_overrides = nil
|
|
336
|
+
@file_cache.clear
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# @private
|
|
340
|
+
# @param [String] file
|
|
341
|
+
# @param [Symbol] strategy
|
|
342
|
+
# @raise [StandardError]
|
|
343
|
+
# @return [(String, Hash<Symbol, String, Array<Hash<Symbol, Object>>>)]
|
|
344
|
+
def rewrite_file(file, strategy)
|
|
345
|
+
@cache_mutex.synchronize do
|
|
346
|
+
config = @effective_config || @config or raise 'Docscribe: config not loaded'
|
|
347
|
+
key = [file, strategy]
|
|
348
|
+
mtime = File.mtime(file)
|
|
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
|
|
353
|
+
|
|
354
|
+
rewrite_and_cache(file, strategy, config, key, mtime)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
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
|
+
|
|
395
|
+
# @private
|
|
396
|
+
# @param [String] file
|
|
397
|
+
# @param [Symbol] strategy
|
|
398
|
+
# @param [Docscribe::Config] config effective or base config
|
|
399
|
+
# @param [Array<String, Symbol>] key cache key
|
|
400
|
+
# @param [Time] mtime file modification time
|
|
401
|
+
# @return [(String, Hash<Symbol, String, Array<Hash<Symbol, Object>>>)]
|
|
402
|
+
def rewrite_and_cache(file, strategy, config, key, mtime)
|
|
403
|
+
src = File.read(file)
|
|
404
|
+
rbs = config.respond_to?(:core_rbs_provider) ? config.core_rbs_provider : nil
|
|
405
|
+
result = Docscribe::InlineRewriter.rewrite_with_report(src, strategy: strategy, config: config,
|
|
406
|
+
core_rbs_provider: rbs, file: file)
|
|
407
|
+
@file_cache[key] = { mtime: mtime, sig_hash: @last_sig_hash, src: src, result: result }
|
|
408
|
+
[src, result]
|
|
409
|
+
end
|
|
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
|
+
|
|
501
|
+
# Handle a shutdown request.
|
|
502
|
+
#
|
|
503
|
+
# @private
|
|
504
|
+
# @param [UNIXSocket] client connected client socket
|
|
505
|
+
# @param [String, Integer] id request ID
|
|
506
|
+
# @return [void]
|
|
507
|
+
def handle_shutdown(client, id)
|
|
508
|
+
send_result(client, id, { 'status' => 'shutting_down' })
|
|
509
|
+
@running = false
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Handle a ping request.
|
|
513
|
+
#
|
|
514
|
+
# @private
|
|
515
|
+
# @param [UNIXSocket] client connected client socket
|
|
516
|
+
# @param [String, Integer] id request ID
|
|
517
|
+
# @return [void]
|
|
518
|
+
def handle_ping(client, id)
|
|
519
|
+
uptime = (Time.now - @started_at).to_i
|
|
520
|
+
send_result(client, id, {
|
|
521
|
+
'version' => Docscribe::VERSION,
|
|
522
|
+
'pid' => Process.pid,
|
|
523
|
+
'socket_path' => @socket_path,
|
|
524
|
+
'started_at' => @started_at.iso8601,
|
|
525
|
+
'uptime' => uptime
|
|
526
|
+
})
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Send a JSON-RPC result response.
|
|
530
|
+
#
|
|
531
|
+
# @private
|
|
532
|
+
# @param [UNIXSocket] client connected client socket
|
|
533
|
+
# @param [String, Integer] id request ID
|
|
534
|
+
# @param [Hash<String, Object>] result result data
|
|
535
|
+
# @return [void]
|
|
536
|
+
def send_result(client, id, result)
|
|
537
|
+
response = { jsonrpc: '2.0', id: id, result: result }
|
|
538
|
+
client.write(Protocol.serialize(response))
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# @private
|
|
542
|
+
# @param [Exception] exception
|
|
543
|
+
# @param [String?] _method_name
|
|
544
|
+
# @param [Hash<String, Object>] params
|
|
545
|
+
# @return [(Integer, String, Hash<Symbol, Object, nil>, nil)]
|
|
546
|
+
def classify_error(exception, _method_name = nil, params = {})
|
|
547
|
+
if exception.is_a?(LoadError) || exception.is_a?(Gem::LoadError)
|
|
548
|
+
classify_gem_error(exception)
|
|
549
|
+
elsif syntax_error?(exception)
|
|
550
|
+
classify_syntax_err(exception, params)
|
|
551
|
+
elsif timeout_error?(exception)
|
|
552
|
+
classify_timeout_err(exception, params)
|
|
553
|
+
else
|
|
554
|
+
classify_internal_err(exception)
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# @private
|
|
559
|
+
# @param [Exception] exception
|
|
560
|
+
# @return [Boolean]
|
|
561
|
+
def syntax_error?(exception)
|
|
562
|
+
exception.is_a?(Docscribe::ParseError) ||
|
|
563
|
+
(defined?(Parser::SyntaxError) && exception.is_a?(Parser::SyntaxError))
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# @private
|
|
567
|
+
# @param [Exception] exception
|
|
568
|
+
# @return [Boolean]
|
|
569
|
+
def timeout_error?(exception)
|
|
570
|
+
!!defined?(Timeout::Error) && exception.is_a?(Timeout::Error)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# @private
|
|
574
|
+
# @param [Exception] exception
|
|
575
|
+
# @return [(Integer, String, Hash<Symbol, String, nil>)]
|
|
576
|
+
def classify_gem_error(exception)
|
|
577
|
+
data = { gem: nil }
|
|
578
|
+
data[:gem] = exception.path if exception.respond_to?(:path) && exception.path
|
|
579
|
+
[ERROR_CODES[:gem_not_found], "#{exception.class}: #{exception.message}", data]
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# @private
|
|
583
|
+
# @param [Exception] exception
|
|
584
|
+
# @param [Hash<String, Object>] params
|
|
585
|
+
# @return [(Integer, String, Hash<Symbol, Object>)]
|
|
586
|
+
def classify_syntax_err(exception, params)
|
|
587
|
+
file = (params['file'] if params.is_a?(Hash)).to_s
|
|
588
|
+
line = if exception.respond_to?(:line)
|
|
589
|
+
exception.line
|
|
590
|
+
elsif exception.respond_to?(:diagnostic)
|
|
591
|
+
exception.diagnostic.location.line
|
|
592
|
+
end
|
|
593
|
+
data = { file: file, detail: exception.message, line: line }.compact
|
|
594
|
+
[ERROR_CODES[:syntax_error], "Syntax error in #{file}", data]
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# @private
|
|
598
|
+
# @param [Exception] exception
|
|
599
|
+
# @param [Hash<String, Object>] params
|
|
600
|
+
# @return [(Integer, String, Hash<Symbol, Object>)]
|
|
601
|
+
def classify_timeout_err(exception, params)
|
|
602
|
+
file = (params['file'] if params.is_a?(Hash)).to_s
|
|
603
|
+
data = { timeout: @idle_timeout || 30, file: file }
|
|
604
|
+
[ERROR_CODES[:timeout], "#{exception.class}: #{exception.message}", data]
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# @private
|
|
608
|
+
# @param [Exception] exception
|
|
609
|
+
# @return [(Integer, String, Hash<Symbol, Object>)]
|
|
610
|
+
def classify_internal_err(exception)
|
|
611
|
+
backtrace = exception.backtrace&.first(5) || []
|
|
612
|
+
data = { backtrace: backtrace }
|
|
613
|
+
[ERROR_CODES[:internal], "#{exception.class}: #{exception.message}", data]
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# @private
|
|
617
|
+
# @param [UNIXSocket] client
|
|
618
|
+
# @param [String, Integer] id
|
|
619
|
+
# @param [Exception] exception
|
|
620
|
+
# @param [String] file
|
|
621
|
+
# @raise [StandardError]
|
|
622
|
+
# @return [void]
|
|
623
|
+
def handle_request_error(client, id, exception, file)
|
|
624
|
+
if exception.is_a?(Docscribe::ParseError) ||
|
|
625
|
+
(defined?(Parser::SyntaxError) && exception.is_a?(Parser::SyntaxError))
|
|
626
|
+
send_syntax_error(client, id, exception, file)
|
|
627
|
+
else
|
|
628
|
+
raise
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# @private
|
|
633
|
+
# @param [UNIXSocket] client
|
|
634
|
+
# @param [String, Integer] id
|
|
635
|
+
# @param [Exception] exception
|
|
636
|
+
# @param [String] file
|
|
637
|
+
# @return [void]
|
|
638
|
+
def send_syntax_error(client, id, exception, file)
|
|
639
|
+
line = if exception.respond_to?(:line)
|
|
640
|
+
exception.line
|
|
641
|
+
elsif exception.respond_to?(:diagnostic)
|
|
642
|
+
exception.diagnostic.location.line
|
|
643
|
+
end
|
|
644
|
+
data = { file: file, detail: exception.message, line: line }.compact
|
|
645
|
+
send_error(client, id, ERROR_CODES[:syntax_error], "Syntax error in #{file}", data)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# @private
|
|
649
|
+
# @param [UNIXSocket] client
|
|
650
|
+
# @param [String, Integer, nil] id
|
|
651
|
+
# @param [Integer] code
|
|
652
|
+
# @param [String] message
|
|
653
|
+
# @param [Hash<Symbol, Object>?] data optional structured error data
|
|
654
|
+
# @return [void]
|
|
655
|
+
def send_error(client, id, code, message, data = nil)
|
|
656
|
+
error = { code: code, message: message }
|
|
657
|
+
error[:data] = data if data
|
|
658
|
+
response = { jsonrpc: '2.0', id: id, error: error }
|
|
659
|
+
client.write(Protocol.serialize(response))
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Cleanup socket and PID files on shutdown.
|
|
663
|
+
#
|
|
664
|
+
# @private
|
|
665
|
+
# @raise [StandardError]
|
|
666
|
+
# @return [void]
|
|
667
|
+
# @return [nil] if StandardError
|
|
668
|
+
def cleanup
|
|
669
|
+
@server&.close
|
|
670
|
+
File.unlink(@socket_path) if @socket_path && File.exist?(@socket_path)
|
|
671
|
+
pid_path = "#{@socket_path}.pid"
|
|
672
|
+
FileUtils.rm_f(pid_path)
|
|
673
|
+
rescue StandardError
|
|
674
|
+
nil
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|