docscribe 1.5.1 → 1.6.0
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 +197 -75
- data/exe/docscribe-client +51 -1
- data/lib/docscribe/cli/config_dump.rb +70 -0
- data/lib/docscribe/cli/coverage.rb +313 -0
- data/lib/docscribe/cli/init.rb +81 -4
- data/lib/docscribe/cli/options.rb +16 -0
- data/lib/docscribe/cli/rbs_gen.rb +3 -2
- data/lib/docscribe/cli/run.rb +134 -0
- data/lib/docscribe/cli/sigs.rb +1 -1
- data/lib/docscribe/cli.rb +7 -5
- data/lib/docscribe/config.rb +10 -0
- data/lib/docscribe/infer/behavior.rb +96 -0
- data/lib/docscribe/infer/params.rb +2 -4
- data/lib/docscribe/infer/returns.rb +1 -1
- data/lib/docscribe/infer.rb +35 -0
- data/lib/docscribe/inline_rewriter/doc_builder.rb +23 -10
- data/lib/docscribe/inline_rewriter.rb +142 -20
- data/lib/docscribe/server.rb +258 -29
- data/lib/docscribe/types/overload_selector.rb +79 -0
- data/lib/docscribe/types/provider_chain.rb +25 -2
- data/lib/docscribe/types/signature.rb +5 -0
- data/lib/docscribe/version.rb +1 -1
- metadata +7 -3
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,15 +630,29 @@ 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
|
-
result = Docscribe::InlineRewriter.rewrite_with_report(src, strategy: strategy, config: config,
|
|
654
|
+
result = Docscribe::InlineRewriter.rewrite_with_report(src, strategy: strategy, config: config,
|
|
655
|
+
core_rbs_provider: rbs, file: file)
|
|
537
656
|
@file_cache[key] = { mtime: mtime, src: src, result: result }
|
|
538
657
|
[src, result]
|
|
539
658
|
end
|
|
@@ -578,14 +697,124 @@ module Docscribe
|
|
|
578
697
|
client.write(Protocol.serialize(response))
|
|
579
698
|
end
|
|
580
699
|
|
|
700
|
+
# @private
|
|
701
|
+
# @param [Exception] exception
|
|
702
|
+
# @param [String?] _method_name
|
|
703
|
+
# @param [Hash<String, Object>] params
|
|
704
|
+
# @return [(Integer, String, Object?)]
|
|
705
|
+
def classify_error(exception, _method_name = nil, params = {})
|
|
706
|
+
if exception.is_a?(LoadError) || exception.is_a?(Gem::LoadError)
|
|
707
|
+
classify_gem_error(exception)
|
|
708
|
+
elsif syntax_error?(exception)
|
|
709
|
+
classify_syntax_err(exception, params)
|
|
710
|
+
elsif timeout_error?(exception)
|
|
711
|
+
classify_timeout_err(exception, params)
|
|
712
|
+
else
|
|
713
|
+
classify_internal_err(exception)
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# @private
|
|
718
|
+
# @param [Exception] exception
|
|
719
|
+
# @return [Boolean]
|
|
720
|
+
def syntax_error?(exception)
|
|
721
|
+
exception.is_a?(Docscribe::ParseError) ||
|
|
722
|
+
(defined?(Parser::SyntaxError) && exception.is_a?(Parser::SyntaxError))
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# @private
|
|
726
|
+
# @param [Exception] exception
|
|
727
|
+
# @return [Boolean]
|
|
728
|
+
def timeout_error?(exception)
|
|
729
|
+
!!defined?(Timeout::Error) && exception.is_a?(Timeout::Error)
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# @private
|
|
733
|
+
# @param [Object] exception
|
|
734
|
+
# @return [(Integer, String, Object)]
|
|
735
|
+
def classify_gem_error(exception)
|
|
736
|
+
data = { gem: nil }
|
|
737
|
+
data[:gem] = exception.path if exception.respond_to?(:path) && exception.path
|
|
738
|
+
[ERROR_CODES[:gem_not_found], "#{exception.class}: #{exception.message}", data]
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# @private
|
|
742
|
+
# @param [Object] exception
|
|
743
|
+
# @param [Hash<String, Object>] params
|
|
744
|
+
# @return [(Integer, String, Object)]
|
|
745
|
+
def classify_syntax_err(exception, params)
|
|
746
|
+
file = (params['file'] if params.is_a?(Hash)).to_s
|
|
747
|
+
line = if exception.respond_to?(:line)
|
|
748
|
+
exception.line
|
|
749
|
+
elsif exception.respond_to?(:diagnostic)
|
|
750
|
+
exception.diagnostic.location.line
|
|
751
|
+
end
|
|
752
|
+
data = { file: file, detail: exception.message, line: line }.compact
|
|
753
|
+
[ERROR_CODES[:syntax_error], "Syntax error in #{file}", data]
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# @private
|
|
757
|
+
# @param [Object] exception
|
|
758
|
+
# @param [Hash<String, Object>] params
|
|
759
|
+
# @return [(Integer, String, Object)]
|
|
760
|
+
def classify_timeout_err(exception, params)
|
|
761
|
+
file = (params['file'] if params.is_a?(Hash)).to_s
|
|
762
|
+
data = { timeout: @idle_timeout || 30, file: file }
|
|
763
|
+
[ERROR_CODES[:timeout], "#{exception.class}: #{exception.message}", data]
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# @private
|
|
767
|
+
# @param [Object] exception
|
|
768
|
+
# @return [(Integer, String, Object)]
|
|
769
|
+
def classify_internal_err(exception)
|
|
770
|
+
backtrace = exception.backtrace&.first(5) || []
|
|
771
|
+
data = { backtrace: backtrace }
|
|
772
|
+
[ERROR_CODES[:internal], "#{exception.class}: #{exception.message}", data]
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# @private
|
|
776
|
+
# @param [UNIXSocket] client
|
|
777
|
+
# @param [String, Integer] id
|
|
778
|
+
# @param [Object] exception
|
|
779
|
+
# @param [String] file
|
|
780
|
+
# @raise [StandardError]
|
|
781
|
+
# @return [void]
|
|
782
|
+
def handle_request_error(client, id, exception, file)
|
|
783
|
+
if exception.is_a?(Docscribe::ParseError) ||
|
|
784
|
+
(defined?(Parser::SyntaxError) && exception.is_a?(Parser::SyntaxError))
|
|
785
|
+
send_syntax_error(client, id, exception, file)
|
|
786
|
+
else
|
|
787
|
+
raise
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# @private
|
|
792
|
+
# @param [UNIXSocket] client
|
|
793
|
+
# @param [String, Integer] id
|
|
794
|
+
# @param [Object] exception
|
|
795
|
+
# @param [String] file
|
|
796
|
+
# @return [void]
|
|
797
|
+
def send_syntax_error(client, id, exception, file)
|
|
798
|
+
line = if exception.respond_to?(:line)
|
|
799
|
+
exception.line
|
|
800
|
+
elsif exception.respond_to?(:diagnostic)
|
|
801
|
+
exception.diagnostic.location.line
|
|
802
|
+
end
|
|
803
|
+
data = { file: file, detail: exception.message, line: line }.compact
|
|
804
|
+
send_error(client, id, ERROR_CODES[:syntax_error], "Syntax error in #{file}", data)
|
|
805
|
+
end
|
|
806
|
+
|
|
581
807
|
# @private
|
|
582
808
|
# @param [UNIXSocket] client
|
|
583
809
|
# @param [String, Integer, nil] id
|
|
584
810
|
# @param [Integer] code
|
|
585
811
|
# @param [String] message
|
|
812
|
+
# @param [Object?] data optional structured error data
|
|
586
813
|
# @return [void]
|
|
587
|
-
def send_error(client, id, code, message)
|
|
588
|
-
|
|
814
|
+
def send_error(client, id, code, message, data = nil)
|
|
815
|
+
error = { code: code, message: message }
|
|
816
|
+
error[:data] = data if data
|
|
817
|
+
response = { jsonrpc: '2.0', id: id, error: error }
|
|
589
818
|
client.write(Protocol.serialize(response))
|
|
590
819
|
end
|
|
591
820
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docscribe
|
|
4
|
+
module Types
|
|
5
|
+
# Selects best matching overload from RBS signatures for given arguments.
|
|
6
|
+
module OverloadSelector
|
|
7
|
+
class << self
|
|
8
|
+
# @param [Array<Object>] overloads
|
|
9
|
+
# @param [Integer] arg_count
|
|
10
|
+
# @param [Array<String>] param_names
|
|
11
|
+
# @return [Object?]
|
|
12
|
+
def select(overloads, arg_count:, param_names: [])
|
|
13
|
+
return nil if overloads.nil? || overloads.empty?
|
|
14
|
+
return overloads.first if overloads.size == 1
|
|
15
|
+
|
|
16
|
+
best = best_match(overloads, arg_count, param_names)
|
|
17
|
+
best&.first || overloads.first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param [Array<Object>] overloads
|
|
21
|
+
# @param [Integer] arg_count
|
|
22
|
+
# @param [Array<String>] param_names
|
|
23
|
+
# @return [(Object, Integer)?]
|
|
24
|
+
def best_match(overloads, arg_count, param_names)
|
|
25
|
+
candidates = overloads.map { |sig| score_signature(sig, arg_count: arg_count, param_names: param_names) }
|
|
26
|
+
candidates.compact.max_by { |_sig, score| score }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @private
|
|
32
|
+
# @param [Object] sig
|
|
33
|
+
# @param [Integer] arg_count
|
|
34
|
+
# @param [Array<String>] param_names
|
|
35
|
+
# @return [(Object, Integer)?]
|
|
36
|
+
def score_signature(sig, arg_count:, param_names:)
|
|
37
|
+
score = 0
|
|
38
|
+
|
|
39
|
+
pos_count = sig.positional_types&.length.to_i
|
|
40
|
+
return nil if pos_count > arg_count
|
|
41
|
+
|
|
42
|
+
score += score_positional(pos_count, arg_count, sig)
|
|
43
|
+
score += score_params(sig, param_names)
|
|
44
|
+
score += 3 if sig.return_type && !sig.return_type.empty?
|
|
45
|
+
score += 1 if sig.return_type && sig.return_type != 'Object'
|
|
46
|
+
|
|
47
|
+
[sig, score]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @private
|
|
51
|
+
# @param [Integer] pos_count
|
|
52
|
+
# @param [Integer] arg_count
|
|
53
|
+
# @param [Object] sig
|
|
54
|
+
# @return [Integer]
|
|
55
|
+
def score_positional(pos_count, arg_count, sig)
|
|
56
|
+
if pos_count == arg_count
|
|
57
|
+
10
|
|
58
|
+
elsif pos_count < arg_count && sig.rest_positional
|
|
59
|
+
5
|
|
60
|
+
else
|
|
61
|
+
0
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @private
|
|
66
|
+
# @param [Object] sig
|
|
67
|
+
# @param [Array<String>] param_names
|
|
68
|
+
# @return [Integer]
|
|
69
|
+
def score_params(sig, param_names)
|
|
70
|
+
if sig.param_types
|
|
71
|
+
(sig.param_types.keys & param_names).length * 2
|
|
72
|
+
else
|
|
73
|
+
0
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'overload_selector'
|
|
4
|
+
|
|
3
5
|
module Docscribe
|
|
4
6
|
module Types
|
|
5
7
|
# Resolve method signatures by querying a list of providers in order.
|
|
@@ -22,18 +24,39 @@ module Docscribe
|
|
|
22
24
|
|
|
23
25
|
# Resolve a method signature from the first provider that can supply it.
|
|
24
26
|
#
|
|
27
|
+
# When overloads are present, selects the best-matching signature.
|
|
28
|
+
#
|
|
25
29
|
# @param [String] container e.g. "MyModule::MyClass"
|
|
26
30
|
# @param [Symbol] scope :instance or :class
|
|
27
31
|
# @param [Symbol, String] name method name
|
|
32
|
+
# @param [Integer?] param_count number of actual arguments
|
|
33
|
+
# @param [Array<String>] param_names actual parameter names
|
|
28
34
|
# @return [Docscribe::Types::MethodSignature, nil]
|
|
29
|
-
def signature_for(container:, scope:, name:)
|
|
35
|
+
def signature_for(container:, scope:, name:, param_count: nil, param_names: [])
|
|
30
36
|
@providers.each do |provider|
|
|
31
37
|
sig = provider.signature_for(container: container, scope: scope, name: name)
|
|
32
|
-
|
|
38
|
+
next unless sig
|
|
39
|
+
|
|
40
|
+
return sig unless sig.overloads&.any?
|
|
41
|
+
|
|
42
|
+
best = select_overload(sig, param_count, param_names)
|
|
43
|
+
return best if best
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
nil
|
|
36
47
|
end
|
|
48
|
+
|
|
49
|
+
# @param [Docscribe::Types::MethodSignature] sig
|
|
50
|
+
# @param [Integer?] param_count
|
|
51
|
+
# @param [Array<String>] param_names
|
|
52
|
+
# @return [Docscribe::Types::MethodSignature, nil]
|
|
53
|
+
def select_overload(sig, param_count, param_names)
|
|
54
|
+
OverloadSelector.select(
|
|
55
|
+
[sig, *sig.overloads],
|
|
56
|
+
arg_count: param_count || 0,
|
|
57
|
+
param_names: param_names
|
|
58
|
+
)
|
|
59
|
+
end
|
|
37
60
|
end
|
|
38
61
|
end
|
|
39
62
|
end
|
|
@@ -21,7 +21,12 @@ module Docscribe
|
|
|
21
21
|
# @!attribute [rw] rest_keywords
|
|
22
22
|
# @return [Docscribe::Types::RestKeywords, nil]
|
|
23
23
|
# @param [Docscribe::Types::RestKeywords, nil] value
|
|
24
|
+
#
|
|
25
|
+
# @!attribute [rw] overloads
|
|
26
|
+
# @return [Array<Docscribe::Types::MethodSignature>, nil]
|
|
27
|
+
# @param [Array<Docscribe::Types::MethodSignature>, nil] value
|
|
24
28
|
MethodSignature = Struct.new(:return_type, :param_types, :positional_types, :rest_positional, :rest_keywords,
|
|
29
|
+
:overloads,
|
|
25
30
|
keyword_init: true)
|
|
26
31
|
|
|
27
32
|
# @!attribute [rw] name
|
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.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- unurgunite
|
|
@@ -163,6 +163,8 @@ files:
|
|
|
163
163
|
- lib/docscribe/cli.rb
|
|
164
164
|
- lib/docscribe/cli/check_for_comments.rb
|
|
165
165
|
- lib/docscribe/cli/config_builder.rb
|
|
166
|
+
- lib/docscribe/cli/config_dump.rb
|
|
167
|
+
- lib/docscribe/cli/coverage.rb
|
|
166
168
|
- lib/docscribe/cli/formatters.rb
|
|
167
169
|
- lib/docscribe/cli/formatters/json.rb
|
|
168
170
|
- lib/docscribe/cli/formatters/sarif.rb
|
|
@@ -188,6 +190,7 @@ files:
|
|
|
188
190
|
- lib/docscribe/config/utils.rb
|
|
189
191
|
- lib/docscribe/infer.rb
|
|
190
192
|
- lib/docscribe/infer/ast_walk.rb
|
|
193
|
+
- lib/docscribe/infer/behavior.rb
|
|
191
194
|
- lib/docscribe/infer/constants.rb
|
|
192
195
|
- lib/docscribe/infer/literals.rb
|
|
193
196
|
- lib/docscribe/infer/names.rb
|
|
@@ -209,6 +212,7 @@ files:
|
|
|
209
212
|
- lib/docscribe/plugin/registry.rb
|
|
210
213
|
- lib/docscribe/plugin/tag.rb
|
|
211
214
|
- lib/docscribe/server.rb
|
|
215
|
+
- lib/docscribe/types/overload_selector.rb
|
|
212
216
|
- lib/docscribe/types/provider_chain.rb
|
|
213
217
|
- lib/docscribe/types/rbs/collection_loader.rb
|
|
214
218
|
- lib/docscribe/types/rbs/provider.rb
|
|
@@ -230,7 +234,7 @@ metadata:
|
|
|
230
234
|
changelog_uri: https://github.com/unurgunite/docscribe/blob/master/CHANGELOG.md
|
|
231
235
|
rubygems_mfa_required: 'true'
|
|
232
236
|
post_install_message: |
|
|
233
|
-
You installed docscribe 1.
|
|
237
|
+
You installed docscribe 1.6.0. Your future self (and your team) thank you.
|
|
234
238
|
|
|
235
239
|
$ docscribe --help
|
|
236
240
|
|
|
@@ -250,7 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
250
254
|
- !ruby/object:Gem::Version
|
|
251
255
|
version: '0'
|
|
252
256
|
requirements: []
|
|
253
|
-
rubygems_version: 4.0.
|
|
257
|
+
rubygems_version: 4.0.15
|
|
254
258
|
specification_version: 4
|
|
255
259
|
summary: Auto-generate inline YARD documentation for Ruby by analyzing code AST. Supports
|
|
256
260
|
RBS and Sorbet type signatures.
|