docscribe 1.4.2 → 1.5.1

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +601 -139
  3. data/exe/docscribe-client +105 -0
  4. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  5. data/lib/docscribe/cli/config_builder.rb +107 -53
  6. data/lib/docscribe/cli/formatters/json.rb +294 -0
  7. data/lib/docscribe/cli/formatters/sarif.rb +235 -0
  8. data/lib/docscribe/cli/formatters/text.rb +208 -0
  9. data/lib/docscribe/cli/formatters.rb +26 -0
  10. data/lib/docscribe/cli/generate.rb +56 -62
  11. data/lib/docscribe/cli/init.rb +14 -6
  12. data/lib/docscribe/cli/options.rb +206 -89
  13. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  14. data/lib/docscribe/cli/run.rb +433 -154
  15. data/lib/docscribe/cli/server.rb +135 -0
  16. data/lib/docscribe/cli/sigs.rb +366 -0
  17. data/lib/docscribe/cli/update_types.rb +103 -0
  18. data/lib/docscribe/cli.rb +21 -24
  19. data/lib/docscribe/config/defaults.rb +7 -2
  20. data/lib/docscribe/config/emit.rb +17 -0
  21. data/lib/docscribe/config/filtering.rb +17 -24
  22. data/lib/docscribe/config/loader.rb +19 -17
  23. data/lib/docscribe/config/plugin.rb +1 -1
  24. data/lib/docscribe/config/rbs.rb +39 -7
  25. data/lib/docscribe/config/sorbet.rb +22 -16
  26. data/lib/docscribe/config/sorting.rb +1 -1
  27. data/lib/docscribe/config/template.rb +10 -1
  28. data/lib/docscribe/config/utils.rb +11 -9
  29. data/lib/docscribe/config.rb +10 -6
  30. data/lib/docscribe/infer/ast_walk.rb +1 -1
  31. data/lib/docscribe/infer/literals.rb +6 -11
  32. data/lib/docscribe/infer/names.rb +2 -3
  33. data/lib/docscribe/infer/params.rb +14 -16
  34. data/lib/docscribe/infer/raises.rb +3 -5
  35. data/lib/docscribe/infer/returns.rb +615 -151
  36. data/lib/docscribe/infer.rb +29 -26
  37. data/lib/docscribe/inline_rewriter/collector.rb +159 -164
  38. data/lib/docscribe/inline_rewriter/doc_block.rb +145 -115
  39. data/lib/docscribe/inline_rewriter/doc_builder.rb +1032 -723
  40. data/lib/docscribe/inline_rewriter/source_helpers.rb +48 -48
  41. data/lib/docscribe/inline_rewriter/tag_sorter.rb +82 -85
  42. data/lib/docscribe/inline_rewriter.rb +485 -488
  43. data/lib/docscribe/lru_cache.rb +49 -0
  44. data/lib/docscribe/parsing.rb +28 -9
  45. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -1
  46. data/lib/docscribe/plugin/base/tag_plugin.rb +0 -1
  47. data/lib/docscribe/plugin/context.rb +28 -18
  48. data/lib/docscribe/plugin/registry.rb +25 -26
  49. data/lib/docscribe/plugin/tag.rb +9 -14
  50. data/lib/docscribe/plugin.rb +17 -16
  51. data/lib/docscribe/server.rb +608 -0
  52. data/lib/docscribe/types/provider_chain.rb +4 -2
  53. data/lib/docscribe/types/rbs/collection_loader.rb +2 -2
  54. data/lib/docscribe/types/rbs/provider.rb +177 -51
  55. data/lib/docscribe/types/rbs/type_formatter.rb +224 -83
  56. data/lib/docscribe/types/signature.rb +22 -42
  57. data/lib/docscribe/types/sorbet/base_provider.rb +29 -21
  58. data/lib/docscribe/types/sorbet/rbi_provider.rb +6 -5
  59. data/lib/docscribe/types/sorbet/source_provider.rb +6 -4
  60. data/lib/docscribe/types/yard/formatter.rb +100 -0
  61. data/lib/docscribe/types/yard/parser.rb +240 -0
  62. data/lib/docscribe/types/yard/types.rb +52 -0
  63. data/lib/docscribe/version.rb +1 -1
  64. metadata +38 -1
@@ -3,7 +3,7 @@
3
3
  require 'pathname'
4
4
 
5
5
  require 'docscribe/cli/config_builder'
6
- require 'docscribe/inline_rewriter'
6
+ require 'docscribe/cli/formatters'
7
7
 
8
8
  module Docscribe
9
9
  module CLI
@@ -23,13 +23,25 @@ module Docscribe
23
23
  checked_ok: 0,
24
24
  checked_fail: 0,
25
25
  corrected: 0,
26
+ corrected_paths: [], #: Array[String]
27
+ corrected_changes: {}, #: Hash[String, untyped]
26
28
  fail_paths: [], #: Array[String]
27
29
  fail_changes: {}, #: Hash[String, untyped]
28
30
  error_paths: [], #: Array[String]
29
31
  error_messages: {}, #: Hash[String, String]
30
32
  type_mismatch_paths: [], #: Array[String]
