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.
- 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 +46 -15
- data/lib/docscribe/server/client.rb +20 -11
- data/lib/docscribe/server/daemon.rb +200 -23
- data/lib/docscribe/server/protocol.rb +4 -4
- 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 +8 -3
|
@@ -27,7 +27,7 @@ module Docscribe
|
|
|
27
27
|
class << self
|
|
28
28
|
# Start the server daemon if not running.
|
|
29
29
|
#
|
|
30
|
-
# @param [String
|
|
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
|
|
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
|
|
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
|
|
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
|
|
125
|
+
# @param [String?] config_path
|
|
126
126
|
# @raise [StandardError]
|
|
127
|
-
# @return [Integer
|
|
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
|
|
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
|
|
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
|
|
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
|
|
202
|
-
|
|
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
|
|
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
|
|
10
|
-
# @param [String
|
|
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 [
|
|
21
|
-
# @return [Hash<String, Object
|
|
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 [
|
|
31
|
-
# @return [Hash<String, Object
|
|
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
|
|
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
|
|
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 [
|
|
57
|
-
# @return [Hash<String, Object
|
|
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 [
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
|
312
|
-
# @param [Array<
|
|
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
|
|
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
|
|
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,
|
|
17
|
-
# @return [Hash<Symbol,
|
|
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
|
|
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 [
|
|
43
|
+
# @param [Object] hash
|
|
44
44
|
# @return [String]
|
|
45
45
|
def serialize(hash)
|
|
46
46
|
"#{JSON.generate(hash)}\n"
|