Module: Docscribe::CLI::Run

Defined in:
lib/docscribe/cli/run.rb

Overview

Execute Docscribe from parsed CLI options.

This module handles:

  • config loading and CLI overrides
  • stdin mode
  • file expansion / filtering
  • inspect vs write behavior
  • process exit status

Constant Summary collapse

INITIAL_RUN_STATE =
{
  changed: false,
  had_errors: false,
  checked_ok: 0,
  checked_fail: 0,
  corrected: 0,
  corrected_paths: [], #: Array[String]
  corrected_changes: {}, #: Hash[String, untyped]
  fail_paths: [], #: Array[String]
  fail_changes: {}, #: Hash[String, untyped]
  error_paths: [], #: Array[String]
  error_messages: {}, #: Hash[String, String]
  type_mismatch_paths: [], #: Array[String]
  type_mismatch_changes: {}, #: Hash[String, untyped]
  total: 0,
  processed: 0
}.freeze
CLI_OVERRIDE_KEYS =
%i[
  keep_descriptions no_boilerplate
  include exclude include_file exclude_file
  rbs rbs_collection sig_dirs
  sorbet rbi_dirs
].freeze

Class Method Summary collapse

Class Method Details

.all_fine?(state, checked_error, type_mismatch_count) ⇒ Boolean

Whether no failures, errors, or type mismatches occurred.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • checked_error (Integer)

    number of files with errors

  • type_mismatch_count (Integer)

    number of files with type mismatches

Returns:

  • (Boolean)


840
841
842
# File 'lib/docscribe/cli/run.rb', line 840

def all_fine?(state, checked_error, type_mismatch_count)
  state[:checked_fail].zero? && checked_error.zero? && type_mismatch_count.zero?
end

.append_expanded_path(files, path) ⇒ void

This method returns an undefined value.

Append a file or recursively expand a directory into the files array.

Parameters:

  • files (Array<String>)

    mutable file path accumulator

  • path (String)

    file or directory path to expand



360
361
362
363
364
365
366
367
368
# File 'lib/docscribe/cli/run.rb', line 360

def append_expanded_path(files, path)
  if File.directory?(path)
    files.concat(Dir.glob(File.join(path, '**', '*.rb')))
  elsif File.file?(path)
    files << path
  else
    warn "Skipping missing path: #{path}"
  end
end

.build_config(options) ⇒ Docscribe::Config

Parameters:

  • options (Docscribe::CLI::Formatters::opts)

Returns:



112
113
114
115
116
117
# File 'lib/docscribe/cli/run.rb', line 112

def build_config(options)
  conf = Docscribe::Config.load(options[:config])
  conf = Docscribe::CLI::ConfigBuilder.build(conf, options)
  conf.load_plugins!
  conf
end

.build_failure_line(state, type_mismatch_count, checked_error) ⇒ String

Build the human-readable failure summary line for check output.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • type_mismatch_count (Integer)

    number of files with type mismatches

  • checked_error (Integer)

    number of files with errors

Returns:

  • (String)


859
860
861
862
863
864
865
# File 'lib/docscribe/cli/run.rb', line 859

def build_failure_line(state, type_mismatch_count, checked_error)
  parts = ["#{state[:checked_fail]} need updates"]
  parts << "#{type_mismatch_count} type mismatches" if type_mismatch_count.positive?
  parts << "#{checked_error} errors"
  parts << "#{state[:checked_ok]} ok"
  "Docscribe: FAILED (#{parts.join(', ')})"
end

.build_light_config(options) ⇒ Docscribe::Config

Parameters:

  • options (Docscribe::CLI::Formatters::opts)

Returns:



121
122
123
124
# File 'lib/docscribe/cli/run.rb', line 121

def build_light_config(options)
  conf = Docscribe::Config.load(options[:config])
  Docscribe::CLI::ConfigBuilder.build(conf, options)
end

.change_line_suffix(change) ⇒ String

Format the line number suffix for a change reason string.

Parameters:

  • change (Docscribe::CLI::Formatters::change)

    structured change record