31
- type_mismatch_changes: {} #: Hash[String, untyped]
33
+ type_mismatch_changes: {}, #: Hash[String, untyped]
34
+ total: 0,
35
+ processed: 0
32
36
  }.freeze
37
+
38
+ CLI_OVERRIDE_KEYS = %i[
39
+ keep_descriptions no_boilerplate
40
+ include exclude include_file exclude_file
41
+ rbs rbs_collection sig_dirs
42
+ sorbet rbi_dirs
43
+ ].freeze
44
+
33
45
  # Run Docscribe for files or STDIN using the selected mode and strategy.
34
46
  #
35
47
  # Modes:
@@ -41,10 +53,13 @@ module Docscribe
41
53
  # - :safe => merge/add/normalize non-destructively
42
54
  # - :aggressive => rebuild existing doc blocks
43
55
  #
44
- # @param [Hash] options parsed CLI options
56
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
45
57
  # @param [Array<String>] argv remaining path arguments
46
58
  # @return [Integer] process exit code
47
59
  def run(options:, argv:)
60
+ return run_via_server(options: options, argv: argv) if options[:server]
61
+
62
+ require 'docscribe/inline_rewriter'
48
63
  conf = build_config(options)
49
64
 
50
65
  return run_stdin(options: options, conf: conf) if options[:mode] == :stdin
@@ -55,10 +70,45 @@ module Docscribe
55
70
  run_files(options: options, conf: conf, paths: paths)
56
71
  end
57
72
 
58
- # Load and build the effective config from CLI options.
73
+ # @param [Docscribe::CLI::Formatters::opts] options
74
+ # @param [Array<String>] argv
75
+ # @raise [RuntimeError]
76
+ # @return [Integer]
77
+ # @return [Integer] if RuntimeError
78
+ def run_via_server(options:, argv:)
79
+ require 'docscribe/server'
80
+ conf = build_light_config(options)
81
+ ensure_server_running!(config_path: conf.config_path)
82
+ client = Docscribe::Server::Client.new(config_path: conf.config_path)
83
+ paths = filtered_paths(argv, conf)
84
+ return no_files_found unless paths.any?
85
+
86
+ run_files_via_server(client, paths, options)
87
+ rescue RuntimeError => e
88
+ warn e.message
89
+ 1
90
+ end
91
+
92
+ # Run files through the server client with progress tracking.
59
93
  #
60
- # @param [Hash] options parsed CLI options
61
- # @return [Docscribe::Config] effective config with plugins loaded
94
+ # @param [Docscribe::Server::Client] client server client
95
+ # @param [Array<String>] paths file paths to process
96
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
97
+ # @return [Integer] exit code
98
+ def run_files_via_server(client, paths, options)
99
+ $stdout.sync = true
100
+ state = initial_run_state
101
+ state[:total] = paths.size
102
+ pwd = Pathname.pwd
103
+ paths.each do |path|
104
+ process_one_file_via_server(client, path, options: options, pwd: pwd, state: state)
105
+ end
106
+ finalize_run(options, state)
107
+ run_exit_code(options, state)
108
+ end
109
+
110
+ # @param [Docscribe::CLI::Formatters::opts] options
111
+ # @return [Docscribe::Config]
62
112
  def build_config(options)
63
113
  conf = Docscribe::Config.load(options[:config])
64
114
  conf = Docscribe::CLI::ConfigBuilder.build(conf, options)
@@ -66,13 +116,21 @@ module Docscribe
66
116
  conf
67
117
  end
68
118
 
119
+ # @param [Docscribe::CLI::Formatters::opts] options
120
+ # @return [Docscribe::Config]
121
+ def build_light_config(options)
122
+ conf = Docscribe::Config.load(options[:config])
123
+ Docscribe::CLI::ConfigBuilder.build(conf, options)
124
+ end
125
+
69
126
  # Rewrite code from STDIN using the selected strategy and print the
70
127
  # result.
71
128
  #
72
- # @param [Hash] options parsed CLI options
129
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
73
130
  # @param [Docscribe::Config] conf effective config
74
131
  # @raise [StandardError]
75
- # @return [Integer] process exit code
132
+ # @return [Integer]
133
+ # @return [Integer] if StandardError
76
134
  def run_stdin(options:, conf:)
77
135
  puts stdin_rewrite_result(options, conf)[:output]
78
136
  0
@@ -83,9 +141,9 @@ module Docscribe
83
141
 
84
142
  # Rewrite STDIN input and return the result report.
85
143
  #
86
- # @param [Hash] options parsed CLI options
144
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
87
145
  # @param [Docscribe::Config] conf effective config
88
- # @return [Hash] rewrite result with :output key
146
+ # @return [Hash<Symbol, Object>] rewrite result with :output key
89
147
  def stdin_rewrite_result(options, conf)
90
148
  Docscribe::InlineRewriter.rewrite_with_report(
91
149
  $stdin.read,
@@ -99,7 +157,7 @@ module Docscribe
99
157
  # Return the core RBS provider from the config if available.
100
158
  #
101
159
  # @param [Docscribe::Config] conf effective config
102
- # @return [Object, nil] core RBS provider or nil
160
+ # @return [Docscribe::Types::RBS::Provider, nil] core RBS provider or nil
103
161
  def core_rbs_provider_for(conf)
104
162
  conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
105
163
  end
@@ -115,10 +173,165 @@ module Docscribe
115
173
 
116
174
  # Warn and return exit code when no matching files were found.
117
175
  #
118
- # @return [Integer] exit code 1
176
+ # @return [Integer] exit code 2
119
177
  def no_files_found
120
178
  warn 'No files found. Pass files or directories (e.g. `docscribe lib`).'
121
- 1
179
+ 2
180
+ end
181
+
182
+ # Ensure the server daemon is running, auto-starting if necessary.
183
+ #
184
+ # @param [String?] config_path
185
+ # @return [void]
186
+ def ensure_server_running!(config_path: nil)
187
+ Docscribe::Server.ensure_running!(config_path: config_path)
188
+ end
189
+
190
+ # Process a single file via the server client.
191
+ #
192
+ # @param [Docscribe::Server::Client] client server client
193
+ # @param [String] path file path
194
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
195
+ # @param [Pathname] pwd current working directory
196
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
197
+ # @return [void]
198
+ def process_one_file_via_server(client, path, options:, pwd:, state:)
199
+ display_path = display_path_for(path, pwd: pwd)
200
+ report_progress(state, options, display_path)
201
+ response = send_server_request(client, path, options)
202
+ return server_error(path, state, 'Server unreachable') unless response
203
+ return server_error(path, state, response['error']['message']) if response['error']
204
+
205
+ result = response['result']
206
+ file_changes = (result['changes'] || []).map { |c| symbolize_change(c) }
207
+ dispatch_server_result(result, file_changes, path,
208
+ display_path: display_path, options: options, state: state)
209
+ end
210
+
211
+ # @param [Docscribe::Server::Client] client
212
+ # @param [String] path
213
+ # @param [Docscribe::CLI::Formatters::opts] options
214
+ # @return [Hash<String, Object>, nil]
215
+ def send_server_request(client, path, options)
216
+ method_name = options[:mode] == :write ? :fix : :check
217
+ strategy = options[:strategy].to_s
218
+ cli_overrides = extract_cli_overrides(options)
219
+ if cli_overrides.empty?
220
+ client.send(method_name, file: path, strategy: strategy)
221
+ else
222
+ client.send(method_name, file: path, strategy: strategy, cli_overrides: cli_overrides)
223
+ end
224
+ end
225
+
226
+ # Record a server error in the shared state and print an indicator.
227
+ #
228
+ # @param [String] path file path
229
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
230
+ # @param [String] message error message
231
+ # @return [void]
232
+ def server_error(path, state, message)
233
+ state[:had_errors] = true
234
+ state[:error_paths] << path
235
+ state[:error_messages][path] = message
236
+ $stderr.print('E')
237
+ end
238
+
239
+ # Dispatch the server result to check or write handler.
240
+ #
241
+ # @param [Hash<String, Object>] result server result with :changed and :changes keys
242
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes change records
243
+ # @param [String] path file path
244
+ # @param [Object] ctx context hash with :display_path, :options, :state keys
245
+ # @return [void]
246
+ def dispatch_server_result(result, file_changes, path, **ctx)
247
+ if ctx[:options][:mode] == :check
248
+ handle_via_server_check(path, file_changes: file_changes,
249
+ display_path: ctx[:display_path],
250
+ options: ctx[:options], state: ctx[:state])
251
+ else
252
+ write_server_result(result, file_changes, display_path: ctx[:display_path],
253
+ options: ctx[:options], state: ctx[:state])
254
+ end
255
+ end
256
+
257
+ # Handle a server write-mode result.
258
+ #
259
+ # @param [Hash<String, Object>] result server result with :changed key
260
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes change records
261
+ # @param [String] display_path path shown in CLI output
262
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
263
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
264
+ # @return [void]
265
+ def write_server_result(result, file_changes, display_path:, options:, state:)
266
+ if result['changed']
267
+ state[:corrected] += 1
268
+ state[:corrected_paths] << display_path
269
+ state[:corrected_changes][display_path] = file_changes
270
+ log_check_verdict('CHANGED', display_path, options)
271
+ else
272
+ log_check_verdict('OK', display_path, options)
273
+ end
274
+ end
275
+
276
+ # Handle a check result from the server.
277
+ #
278
+ # @param [String] path file path
279
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes change records from server
280
+ # @param [String] display_path path shown in CLI output
281
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
282
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
283
+ # @return [void]
284
+ def handle_via_server_check(path, file_changes:, display_path:, options:, state:)
285
+ if file_changes.empty?
286
+ state[:checked_ok] += 1
287
+ return log_check_verdict('OK', display_path, options)
288
+ end
289
+
290
+ report_check_failure(display_path, file_changes, options)
291
+ update_check_failure_state(path, file_changes, state)
292
+ end
293
+
294
+ # Report a check failure with verbose or compact output.
295
+ #
296
+ # @param [String] display_path path shown in CLI output
297
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes change records from server
298
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
299
+ # @return [void]
300
+ def report_check_failure(display_path, file_changes, options)
301
+ if options[:verbose]
302
+ warn("FAIL #{display_path}")
303
+ print_check_explanations(file_changes)
304
+ else
305
+ $stderr.print('F')
306
+ end
307
+ end
308
+
309
+ # Update shared state after a check failure.
310
+ #
311
+ # @param [String] path file path
312
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes change records from server
313
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
314
+ # @return [void]
315
+ def update_check_failure_state(path, file_changes, state)
316
+ state[:checked_fail] += 1
317
+ state[:changed] = true
318
+ state[:fail_paths] << path
319
+ state[:fail_changes][path] = file_changes
320
+ end
321
+
322
+ # Convert server response change (string keys) to formatter-compatible
323
+ # change (symbol keys).
324
+ #
325
+ # @param [Hash<String, Object>] change change record from server
326
+ # @return [Docscribe::CLI::Formatters::change]
327
+ def symbolize_change(change)
328
+ {
329
+ type: change['type'].to_sym,
330
+ file: change['file'],
331
+ line: change['line'],
332
+ method: change['method'],
333
+ message: change['message']
334
+ }
122
335
  end
123
336
 
124
337
  # Expand CLI path arguments into a sorted list of Ruby files.
@@ -164,7 +377,7 @@ module Docscribe
164
377
  # - rewrites changed files in place
165
378
  # - exits non-zero only if errors occurred
166
379
  #
167
- # @param [Hash] options parsed CLI options
380
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
168
381
  # @param [Docscribe::Config] conf effective config
169
382
  # @param [Array<String>] paths Ruby file paths to process
170
383
  # @return [Integer] process exit code
@@ -172,6 +385,7 @@ module Docscribe
172
385
  $stdout.sync = true
173
386
 
174
387
  state = initial_run_state
388
+ state[:total] = paths.size
175
389
  pwd = Pathname.pwd
176
390
 
177
391
  paths.each do |path|
@@ -183,30 +397,46 @@ module Docscribe
183
397
  run_exit_code(options, state)
184
398
  end
185
399
 
400
+ # @param [Docscribe::CLI::Formatters::opts] options
401
+ # @return [Hash<String, Object>]
402
+ def extract_cli_overrides(options)
403
+ overrides = options.slice(*CLI_OVERRIDE_KEYS)
404
+ acc = {} #: Hash[String, untyped]
405
+ overrides.each do |k, v|
406
+ next if v.nil? || v == false
407
+ next if v.is_a?(Array) && v.empty?
408
+
409
+ acc[k.to_s] = v
410
+ end
411
+ acc
412
+ end
413
+
186
414
  private
187
415
 
188
416
  # Print the check or write summary at the end of a run.
189
417
  #
190
418
  # @private
191
- # @param [Hash] options CLI options
192
- # @param [Hash] state shared processing state
419
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
420
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
193
421
  # @return [void]
194
422
  def finalize_run(options, state)
423
+ formatter = Formatters.for(options[:format])
424
+
195
425
  if options[:mode] == :check
196
- print_check_summary(state: state, options: options)
426
+ formatter.format_check_summary(state: state, options: options)
197
427
  elsif options[:mode] == :write
198
- print_write_summary(state: state)
428
+ formatter.format_write_summary(state: state, options: options)
199
429
  end
200
430
  end
201
431
 
202
432
  # Determine the process exit code based on run state and mode.
203
433
  #
204
434
  # @private
205
- # @param [Hash] options CLI options
206
- # @param [Hash] state shared processing state
207
- # @return [Integer] exit code 0 or 1
435
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
436
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
437
+ # @return [Integer] exit code: 0 = OK, 1 = findings, 2 = error
208
438
  def run_exit_code(options, state)
209
- return 1 if state[:had_errors]
439
+ return 2 if state[:had_errors]
210
440
  return 1 if options[:mode] == :check && state[:changed]
211
441
 
212
442
  0
@@ -215,7 +445,7 @@ module Docscribe
215
445
  # Initialize the shared state hash used throughout a run.
216
446
  #
217
447
  # @private
218
- # @return [Hash] initial state with counters and tracking arrays
448
+ # @return [Docscribe::CLI::Formatters::state] initial state with counters and tracking arrays
219
449
  def initial_run_state
220
450
  Marshal.load(Marshal.dump(INITIAL_RUN_STATE))
221
451
  end
@@ -224,13 +454,14 @@ module Docscribe
224
454
  #
225
455
  # @private
226
456
  # @param [String] path file path
227
- # @param [Hash] options CLI options
457
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
228
458
  # @param [Docscribe::Config] conf configuration
229
459
  # @param [Pathname] pwd current working directory
230
- # @param [Hash] state shared processing state
460
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
231
461
  # @return [void]
232
462
  def process_one_file(path, options:, conf:, pwd:, state:)
233
463
  display_path = display_path_for(path, pwd: pwd)
464
+ report_progress(state, options, display_path)
234
465
 
235
466
  src = read_source_for_path(path, display_path: display_path, options: options, state: state)
236
467
  return unless src
@@ -243,14 +474,28 @@ module Docscribe
243
474
  display_path: display_path, options: options, state: state)