Returns:

  • (String)

    " at line N" or empty



937
938
939
# File 'lib/docscribe/cli/run.rb', line 937

def change_line_suffix(change)
  change[:line] ? " at line #{change[:line]}" : ''
end

.change_method_suffix(change) ⇒ String

Format the method name suffix for a change reason string.

Parameters:

  • change (Docscribe::CLI::Formatters::change)

    structured change record

Returns:

  • (String)

    " for method_name" or empty



945
946
947
# File 'lib/docscribe/cli/run.rb', line 945

def change_method_suffix(change)
  change[:method] ? " for #{change[:method]}" : ''
end

.core_rbs_provider_for(conf) ⇒ Docscribe::Types::RBS::Provider?

Return the core RBS provider from the config if available.

Parameters:

Returns:



161
162
163
# File 'lib/docscribe/cli/run.rb', line 161

def core_rbs_provider_for(conf)
  conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
end

.direct_message_change?(change) ⇒ Boolean

Whether a change type uses its own :message field directly as the reason.

Parameters:

  • change (Docscribe::CLI::Formatters::change)

    structured change record

Returns:

  • (Boolean)


953
954
955
956
957
958
959
960
961
962
# File 'lib/docscribe/cli/run.rb', line 953

def direct_message_change?(change)
  %i[
    missing_param
    missing_return
    missing_raise
    missing_visibility
    missing_module_function_note
    insert_full_doc_block
  ].include?(change[:type])
end

.dispatch_server_result(result, file_changes, path, **ctx) ⇒ void

This method returns an undefined value.

Dispatch the server result to check or write handler.

Parameters:

  • result (Hash<String, Object>)

    server result with :changed and :changes keys

  • file_changes (Array<Docscribe::CLI::Formatters::change>)

    change records

  • path (String)

    file path

  • ctx (Object)

    context hash with :display_path, :options, :state keys



246
247
248
249
250
251
252
253
254
255
# File 'lib/docscribe/cli/run.rb', line 246

def dispatch_server_result(result, file_changes, path, **ctx)
  if ctx[:options][:mode] == :check
    handle_via_server_check(path, file_changes: file_changes,
                                  display_path: ctx[:display_path],
                                  options: ctx[:options], state: ctx[:state])
  else
    write_server_result(result, file_changes, display_path: ctx[:display_path],
                                              options: ctx[:options], state: ctx[:state])
  end
end

.ensure_server_running!(config_path: nil) ⇒ void

This method returns an undefined value.

Ensure the server daemon is running, auto-starting if necessary.

Parameters:

  • config_path (String?) (defaults to: nil)


186
187
188
# File 'lib/docscribe/cli/run.rb', line 186

def ensure_server_running!(config_path: nil)
  Docscribe::Server.ensure_running!(config_path: config_path)
end

.expand_paths(args) ⇒ Array<String>

Expand CLI path arguments into a sorted list of Ruby files.