244
475
  end
245
476
 
477
+ # Print progress indicator to stderr when --progress is active.
478
+ #
479
+ # @private
480
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
481
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
482
+ # @param [String] display_path path to display
483
+ # @return [void]
484
+ def report_progress(state, options, display_path)
485
+ state[:processed] += 1
486
+ return unless options[:progress]
487
+
488
+ warn "[#{state[:processed]}/#{state[:total]}] #{display_path}"
489
+ end
490
+
246
491
  # Dispatch the rewrite result to the check or write handler based on mode.
247
492
  #
248
493
  # @private
249
494
  # @param [String] path file path
250
495
  # @param [String] src original source code
251
496
  # @param [String] out rewritten source code
252
- # @param [Array<Hash>] file_changes structured change records
253
- # @param [Hash] ctx context hash with :options, :state, :display_path, :conf
497
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
498
+ # @param [Object] ctx context hash with :options, :state, :display_path, :conf
254
499
  # @return [void]
255
500
  def dispatch_file_result(path, src:, out:, file_changes:, **ctx)
256
501
  if ctx[:options][:mode] == :check
@@ -269,7 +514,8 @@ module Docscribe
269
514
  # @param [String] path file path to display
270
515
  # @param [Pathname] pwd current working directory
271
516
  # @raise [StandardError]
272
- # @return [String] path shown in CLI output
517
+ # @return [String]
518
+ # @return [Object] if StandardError
273
519
  def display_path_for(path, pwd:)
274
520
  abs = Pathname.new(path).expand_path
275
521
 
@@ -287,17 +533,18 @@ module Docscribe
287
533
  # @private
288
534
  # @param [String] path file path to read
289
535
  # @param [String] display_path path shown in CLI output
290
- # @param [Hash] options CLI options
291
- # @param [Hash] state shared processing state
536
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
537
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
292
538
  # @raise [StandardError]
293
- # @return [String, nil] file contents or nil on error
539
+ # @return [String, nil]
540
+ # @return [nil] if StandardError
294
541
  def read_source_for_path(path, display_path:, options:, state:)
295
542
  File.read(path)
296
543
  rescue StandardError => e
297
544
  state[:had_errors] = true
298
545
  state[:error_paths] << path
299
546
  state[:error_messages][path] = "#{e.class}: #{e.message}"
300
- options[:verbose] ? warn("ERR #{display_path}: #{state[:error_messages][path]}") : print('E')
547
+ options[:verbose] ? warn("ERR #{display_path}: #{state[:error_messages][path]}") : $stderr.print('E')
301
548
  nil
302
549
  end
303
550
 
@@ -306,13 +553,10 @@ module Docscribe
306
553
  # @private
307
554
  # @param [String] path file path
308
555
  # @param [String] src source code
309
- # @param [Docscribe::Config] conf configuration
310
- # @param [String] display_path path shown in CLI output
311
- # @param [Hash] options CLI options
312
- # @param [Hash] state shared processing state
313
- # @param [Hash] ctx context hash with :conf, :display_path, :options, :state keys
556
+ # @param [Hash<Symbol, Object>] ctx context hash with :conf, :display_path, :options, :state keys
314
557
  # @raise [StandardError]
315
- # @return [Hash, nil] rewrite result or nil on error
558
+ # @return [Hash<Symbol, Object>, nil]
559
+ # @return [nil] if StandardError
316
560
  def rewrite_result_for_path(path, src:, ctx:)
317
561
  conf = ctx[:conf]
318
562
 
@@ -332,7 +576,7 @@ module Docscribe
332
576
  # @private
333
577
  # @param [String] path file path that caused the error
334
578
  # @param [StandardError] error the exception raised during rewriting
335
- # @param [Hash] ctx context hash with :state, :options, :display_path
579
+ # @param [Hash<Symbol, Object>] ctx context hash with :state, :options, :display_path
336
580
  # @return [void]
337
581
  def record_rewrite_error(path, error, ctx)
338
582
  state = ctx[:state]
@@ -344,7 +588,7 @@ module Docscribe
344
588
  if ctx[:options][:verbose]
345
589
  warn "ERR #{ctx[:display_path]}: #{state[:error_messages][path]}"
346
590
  else
347
- print('E')
591
+ $stderr.print('E')
348
592
  end
349
593
  end
350
594
 
@@ -354,11 +598,8 @@ module Docscribe
354
598
  # @param [String] path file path
355
599
  # @param [String] src original source code
356
600
  # @param [String] out rewritten source code
357
- # @param [Array<Hash>] file_changes structured change records
358
- # @param [String] display_path path shown in CLI output
359
- # @param [Hash] options CLI options
360
- # @param [Hash] state shared processing state
361
- # @param [Hash] ctx context hash with :display_path, :options, :state keys
601
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
602
+ # @param [Object] ctx context hash with :display_path, :options, :state keys
362
603
  # @return [void]
363
604
  def handle_check_result(path, src:, out:, file_changes:, **ctx)
364
605
  type_mismatches = type_mismatch_changes(file_changes)
@@ -377,8 +618,8 @@ module Docscribe
377
618
  # Extract type mismatch changes from file_changes.
378
619
  #
379
620
  # @private