Directories are expanded recursively to **/*.rb. If no arguments are provided, the current directory is used.

Parameters:

  • args (Array<String>)

    file and/or directory arguments

Returns:

  • (Array<String>)

    unique sorted Ruby file paths



344
345
346
347
348
349
350
351
352
353
# File 'lib/docscribe/cli/run.rb', line 344

def expand_paths(args)
  files = [] #: Array[String]
  args = ['.'] if args.empty?

  args.each do |path|
    append_expanded_path(files, path)
  end

  files.uniq.sort
end

.extract_cli_overrides(options) ⇒ Hash<String, Object>

Parameters:

  • options (Docscribe::CLI::Formatters::opts)

Returns:

  • (Hash<String, Object>)


402
403
404
405
406
407
408
409
410
411
412
# File 'lib/docscribe/cli/run.rb', line 402

def extract_cli_overrides(options)
  overrides = options.slice(*CLI_OVERRIDE_KEYS)
  acc = {} #: Hash[String, untyped]
  overrides.each do |k, v|
    next if v.nil? || v == false
    next if v.is_a?(Array) && v.empty?

    acc[k.to_s] = v
  end
  acc
end

.filtered_paths(argv, conf) ⇒ Array<String>

Expand CLI path arguments and filter through config file patterns.

Parameters:

  • argv (Array<String>)

    CLI path arguments

  • conf (Docscribe::Config)

    effective config

Returns:

  • (Array<String>)

    filtered Ruby file paths



170
171
172
# File 'lib/docscribe/cli/run.rb', line 170

def filtered_paths(argv, conf)
  expand_paths(argv).select { |path| conf.process_file?(path) }
end

.format_change_reason(change) ⇒ String

Format a structured change record into human-readable CLI output.

Parameters:

  • change (Docscribe::CLI::Formatters::change)

    structured change produced by the inline rewriter

Returns:

  • (String)

    human-readable explanation line



923
924
925
926
927
928
929
930
931
# File 'lib/docscribe/cli/run.rb', line 923

def format_change_reason(change)
  line = change_line_suffix(change)
  method = change_method_suffix(change)

  return "unsorted tags#{line}" if change[:type] == :unsorted_tags
  return "#{change[:message]}#{method}#{line}" if direct_message_change?(change)

  "#{change[:message] || change[:type].to_s.tr('_', ' ')}#{method}#{line}"
end

.handle_via_server_check(path, file_changes:, display_path:, options:, state:) ⇒ void

This method returns an undefined value.

Handle a check result from the server.

Parameters:

  • path (String)

    file path

  • file_changes (Array<Docscribe::CLI::Formatters::change>)

    change records from server

  • display_path (String)

    path shown in CLI output

  • options (Docscribe::CLI::Formatters::opts)

    CLI options

  • state (Docscribe::CLI::Formatters::state)

    shared processing state



284
285
286
287
288
289
290
291
292
# File 'lib/docscribe/cli/run.rb', line 284

def handle_via_server_check(path, file_changes:, display_path:, options:, state:)
  if file_changes.empty?
    state[:checked_ok] += 1
    return log_check_verdict('OK', display_path, options)
  end

  report_check_failure(display_path, file_changes, options)
  update_check_failure_state(path, file_changes, state)
end

.mismatch_only?(state, checked_error) ⇒ Boolean

Whether type mismatches exist but no failures or errors.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • checked_error (Integer)

    number of files with errors

Returns:

  • (Boolean)


849
850
851
# File 'lib/docscribe/cli/run.rb', line 849

def mismatch_only?(state, checked_error)
  state[:checked_fail].zero? && checked_error.zero?
end

.no_files_foundInteger

Warn and return exit code when no matching files were found.

Returns:

  • (Integer)

    exit code 2



177
178
179
180
# File 'lib/docscribe/cli/run.rb', line 177

def no_files_found
  warn 'No files found. Pass files or directories (e.g. `docscribe lib`).'
  2
end

This method returns an undefined value.

Print the check-mode status line.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state



821
822
823
824
825
826
827
828
829
830
831
832
# File 'lib/docscribe/cli/run.rb', line 821

def print_check_status_line(state)
  checked_error = state[:error_paths].size
  type_mismatch_count = state[:type_mismatch_paths].size

  if all_fine?(state, checked_error, type_mismatch_count)
    puts "Docscribe: OK (#{state[:checked_ok]} files checked)"
  elsif mismatch_only?(state, checked_error)
    puts "Docscribe: OK (#{state[:checked_ok]} files checked, #{type_mismatch_count} with type mismatches)"
  else
    puts build_failure_line(state, type_mismatch_count, checked_error)
  end
end

This method returns an undefined value.

Print corrected paths from write-mode summary (stdout).

Skips explanations when --verbose showed them inline per-file.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • options (Docscribe::CLI::Formatters::opts)

    CLI options



907
908
909
910
911
912
913
914
915
916
917
# File 'lib/docscribe/cli/run.rb', line 907

def print_corrected_paths(state, options)
  state[:corrected_paths].each do |p|
    puts "Updated: #{p}"

    next if options[:verbose] || options[:quiet]

    Array(state[:corrected_changes][p]).each do |change|
      puts "  - #{format_change_reason(change)}"
    end
  end
end

This method returns an undefined value.

Print error paths from check summary.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state



968
969
970
971
972
973
974
975
976
# File 'lib/docscribe/cli/run.rb', line 968

def print_error_paths(state)
  return if state[:error_paths].empty?

  warn ''
  state[:error_paths].each do |p|
    warn "Error processing: #{p}"
    warn "  #{state[:error_messages][p]}" if state[:error_messages][p]
  end
end

This method returns an undefined value.

Print fail paths from check summary (stdout).

Skips explanations when --verbose showed them inline per-file.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • options (Docscribe::CLI::Formatters::opts)

    CLI options



805
806
807
808
809
810
811
812
813
814
815
# File 'lib/docscribe/cli/run.rb', line 805

def print_fail_paths(state, options)
  state[:fail_paths].each do |p|
    puts "Would update: #{p}"

    next if options[:verbose] || options[:quiet]

    Array(state[:fail_changes][p]).each do |change|
      puts "  - #{format_change_reason(change)}"
    end
  end
end

This method returns an undefined value.

Print type mismatch paths from check summary.

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • options (Docscribe::CLI::Formatters::opts)

    CLI options



872
873
874
875
876
877
878
879
880
881
882
# File 'lib/docscribe/cli/run.rb', line 872

def print_type_mismatch_paths(state, options)
  return if options[:quiet]
  return unless options[:verbose] || options[:explain]

  state[:type_mismatch_paths].each do |p|
    warn "Type mismatches: #{p}"
    Array(state[:type_mismatch_changes][p]).each do |change|
      warn "  - #{format_change_reason(change)}"
    end
  end
end

This method returns an undefined value.

Print the write-mode summary (files corrected, errors).

Parameters:

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • options (Docscribe::CLI::Formatters::opts)

    CLI options



889
890
891
892
893
894
895
896
897
898
# File 'lib/docscribe/cli/run.rb', line 889

def print_write_summary(state:, options:)
  puts
  puts "Docscribe: updated #{state[:corrected]} file(s)" if state[:corrected].positive?
  print_corrected_paths(state, options)

  return unless state[:had_errors]

  warn "Docscribe: #{state[:error_paths].size} file(s) had errors"
  print_error_paths(state)
end

.process_one_file_via_server(client, path, options:, pwd:, state:) ⇒ void

This method returns an undefined value.

Process a single file via the server client.

Parameters:

  • client (Docscribe::Server::Client)

    server client

  • path (String)

    file path

  • options (Docscribe::CLI::Formatters::opts)

    CLI options

  • pwd (Pathname)

    current working directory

  • state (Docscribe::CLI::Formatters::state)

    shared processing state



198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/docscribe/cli/run.rb', line 198

def process_one_file_via_server(client, path, options:, pwd:, state:)
  display_path = display_path_for(path, pwd: pwd)
  report_progress(state, options, display_path)
  response = send_server_request(client, path, options)
  return server_error(path, state, 'Server unreachable') unless response
  return server_error(path, state, response['error']['message']) if response['error']

  result = response['result']
  file_changes = (result['changes'] || []).map { |c| symbolize_change(c) }
  dispatch_server_result(result, file_changes, path,
                         display_path: display_path, options: options, state: state)
end

.report_check_failure(display_path, file_changes, options) ⇒ void

This method returns an undefined value.

Report a check failure with verbose or compact output.

Parameters:

  • display_path (String)

    path shown in CLI output

  • file_changes (Array<Docscribe::CLI::Formatters::change>)

    change records from server

  • options (Docscribe::CLI::Formatters::opts)

    CLI options



300
301
302
303
304
305
306
307
# File 'lib/docscribe/cli/run.rb', line 300

def report_check_failure(display_path, file_changes, options)
  if options[:verbose]
    warn("FAIL #{display_path}")
    print_check_explanations(file_changes)
  else
    $stderr.print('F')
  end
end

.run(options:, argv:) ⇒ Integer

Run Docscribe for files or STDIN using the selected mode and strategy.

Modes:

  • :check => inspect what the selected strategy would change
  • :write => apply the selected strategy in place
  • :stdin => rewrite STDIN and print to STDOUT

Strategies:

  • :safe => merge/add/normalize non-destructively
  • :aggressive => rebuild existing doc blocks

Parameters:

  • options (Docscribe::CLI::Formatters::opts)

    parsed CLI options

  • argv (Array<String>)

    remaining path arguments

Returns:

  • (Integer)

    process exit code



59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/docscribe/cli/run.rb', line 59

def run(options:, argv:)
  return run_via_server(options: options, argv: argv) if options[:server]

  require 'docscribe/inline_rewriter'
  conf = build_config(options)

  return run_stdin(options: options, conf: conf) if options[:mode] == :stdin

  paths = filtered_paths(argv, conf)
  return no_files_found unless paths.any?

  run_files(options: options, conf: conf, paths: paths)
end

.run_files(options:, conf:, paths:) ⇒ Integer

Process file paths in inspect or write mode.

In inspect mode:

  • prints progress/status
  • exits non-zero if any file would change or if any errors occurred

In write mode:

  • rewrites changed files in place
  • exits non-zero only if errors occurred

Parameters:

  • options (Docscribe::CLI::Formatters::opts)

    parsed CLI options

  • conf (Docscribe::Config)

    effective config

  • paths (Array<String>)

    Ruby file paths to process

Returns:

  • (Integer)

    process exit code



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/docscribe/cli/run.rb', line 384

def run_files(options:, conf:, paths:)
  $stdout.sync = true

  state = initial_run_state
  state[:total] = paths.size
  pwd = Pathname.pwd

  paths.each do |path|
    process_one_file(path, options: options, conf: conf, pwd: pwd, state: state)
  end

  finalize_run(options, state)

  run_exit_code(options, state)
end

.run_files_via_server(client, paths, options) ⇒ Integer

Run files through the server client with progress tracking.

Parameters:

  • client (Docscribe::Server::Client)

    server client

  • paths (Array<String>)

    file paths to process

  • options (Docscribe::CLI::Formatters::opts)

    CLI options

Returns:

  • (Integer)

    exit code



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/docscribe/cli/run.rb', line 98

def run_files_via_server(client, paths, options)
  $stdout.sync = true
  state = initial_run_state
  state[:total] = paths.size
  pwd = Pathname.pwd
  paths.each do |path|
    process_one_file_via_server(client, path, options: options, pwd: pwd, state: state)
  end
  finalize_run(options, state)
  run_exit_code(options, state)
end

.run_stdin(options:, conf:) ⇒ Integer

Rewrite code from STDIN using the selected strategy and print the result.

Parameters:

  • options (Docscribe::CLI::Formatters::opts)

    parsed CLI options

  • conf (Docscribe::Config)

    effective config

Returns:

  • (Integer)
  • (Integer)

    if StandardError

Raises:

  • (StandardError)


134
135
136
137
138
139
140
# File 'lib/docscribe/cli/run.rb', line 134

def run_stdin(options:, conf:)
  puts stdin_rewrite_result(options, conf)[:output]
  0
rescue StandardError => e
  warn "Docscribe: Error processing stdin: #{e.class}: #{e.message}"
  1
end

.run_via_server(options:, argv:) ⇒ Integer

Parameters:

  • options (Docscribe::CLI::Formatters::opts)
  • argv (Array<String>)

Returns:

  • (Integer)
  • (Integer)

    if RuntimeError

Raises:

  • (RuntimeError)


78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/docscribe/cli/run.rb', line 78

def run_via_server(options:, argv:)
  require 'docscribe/server'
  conf = build_light_config(options)
  ensure_server_running!(config_path: conf.config_path)
  client = Docscribe::Server::Client.new(config_path: conf.config_path)
  paths = filtered_paths(argv, conf)
  return no_files_found unless paths.any?

  run_files_via_server(client, paths, options)
rescue RuntimeError => e
  warn e.message
  1
end

.send_server_request(client, path, options) ⇒ Hash<String, Object>?

Parameters:

Returns:

  • (Hash<String, Object>, nil)


215
216
217
218
219
220
221
222
223
224
# File 'lib/docscribe/cli/run.rb', line 215

def send_server_request(client, path, options)
  method_name = options[:mode] == :write ? :fix : :check
  strategy = options[:strategy].to_s
  cli_overrides = extract_cli_overrides(options)
  if cli_overrides.empty?
    client.send(method_name, file: path, strategy: strategy)
  else
    client.send(method_name, file: path, strategy: strategy, cli_overrides: cli_overrides)
  end
end

.server_error(path, state, message) ⇒ void

This method returns an undefined value.

Record a server error in the shared state and print an indicator.

Parameters:

  • path (String)

    file path

  • state (Docscribe::CLI::Formatters::state)

    shared processing state

  • message (String)

    error message



232
233
234
235
236
237
# File 'lib/docscribe/cli/run.rb', line 232

def server_error(path, state, message)
  state[:had_errors] = true
  state[:error_paths] << path
  state[:error_messages][path] = message
  $stderr.print('E')
end

.stdin_rewrite_result(options, conf) ⇒ Hash<Symbol, Object>

Rewrite STDIN input and return the result report.

Parameters:

  • options (Docscribe::CLI::Formatters::opts)

    parsed CLI options

  • conf (Docscribe::Config)

    effective config

Returns:

  • (Hash<Symbol, Object>)

    rewrite result with :output key



147
148
149
150
151
152
153
154
155
# File 'lib/docscribe/cli/run.rb', line 147

def stdin_rewrite_result(options, conf)
  Docscribe::InlineRewriter.rewrite_with_report(
    $stdin.read,
    strategy: options[:strategy],
    config: conf,
    core_rbs_provider: core_rbs_provider_for(conf),
    file: '(stdin)'
  )
end

.symbolize_change(change) ⇒ Docscribe::CLI::Formatters::change

Convert server response change (string keys) to formatter-compatible change (symbol keys).

Parameters:

  • change (Hash<String, Object>)

    change record from server

Returns:

  • (Docscribe::CLI::Formatters::change)


327
328
329
330
331
332
333
334
335
# File 'lib/docscribe/cli/run.rb', line 327

def symbolize_change(change)
  {
    type: change['type'].to_sym,
    file: change['file'],
    line: change['line'],
    method: change['method'],
    message: change['message']
  }
end

.update_check_failure_state(path, file_changes, state) ⇒ void

This method returns an undefined value.

Update shared state after a check failure.

Parameters:

  • path (String)

    file path

  • file_changes (Array<Docscribe::CLI::Formatters::change>)

    change records from server

  • state (Docscribe::CLI::Formatters::state)

    shared processing state



315
316
317
318
319
320
# File 'lib/docscribe/cli/run.rb', line 315

def update_check_failure_state(path, file_changes, state)
  state[:checked_fail] += 1
  state[:changed] = true
  state[:fail_paths] << path
  state[:fail_changes][path] = file_changes
end

.write_server_result(result, file_changes, display_path:, options:, state:) ⇒ void

This method returns an undefined value.

Handle a server write-mode result.

Parameters:

  • result (Hash<String, Object>)

    server result with :changed key

  • file_changes (Array<Docscribe::CLI::Formatters::change>)

    change records

  • display_path (String)

    path shown in CLI output

  • options (Docscribe::CLI::Formatters::opts)

    CLI options

  • state (Docscribe::CLI::Formatters::state)

    shared processing state



265
266
267
268
269
270
271
272
273
274
# File 'lib/docscribe/cli/run.rb', line 265

def write_server_result(result, file_changes, display_path:, options:, state:)
  if result['changed']
    state[:corrected] += 1
    state[:corrected_paths] << display_path
    state[:corrected_changes][display_path] = file_changes
    log_check_verdict('CHANGED', display_path, options)
  else
    log_check_verdict('OK', display_path, options)
  end
end