380
- # @param [Array<Hash>] file_changes
381
- # @return [Array<Hash>]
621
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
622
+ # @return [Array<Docscribe::CLI::Formatters::change>]
382
623
  def type_mismatch_changes(file_changes)
383
624
  file_changes.select { |c| %i[updated_param updated_return].include?(c[:type]) }
384
625
  end
@@ -386,11 +627,11 @@ module Docscribe
386
627
  # Handle check result when there are no real changes.
387
628
  #
388
629
  # @private
389
- # @param [String] path
390
- # @param [Array<Hash>] type_mismatches
391
- # @param [String] display_path
392
- # @param [Hash] options
393
- # @param [Hash] state
630
+ # @param [String] path file path
631
+ # @param [Array<Docscribe::CLI::Formatters::change>] type_mismatches type mismatch changes to record
632
+ # @param [String] display_path path shown in CLI output
633
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
634
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
394
635
  # @return [void]
395
636
  def handle_check_no_changes(path, type_mismatches:, display_path:, options:, state:)
396
637
  if type_mismatches.any?
@@ -404,20 +645,21 @@ module Docscribe
404
645
  end
405
646
 
406
647
  # Handle a failed check (file needs updates).
648
+ # With --verbose, prints the per-file verdict and all change reasons.
407
649
  #
408
650
  # @private
409
- # @param [String] path
410
- # @param [Array<Hash>] file_changes
411
- # @param [String] display_path
412
- # @param [Hash] options
413
- # @param [Hash] state
651
+ # @param [String] path file path
652
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
653
+ # @param [String] display_path path shown in CLI output
654
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
655
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
414
656
  # @return [void]
415
657
  def handle_check_failed(path, file_changes:, display_path:, options:, state:)
416
658
  if options[:verbose]
417
- puts("FAIL #{display_path}")
418
- print_check_explanations(file_changes, options)
659
+ warn("FAIL #{display_path}")
660
+ print_check_explanations(file_changes)
419
661
  else
420
- print('F')
662
+ $stderr.print('F')
421
663
  end
422
664
 
423
665
  state[:checked_fail] += 1
@@ -432,66 +674,84 @@ module Docscribe
432
674
  # @param [String] path file path
433
675
  # @param [String] src original source code
434
676
  # @param [String] out rewritten source code
435
- # @param [Array<Hash>] file_changes structured change records
436
- # @param [String] display_path path shown in CLI output
437
- # @param [Hash] options CLI options
438
- # @param [Hash] state shared processing state
439
- # @param [Hash] ctx context hash with :display_path, :options, :state keys
677
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
678
+ # @param [Object] ctx context hash with :display_path, :options, :state keys
440
679
  # @raise [StandardError]
441
680
  # @return [void]
681
+ # @return [Object] if StandardError
442
682
  def handle_write_result(path, src:, out:, file_changes:, **ctx)
443
- if out == src
444
- log_check_verdict('OK', ctx[:display_path], ctx[:options])
445
- return
446
- end
683
+ return log_check_verdict('OK', ctx[:display_path], ctx[:options]) if out == src
447
684
 
448
- File.write(path, out)
449
- log_write_verdict('CHANGED', ctx[:display_path], file_changes, ctx[:options])
450
- ctx[:state][:corrected] += 1
685
+ apply_correction(path, out, file_changes, ctx)
451
686
  rescue StandardError => e
452
687
  record_write_error(path, e, display_path: ctx[:display_path], options: ctx[:options], state: ctx[:state])
453
688
  end
454
689
 
690
+ # Apply a file correction — write to disk, log, and update state.
691
+ #
692
+ # @private
693
+ # @param [String] path file path
694
+ # @param [String] out rewritten source code
695
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
696
+ # @param [Hash<Symbol, Object>] ctx context hash with :display_path, :options, :state keys
697
+ # @return [void]
698
+ def apply_correction(path, out, file_changes, ctx)
699
+ File.write(path, out)
700
+ log_write_verdict('CHANGED', ctx[:display_path], file_changes, ctx[:options])
701
+ update_correction_state(ctx[:state], ctx[:display_path], file_changes)
702
+ end
703
+
704
+ # Update the shared state after a successful correction.
705
+ #
706
+ # @private
707
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
708
+ # @param [String] display_path path shown in CLI output
709
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
710
+ # @return [void]
711
+ def update_correction_state(state, display_path, file_changes)
712
+ state[:corrected] += 1
713
+ state[:corrected_paths] << display_path
714
+ state[:corrected_changes][display_path] = file_changes
715
+ end
716
+
455
717
  # Log a write-mode verdict.
456
718
  #
457
719
  # @private
458
- # @param [String] verdict
459
- # @param [String] display_path
460
- # @param [Array<Hash>] file_changes
461
- # @param [Hash] options
720
+ # @param [String] verdict verdict string to display
721
+ # @param [String] display_path path shown in CLI output
722
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
723
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
462
724
  # @return [void]
463
725
  def log_write_verdict(verdict, display_path, file_changes, options)
464
726
  if options[:verbose]
465
- puts("#{verdict} #{display_path}")
466
- print_check_explanations(file_changes, options)
727
+ warn("#{verdict} #{display_path}")
728
+ print_check_explanations(file_changes)
467
729
  else
468
- print('C')
730
+ $stderr.print('C')
469
731
  end
470
732
  end
471
733
 
472
734
  # Print explanations for file changes.
473
735
  #
736
+ # Callers are responsible for gating on --verbose / --explain.
737
+ #
474
738
  # @private
475
- # @param [Array<Hash>] file_changes
476
- # @param [Hash] options
739
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
477
740
  # @return [void]
478
- def print_check_explanations(file_changes, options)
479
- return unless options[:explain]
480
-
741
+ def print_check_explanations(file_changes)
481
742
  file_changes.each do |change|
482
- puts(" - #{format_change_reason(change)}")
743
+ warn(" - #{format_change_reason(change)}")
483
744
  end
484
745
  end
485
746
 
486
747
  # Record a write error in state.
487
748
  #
488
749
  # @private
489
- # @param [String] path
490
- # @param [StandardError] e
491
- # @param [String] display_path
492
- # @param [Hash] options
493
- # @param [Hash] state
750
+ # @param [String] path file path
494
751
  # @param [StandardError] error the exception raised during file write
752
+ # @param [String] display_path path shown in CLI output
753
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
754
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
495
755
  # @return [void]
496
756
  def record_write_error(path, error, display_path:, options:, state:)
497
757
  state[:had_errors] = true
@@ -503,40 +763,60 @@ module Docscribe
503
763
  # Log a per-file check verdict.
504
764
  #
505
765
  # @private
506
- # @param [String] verdict
507
- # @param [String] display_path
508
- # @param [Hash] options
766
+ # @param [String] verdict verdict string to display
767
+ # @param [String] display_path path shown in CLI output
768
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
509
769
  # @return [void]
510
770
  def log_check_verdict(verdict, display_path, options)
511
771
  if options[:verbose]
512
- puts("#{verdict} #{display_path}")
772
+ warn("#{verdict} #{display_path}")
513
773
  else
514
- print(if verdict == 'FAIL'
515
- 'F'
516
- else
517
- verdict == 'MT' ? 'M' : '.'
518
- end)
774
+ $stderr.print(if verdict == 'FAIL'
775
+ 'F'
776
+ else
777
+ verdict == 'MT' ? 'M' : '.'
778
+ end)
519
779
  end
520
780
  end
521
781
 
522
- # Print the check-mode summary (files OK / need updates / errors).
782
+ # Print the check-mode summary (fail paths, then status line).
523
783
  #
524
784
  # @private
525
- # @param [Hash] state shared processing state
526
- # @param [Hash] options CLI options
785
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
786
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
527
787
  # @return [void]
528
788
  def print_check_summary(state:, options:)
529
789
  puts
530
- print_check_status_line(state)
531
790
  print_fail_paths(state, options)
791
+ print_check_status_line(state)
532
792
  print_type_mismatch_paths(state, options)
533
793
  print_error_paths(state)
534
794
  end
535
795
 
796
+ public
797
+
798
+ # Print fail paths from check summary (stdout).
799
+ #
800
+ # Skips explanations when --verbose showed them inline per-file.
801
+ #
802
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
803
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
804
+ # @return [void]
805
+ def print_fail_paths(state, options)
806
+ state[:fail_paths].each do |p|
807
+ puts "Would update: #{p}"
808
+
809
+ next if options[:verbose] || options[:quiet]
810
+
811
+ Array(state[:fail_changes][p]).each do |change|
812
+ puts " - #{format_change_reason(change)}"
813
+ end
814
+ end
815
+ end
816
+
536
817
  # Print the check-mode status line.
537
818
  #
538
- # @private
539
- # @param [Hash] state
819
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
540
820
  # @return [void]
541
821
  def print_check_status_line(state)
542
822
  checked_error = state[:error_paths].size
@@ -553,8 +833,7 @@ module Docscribe
553
833
 
554
834
  # Whether no failures, errors, or type mismatches occurred.
555
835
  #
556
- # @private
557
- # @param [Hash] state shared processing state
836
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
558
837
  # @param [Integer] checked_error number of files with errors
559
838
  # @param [Integer] type_mismatch_count number of files with type mismatches
560
839
  # @return [Boolean]
@@ -564,8 +843,7 @@ module Docscribe
564
843
 
565
844
  # Whether type mismatches exist but no failures or errors.
566
845
  #
567
- # @private
568
- # @param [Hash] state shared processing state
846
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
569
847
  # @param [Integer] checked_error number of files with errors
570
848
  # @return [Boolean]
571
849
  def mismatch_only?(state, checked_error)
@@ -574,8 +852,7 @@ module Docscribe
574
852
 
575
853
  # Build the human-readable failure summary line for check output.
576
854
  #
577
- # @private
578
- # @param [Hash] state shared processing state
855
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
579
856
  # @param [Integer] type_mismatch_count number of files with type mismatches
580
857
  # @param [Integer] checked_error number of files with errors
581
858
  # @return [String]
@@ -587,46 +864,61 @@ module Docscribe
587
864
  "Docscribe: FAILED (#{parts.join(', ')})"
588
865
  end
589
866
 
590
- public
591
-
592
- # Print fail paths from check summary.
867
+ # Print type mismatch paths from check summary.
593
868
  #
594
- # @private
595
- # @param [Hash] state
596
- # @param [Hash] options
869
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
870
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
597
871
  # @return [void]
598
- def print_fail_paths(state, options)
599
- state[:fail_paths].each do |p|
600
- warn "Would update docs: #{p}"
601
- next unless options[:explain] && !options[:verbose]
872
+ def print_type_mismatch_paths(state, options)
873
+ return if options[:quiet]
874
+ return unless options[:verbose] || options[:explain]
602
875
 
603
- Array(state[:fail_changes][p]).each do |change|
876
+ state[:type_mismatch_paths].each do |p|
877
+ warn "Type mismatches: #{p}"
878
+ Array(state[:type_mismatch_changes][p]).each do |change|
604
879
  warn " - #{format_change_reason(change)}"
605
880
  end
606
881
  end
607
882
  end
608
883
 
609
- # Print type mismatch paths from check summary.
884
+ # Print the write-mode summary (files corrected, errors).
610
885
  #
611
- # @private
612
- # @param [Hash] state
613
- # @param [Hash] options
886
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
887
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
614
888
  # @return [void]
615
- def print_type_mismatch_paths(state, options)
616
- return unless options[:verbose] || options[:explain]
889
+ def print_write_summary(state:, options:)
890
+ puts
891
+ puts "Docscribe: updated #{state[:corrected]} file(s)" if state[:corrected].positive?
892
+ print_corrected_paths(state, options)
617
893
 
618
- state[:type_mismatch_paths].each do |p|
619
- warn "Type mismatches: #{p}"
620
- Array(state[:type_mismatch_changes][p]).each do |change|
621
- warn " - #{format_change_reason(change)}"
894
+ return unless state[:had_errors]
895
+
896
+ warn "Docscribe: #{state[:error_paths].size} file(s) had errors"
897
+ print_error_paths(state)
898
+ end
899
+
900
+ # Print corrected paths from write-mode summary (stdout).
901
+ #
902
+ # Skips explanations when --verbose showed them inline per-file.
903
+ #
904
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
905
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
906
+ # @return [void]
907
+ def print_corrected_paths(state, options)
908
+ state[:corrected_paths].each do |p|
909
+ puts "Updated: #{p}"
910
+
911
+ next if options[:verbose] || options[:quiet]
912
+
913
+ Array(state[:corrected_changes][p]).each do |change|
914
+ puts " - #{format_change_reason(change)}"
622
915
  end
623
916
  end
624
917
  end
625
918
 
626
919
  # Format a structured change record into human-readable CLI output.
627
920
  #
628
- # @private
629
- # @param [Hash] change structured change produced by the inline rewriter
921
+ # @param [Docscribe::CLI::Formatters::change] change structured change produced by the inline rewriter
630
922
  # @return [String] human-readable explanation line
631
923
  def format_change_reason(change)
632
924
  line = change_line_suffix(change)
@@ -640,7 +932,7 @@ module Docscribe
640
932
 
641
933
  # Format the line number suffix for a change reason string.
642
934
  #
643
- # @param [Hash] change structured change record
935
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
644
936
  # @return [String] " at line N" or empty
645
937
  def change_line_suffix(change)
646
938
  change[:line] ? " at line #{change[:line]}" : ''
@@ -648,7 +940,7 @@ module Docscribe
648
940
 
649
941
  # Format the method name suffix for a change reason string.
650
942
  #
651
- # @param [Hash] change structured change record
943
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
652
944
  # @return [String] " for method_name" or empty
653
945
  def change_method_suffix(change)
654
946
  change[:method] ? " for #{change[:method]}" : ''
@@ -656,7 +948,7 @@ module Docscribe
656
948
 
657
949
  # Whether a change type uses its own :message field directly as the reason.
658
950
  #
659
- # @param [Hash] change structured change record
951
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
660
952
  # @return [Boolean]
661
953
  def direct_message_change?(change)
662
954
  %i[
@@ -669,27 +961,14 @@ module Docscribe
669
961
  ].include?(change[:type])
670
962
  end
671
963
 
672
- # Print the write-mode summary (files corrected, errors).
673
- #
674
- # @private
675
- # @param [Hash] state shared processing state
676
- # @return [void]
677
- def print_write_summary(state:)
678
- puts
679
- puts "Docscribe: updated #{state[:corrected]} file(s)" if state[:corrected].positive?
680
-
681
- return unless state[:had_errors]
682
-
683
- warn "Docscribe: #{state[:error_paths].size} file(s) had errors"
684
- print_error_paths(state)
685
- end
686
-
687
964
  # Print error paths from check summary.
688
965
  #
689
- # @private
690
- # @param [Hash] state
966
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
691
967
  # @return [void]
692
968
  def print_error_paths(state)
969
+ return if state[:error_paths].empty?
970
+
971
+ warn ''
693
972
  state[:error_paths].each do |p|
694
973
  warn "Error processing: #{p}"
695
974
  warn " #{state[:error_messages][p]}" if state[:error_messages][p]