docscribe 1.4.2 → 1.5.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +465 -130
  3. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  4. data/lib/docscribe/cli/config_builder.rb +107 -53
  5. data/lib/docscribe/cli/formatters/json.rb +294 -0
  6. data/lib/docscribe/cli/formatters/sarif.rb +235 -0
  7. data/lib/docscribe/cli/formatters/text.rb +208 -0
  8. data/lib/docscribe/cli/formatters.rb +26 -0
  9. data/lib/docscribe/cli/generate.rb +45 -45
  10. data/lib/docscribe/cli/init.rb +14 -6
  11. data/lib/docscribe/cli/options.rb +190 -88
  12. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  13. data/lib/docscribe/cli/run.rb +210 -152
  14. data/lib/docscribe/cli/sigs.rb +366 -0
  15. data/lib/docscribe/cli/update_types.rb +103 -0
  16. data/lib/docscribe/cli.rb +21 -13
  17. data/lib/docscribe/config/defaults.rb +5 -1
  18. data/lib/docscribe/config/emit.rb +17 -0
  19. data/lib/docscribe/config/filtering.rb +18 -25
  20. data/lib/docscribe/config/loader.rb +15 -11
  21. data/lib/docscribe/config/plugin.rb +1 -1
  22. data/lib/docscribe/config/rbs.rb +41 -9
  23. data/lib/docscribe/config/sorbet.rb +9 -12
  24. data/lib/docscribe/config/sorting.rb +1 -1
  25. data/lib/docscribe/config/template.rb +9 -1
  26. data/lib/docscribe/config/utils.rb +11 -9
  27. data/lib/docscribe/config.rb +2 -4
  28. data/lib/docscribe/infer/ast_walk.rb +1 -1
  29. data/lib/docscribe/infer/literals.rb +6 -11
  30. data/lib/docscribe/infer/names.rb +2 -3
  31. data/lib/docscribe/infer/params.rb +15 -17
  32. data/lib/docscribe/infer/raises.rb +3 -5
  33. data/lib/docscribe/infer/returns.rb +542 -140
  34. data/lib/docscribe/infer.rb +22 -23
  35. data/lib/docscribe/inline_rewriter/collector.rb +159 -164
  36. data/lib/docscribe/inline_rewriter/doc_block.rb +145 -115
  37. data/lib/docscribe/inline_rewriter/doc_builder.rb +1026 -723
  38. data/lib/docscribe/inline_rewriter/source_helpers.rb +49 -49
  39. data/lib/docscribe/inline_rewriter/tag_sorter.rb +82 -85
  40. data/lib/docscribe/inline_rewriter.rb +495 -492
  41. data/lib/docscribe/parsing.rb +29 -10
  42. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -1
  43. data/lib/docscribe/plugin/base/tag_plugin.rb +0 -1
  44. data/lib/docscribe/plugin/context.rb +28 -18
  45. data/lib/docscribe/plugin/registry.rb +26 -27
  46. data/lib/docscribe/plugin/tag.rb +9 -14
  47. data/lib/docscribe/plugin.rb +17 -16
  48. data/lib/docscribe/types/provider_chain.rb +4 -2
  49. data/lib/docscribe/types/rbs/collection_loader.rb +2 -2
  50. data/lib/docscribe/types/rbs/provider.rb +60 -44
  51. data/lib/docscribe/types/rbs/type_formatter.rb +224 -83
  52. data/lib/docscribe/types/signature.rb +22 -42
  53. data/lib/docscribe/types/sorbet/base_provider.rb +24 -19
  54. data/lib/docscribe/types/sorbet/rbi_provider.rb +3 -3
  55. data/lib/docscribe/types/sorbet/source_provider.rb +3 -2
  56. data/lib/docscribe/types/yard/formatter.rb +100 -0
  57. data/lib/docscribe/types/yard/parser.rb +240 -0
  58. data/lib/docscribe/types/yard/types.rb +52 -0
  59. data/lib/docscribe/version.rb +1 -1
  60. metadata +33 -1
@@ -3,6 +3,7 @@
3
3
  require 'pathname'
4
4
 
5
5
  require 'docscribe/cli/config_builder'
6
+ require 'docscribe/cli/formatters'
6
7
  require 'docscribe/inline_rewriter'
7
8
 
8
9
  module Docscribe
@@ -23,12 +24,16 @@ module Docscribe
23
24
  checked_ok: 0,
24
25
  checked_fail: 0,
25
26
  corrected: 0,
27
+ corrected_paths: [], #: Array[String]
28
+ corrected_changes: {}, #: Hash[String, untyped]
26
29
  fail_paths: [], #: Array[String]
27
30
  fail_changes: {}, #: Hash[String, untyped]
28
31
  error_paths: [], #: Array[String]
29
32
  error_messages: {}, #: Hash[String, String]
30
33
  type_mismatch_paths: [], #: Array[String]
31
- type_mismatch_changes: {} #: Hash[String, untyped]
34
+ type_mismatch_changes: {}, #: Hash[String, untyped]
35
+ total: 0,
36
+ processed: 0
32
37
  }.freeze
33
38
  # Run Docscribe for files or STDIN using the selected mode and strategy.
34
39
  #
@@ -41,7 +46,7 @@ module Docscribe
41
46
  # - :safe => merge/add/normalize non-destructively
42
47
  # - :aggressive => rebuild existing doc blocks
43
48
  #
44
- # @param [Hash] options parsed CLI options
49
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
45
50
  # @param [Array<String>] argv remaining path arguments
46
51
  # @return [Integer] process exit code
47
52
  def run(options:, argv:)
@@ -57,7 +62,7 @@ module Docscribe
57
62
 
58
63
  # Load and build the effective config from CLI options.
59
64
  #
60
- # @param [Hash] options parsed CLI options
65
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
61
66
  # @return [Docscribe::Config] effective config with plugins loaded
62
67
  def build_config(options)
63
68
  conf = Docscribe::Config.load(options[:config])
@@ -69,10 +74,11 @@ module Docscribe
69
74
  # Rewrite code from STDIN using the selected strategy and print the
70
75
  # result.
71
76
  #
72
- # @param [Hash] options parsed CLI options
77
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
73
78
  # @param [Docscribe::Config] conf effective config
74
79
  # @raise [StandardError]
75
- # @return [Integer] process exit code
80
+ # @return [Integer] if StandardError
81
+ # @return [Integer] if StandardError
76
82
  def run_stdin(options:, conf:)
77
83
  puts stdin_rewrite_result(options, conf)[:output]
78
84
  0
@@ -83,9 +89,9 @@ module Docscribe
83
89
 
84
90
  # Rewrite STDIN input and return the result report.
85
91
  #
86
- # @param [Hash] options parsed CLI options
92
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
87
93
  # @param [Docscribe::Config] conf effective config
88
- # @return [Hash] rewrite result with :output key
94
+ # @return [Hash<Symbol, Object>] rewrite result with :output key
89
95
  def stdin_rewrite_result(options, conf)
90
96
  Docscribe::InlineRewriter.rewrite_with_report(
91
97
  $stdin.read,
@@ -99,7 +105,7 @@ module Docscribe
99
105
  # Return the core RBS provider from the config if available.
100
106
  #
101
107
  # @param [Docscribe::Config] conf effective config
102
- # @return [Object, nil] core RBS provider or nil
108
+ # @return [Docscribe::Types::RBS::Provider, nil] core RBS provider or nil
103
109
  def core_rbs_provider_for(conf)
104
110
  conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
105
111
  end
@@ -115,10 +121,10 @@ module Docscribe
115
121
 
116
122
  # Warn and return exit code when no matching files were found.
117
123
  #
118
- # @return [Integer] exit code 1
124
+ # @return [Integer] exit code 2
119
125
  def no_files_found
120
126
  warn 'No files found. Pass files or directories (e.g. `docscribe lib`).'
121
- 1
127
+ 2
122
128
  end
123
129
 
124
130
  # Expand CLI path arguments into a sorted list of Ruby files.
@@ -164,7 +170,7 @@ module Docscribe
164
170
  # - rewrites changed files in place
165
171
  # - exits non-zero only if errors occurred
166
172
  #
167
- # @param [Hash] options parsed CLI options
173
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
168
174
  # @param [Docscribe::Config] conf effective config
169
175
  # @param [Array<String>] paths Ruby file paths to process
170
176
  # @return [Integer] process exit code
@@ -172,6 +178,7 @@ module Docscribe
172
178
  $stdout.sync = true
173
179
 
174
180
  state = initial_run_state
181
+ state[:total] = paths.size
175
182
  pwd = Pathname.pwd
176
183
 
177
184
  paths.each do |path|
@@ -188,25 +195,27 @@ module Docscribe
188
195
  # Print the check or write summary at the end of a run.
189
196
  #
190
197
  # @private
191
- # @param [Hash] options CLI options
192
- # @param [Hash] state shared processing state
198
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
199
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
193
200
  # @return [void]
194
201
  def finalize_run(options, state)
202
+ formatter = Formatters.for(options[:format])
203
+
195
204
  if options[:mode] == :check
196
- print_check_summary(state: state, options: options)
205
+ formatter.format_check_summary(state: state, options: options)
197
206
  elsif options[:mode] == :write
198
- print_write_summary(state: state)
207
+ formatter.format_write_summary(state: state, options: options)
199
208
  end
200
209
  end
201
210
 
202
211
  # Determine the process exit code based on run state and mode.
203
212
  #
204
213
  # @private
205
- # @param [Hash] options CLI options
206
- # @param [Hash] state shared processing state
207
- # @return [Integer] exit code 0 or 1
214
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
215
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
216
+ # @return [Integer] exit code: 0 = OK, 1 = findings, 2 = error
208
217
  def run_exit_code(options, state)
209
- return 1 if state[:had_errors]
218
+ return 2 if state[:had_errors]
210
219
  return 1 if options[:mode] == :check && state[:changed]
211
220
 
212
221
  0
@@ -215,7 +224,7 @@ module Docscribe
215
224
  # Initialize the shared state hash used throughout a run.
216
225
  #
217
226
  # @private
218
- # @return [Hash] initial state with counters and tracking arrays
227
+ # @return [Docscribe::CLI::Formatters::state] initial state with counters and tracking arrays
219
228
  def initial_run_state
220
229
  Marshal.load(Marshal.dump(INITIAL_RUN_STATE))
221
230
  end
@@ -224,13 +233,14 @@ module Docscribe
224
233
  #
225
234
  # @private
226
235
  # @param [String] path file path
227
- # @param [Hash] options CLI options
236
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
228
237
  # @param [Docscribe::Config] conf configuration
229
238
  # @param [Pathname] pwd current working directory
230
- # @param [Hash] state shared processing state
239
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
231
240
  # @return [void]
232
241
  def process_one_file(path, options:, conf:, pwd:, state:)
233
242
  display_path = display_path_for(path, pwd: pwd)
243
+ report_progress(state, options, display_path)
234
244
 
235
245
  src = read_source_for_path(path, display_path: display_path, options: options, state: state)
236
246
  return unless src
@@ -243,14 +253,28 @@ module Docscribe
243
253
  display_path: display_path, options: options, state: state)
244
254
  end
245
255
 
256
+ # Print progress indicator to stderr when --progress is active.
257
+ #
258
+ # @private
259
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
260
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
261
+ # @param [String] display_path path to display
262
+ # @return [void]
263
+ def report_progress(state, options, display_path)
264
+ state[:processed] += 1
265
+ return unless options[:progress]
266
+
267
+ warn "[#{state[:processed]}/#{state[:total]}] #{display_path}"
268
+ end
269
+
246
270
  # Dispatch the rewrite result to the check or write handler based on mode.
247
271
  #
248
272
  # @private
249
273
  # @param [String] path file path
250
274
  # @param [String] src original source code
251
275
  # @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
276
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
277
+ # @param [Object] ctx context hash with :options, :state, :display_path, :conf
254
278
  # @return [void]
255
279
  def dispatch_file_result(path, src:, out:, file_changes:, **ctx)
256
280
  if ctx[:options][:mode] == :check
@@ -269,7 +293,8 @@ module Docscribe
269
293
  # @param [String] path file path to display
270
294
  # @param [Pathname] pwd current working directory
271
295
  # @raise [StandardError]
272
- # @return [String] path shown in CLI output
296
+ # @return [String] if StandardError
297
+ # @return [Object] if StandardError
273
298
  def display_path_for(path, pwd:)
274
299
  abs = Pathname.new(path).expand_path
275
300
 
@@ -287,17 +312,18 @@ module Docscribe
287
312
  # @private
288
313
  # @param [String] path file path to read
289
314
  # @param [String] display_path path shown in CLI output
290
- # @param [Hash] options CLI options
291
- # @param [Hash] state shared processing state
315
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
316
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
292
317
  # @raise [StandardError]
293
- # @return [String, nil] file contents or nil on error
318
+ # @return [String, nil] if StandardError
319
+ # @return [nil] if StandardError
294
320
  def read_source_for_path(path, display_path:, options:, state:)
295
321
  File.read(path)
296
322
  rescue StandardError => e
297
323
  state[:had_errors] = true
298
324
  state[:error_paths] << path
299
325
  state[:error_messages][path] = "#{e.class}: #{e.message}"
300
- options[:verbose] ? warn("ERR #{display_path}: #{state[:error_messages][path]}") : print('E')
326
+ options[:verbose] ? warn("ERR #{display_path}: #{state[:error_messages][path]}") : $stderr.print('E')
301
327
  nil
302
328
  end
303
329
 
@@ -306,13 +332,10 @@ module Docscribe
306
332
  # @private
307
333
  # @param [String] path file path
308
334
  # @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
335
+ # @param [Hash<Symbol, Object>] ctx context hash with :conf, :display_path, :options, :state keys
314
336
  # @raise [StandardError]
315
- # @return [Hash, nil] rewrite result or nil on error
337
+ # @return [Hash<Symbol, Object>, nil] if StandardError
338
+ # @return [nil] if StandardError
316
339
  def rewrite_result_for_path(path, src:, ctx:)
317
340
  conf = ctx[:conf]
318
341
 
@@ -332,7 +355,7 @@ module Docscribe
332
355
  # @private
333
356
  # @param [String] path file path that caused the error
334
357
  # @param [StandardError] error the exception raised during rewriting
335
- # @param [Hash] ctx context hash with :state, :options, :display_path
358
+ # @param [Hash<Symbol, Object>] ctx context hash with :state, :options, :display_path
336
359
  # @return [void]
337
360
  def record_rewrite_error(path, error, ctx)
338
361
  state = ctx[:state]
@@ -344,7 +367,7 @@ module Docscribe
344
367
  if ctx[:options][:verbose]
345
368
  warn "ERR #{ctx[:display_path]}: #{state[:error_messages][path]}"
346
369
  else
347
- print('E')
370
+ $stderr.print('E')
348
371
  end
349
372
  end
350
373
 
@@ -354,11 +377,8 @@ module Docscribe
354
377
  # @param [String] path file path
355
378
  # @param [String] src original source code
356
379
  # @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
380
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
381
+ # @param [Object] ctx context hash with :display_path, :options, :state keys
362
382
  # @return [void]
363
383
  def handle_check_result(path, src:, out:, file_changes:, **ctx)
364
384
  type_mismatches = type_mismatch_changes(file_changes)
@@ -377,8 +397,8 @@ module Docscribe
377
397
  # Extract type mismatch changes from file_changes.
378
398
  #
379
399
  # @private
380
- # @param [Array<Hash>] file_changes
381
- # @return [Array<Hash>]
400
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
401
+ # @return [Array<Docscribe::CLI::Formatters::change>]
382
402
  def type_mismatch_changes(file_changes)
383
403
  file_changes.select { |c| %i[updated_param updated_return].include?(c[:type]) }
384
404
  end
@@ -386,11 +406,11 @@ module Docscribe
386
406
  # Handle check result when there are no real changes.
387
407
  #
388
408
  # @private
389
- # @param [String] path
390
- # @param [Array<Hash>] type_mismatches
391
- # @param [String] display_path
392
- # @param [Hash] options
393
- # @param [Hash] state
409
+ # @param [String] path file path
410
+ # @param [Array<Docscribe::CLI::Formatters::change>] type_mismatches type mismatch changes to record
411
+ # @param [String] display_path path shown in CLI output
412
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
413
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
394
414
  # @return [void]
395
415
  def handle_check_no_changes(path, type_mismatches:, display_path:, options:, state:)
396
416
  if type_mismatches.any?
@@ -404,20 +424,21 @@ module Docscribe
404
424
  end
405
425
 
406
426
  # Handle a failed check (file needs updates).
427
+ # With --verbose, prints the per-file verdict and all change reasons.
407
428
  #
408
429
  # @private
409
- # @param [String] path
410
- # @param [Array<Hash>] file_changes
411
- # @param [String] display_path
412
- # @param [Hash] options
413
- # @param [Hash] state
430
+ # @param [String] path file path
431
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
432
+ # @param [String] display_path path shown in CLI output
433
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
434
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
414
435
  # @return [void]
415
436
  def handle_check_failed(path, file_changes:, display_path:, options:, state:)
416
437
  if options[:verbose]
417
- puts("FAIL #{display_path}")
418
- print_check_explanations(file_changes, options)
438
+ warn("FAIL #{display_path}")
439
+ print_check_explanations(file_changes)
419
440
  else
420
- print('F')
441
+ $stderr.print('F')
421
442
  end
422
443
 
423
444
  state[:checked_fail] += 1
@@ -432,66 +453,84 @@ module Docscribe
432
453
  # @param [String] path file path
433
454
  # @param [String] src original source code
434
455
  # @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
456
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
457
+ # @param [Object] ctx context hash with :display_path, :options, :state keys
440
458
  # @raise [StandardError]
441
- # @return [void]
459
+ # @return [void] if StandardError
460
+ # @return [Object] if StandardError
442
461
  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
462
+ return log_check_verdict('OK', ctx[:display_path], ctx[:options]) if out == src
447
463
 
448
- File.write(path, out)
449
- log_write_verdict('CHANGED', ctx[:display_path], file_changes, ctx[:options])
450
- ctx[:state][:corrected] += 1
464
+ apply_correction(path, out, file_changes, ctx)
451
465
  rescue StandardError => e
452
466
  record_write_error(path, e, display_path: ctx[:display_path], options: ctx[:options], state: ctx[:state])
453
467
  end
454
468
 
469
+ # Apply a file correction — write to disk, log, and update state.
470
+ #
471
+ # @private
472
+ # @param [String] path file path
473
+ # @param [String] out rewritten source code
474
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
475
+ # @param [Hash<Symbol, Object>] ctx context hash with :display_path, :options, :state keys
476
+ # @return [void]
477
+ def apply_correction(path, out, file_changes, ctx)
478
+ File.write(path, out)
479
+ log_write_verdict('CHANGED', ctx[:display_path], file_changes, ctx[:options])
480
+ update_correction_state(ctx[:state], ctx[:display_path], file_changes)
481
+ end
482
+
483
+ # Update the shared state after a successful correction.
484
+ #
485
+ # @private
486
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
487
+ # @param [String] display_path path shown in CLI output
488
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
489
+ # @return [void]
490
+ def update_correction_state(state, display_path, file_changes)
491
+ state[:corrected] += 1
492
+ state[:corrected_paths] << display_path
493
+ state[:corrected_changes][display_path] = file_changes
494
+ end
495
+
455
496
  # Log a write-mode verdict.
456
497
  #
457
498
  # @private
458
- # @param [String] verdict
459
- # @param [String] display_path
460
- # @param [Array<Hash>] file_changes
461
- # @param [Hash] options
499
+ # @param [String] verdict verdict string to display
500
+ # @param [String] display_path path shown in CLI output
501
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
502
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
462
503
  # @return [void]
463
504
  def log_write_verdict(verdict, display_path, file_changes, options)
464
505
  if options[:verbose]
465
- puts("#{verdict} #{display_path}")
466
- print_check_explanations(file_changes, options)
506
+ warn("#{verdict} #{display_path}")
507
+ print_check_explanations(file_changes)
467
508
  else
468
- print('C')
509
+ $stderr.print('C')
469
510
  end
470
511
  end
471
512
 
472
513
  # Print explanations for file changes.
473
514
  #
515
+ # Callers are responsible for gating on --verbose / --explain.
516
+ #
474
517
  # @private
475
- # @param [Array<Hash>] file_changes
476
- # @param [Hash] options
518
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
477
519
  # @return [void]
478
- def print_check_explanations(file_changes, options)
479
- return unless options[:explain]
480
-
520
+ def print_check_explanations(file_changes)
481
521
  file_changes.each do |change|
482
- puts(" - #{format_change_reason(change)}")
522
+ warn(" - #{format_change_reason(change)}")
483
523
  end
484
524
  end
485
525
 
486
526
  # Record a write error in state.
487
527
  #
488
528
  # @private
489
- # @param [String] path
490
- # @param [StandardError] e
491
- # @param [String] display_path
492
- # @param [Hash] options
493
- # @param [Hash] state
529
+ # @param [String] path file path
494
530
  # @param [StandardError] error the exception raised during file write
531
+ # @param [String] display_path path shown in CLI output
532
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
533
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
495
534
  # @return [void]
496
535
  def record_write_error(path, error, display_path:, options:, state:)
497
536
  state[:had_errors] = true
@@ -503,40 +542,60 @@ module Docscribe
503
542
  # Log a per-file check verdict.
504
543
  #
505
544
  # @private
506
- # @param [String] verdict
507
- # @param [String] display_path
508
- # @param [Hash] options
545
+ # @param [String] verdict verdict string to display
546
+ # @param [String] display_path path shown in CLI output
547
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
509
548
  # @return [void]
510
549
  def log_check_verdict(verdict, display_path, options)
511
550
  if options[:verbose]
512
- puts("#{verdict} #{display_path}")
551
+ warn("#{verdict} #{display_path}")
513
552
  else
514
- print(if verdict == 'FAIL'
515
- 'F'
516
- else
517
- verdict == 'MT' ? 'M' : '.'
518
- end)
553
+ $stderr.print(if verdict == 'FAIL'
554
+ 'F'
555
+ else
556
+ verdict == 'MT' ? 'M' : '.'
557
+ end)
519
558
  end
520
559
  end
521
560
 
522
- # Print the check-mode summary (files OK / need updates / errors).
561
+ # Print the check-mode summary (fail paths, then status line).
523
562
  #
524
563
  # @private
525
- # @param [Hash] state shared processing state
526
- # @param [Hash] options CLI options
564
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
565
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
527
566
  # @return [void]
528
567
  def print_check_summary(state:, options:)
529
568
  puts
530
- print_check_status_line(state)
531
569
  print_fail_paths(state, options)
570
+ print_check_status_line(state)
532
571
  print_type_mismatch_paths(state, options)
533
572
  print_error_paths(state)
534
573
  end
535
574
 
575
+ public
576
+
577
+ # Print fail paths from check summary (stdout).
578
+ #
579
+ # Skips explanations when --verbose showed them inline per-file.
580
+ #
581
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
582
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
583
+ # @return [void]
584
+ def print_fail_paths(state, options)
585
+ state[:fail_paths].each do |p|
586
+ puts "Would update: #{p}"
587
+
588
+ next if options[:verbose] || options[:quiet]
589
+
590
+ Array(state[:fail_changes][p]).each do |change|
591
+ puts " - #{format_change_reason(change)}"
592
+ end
593
+ end
594
+ end
595
+
536
596
  # Print the check-mode status line.
537
597
  #
538
- # @private
539
- # @param [Hash] state
598
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
540
599
  # @return [void]
541
600
  def print_check_status_line(state)
542
601
  checked_error = state[:error_paths].size
@@ -553,8 +612,7 @@ module Docscribe
553
612
 
554
613
  # Whether no failures, errors, or type mismatches occurred.
555
614
  #
556
- # @private
557
- # @param [Hash] state shared processing state
615
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
558
616
  # @param [Integer] checked_error number of files with errors
559
617
  # @param [Integer] type_mismatch_count number of files with type mismatches
560
618
  # @return [Boolean]
@@ -564,8 +622,7 @@ module Docscribe
564
622
 
565
623
  # Whether type mismatches exist but no failures or errors.
566
624
  #
567
- # @private
568
- # @param [Hash] state shared processing state
625
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
569
626
  # @param [Integer] checked_error number of files with errors
570
627
  # @return [Boolean]
571
628
  def mismatch_only?(state, checked_error)
@@ -574,8 +631,7 @@ module Docscribe
574
631
 
575
632
  # Build the human-readable failure summary line for check output.
576
633
  #
577
- # @private
578
- # @param [Hash] state shared processing state
634
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
579
635
  # @param [Integer] type_mismatch_count number of files with type mismatches
580
636
  # @param [Integer] checked_error number of files with errors
581
637
  # @return [String]
@@ -587,46 +643,61 @@ module Docscribe
587
643
  "Docscribe: FAILED (#{parts.join(', ')})"
588
644
  end
589
645
 
590
- public
591
-
592
- # Print fail paths from check summary.
646
+ # Print type mismatch paths from check summary.
593
647
  #
594
- # @private
595
- # @param [Hash] state
596
- # @param [Hash] options
648
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
649
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
597
650
  # @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]
651
+ def print_type_mismatch_paths(state, options)
652
+ return if options[:quiet]
653
+ return unless options[:verbose] || options[:explain]
602
654
 
603
- Array(state[:fail_changes][p]).each do |change|
655
+ state[:type_mismatch_paths].each do |p|
656
+ warn "Type mismatches: #{p}"
657
+ Array(state[:type_mismatch_changes][p]).each do |change|
604
658
  warn " - #{format_change_reason(change)}"
605
659
  end
606
660
  end
607
661
  end
608
662
 
609
- # Print type mismatch paths from check summary.
663
+ # Print the write-mode summary (files corrected, errors).
610
664
  #
611
- # @private
612
- # @param [Hash] state
613
- # @param [Hash] options
665
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
666
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
614
667
  # @return [void]
615
- def print_type_mismatch_paths(state, options)
616
- return unless options[:verbose] || options[:explain]
668
+ def print_write_summary(state:, options:)
669
+ puts
670
+ puts "Docscribe: updated #{state[:corrected]} file(s)" if state[:corrected].positive?
671
+ print_corrected_paths(state, options)
617
672
 
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)}"
673
+ return unless state[:had_errors]
674
+
675
+ warn "Docscribe: #{state[:error_paths].size} file(s) had errors"
676
+ print_error_paths(state)
677
+ end
678
+
679
+ # Print corrected paths from write-mode summary (stdout).
680
+ #
681
+ # Skips explanations when --verbose showed them inline per-file.
682
+ #
683
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
684
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
685
+ # @return [void]
686
+ def print_corrected_paths(state, options)
687
+ state[:corrected_paths].each do |p|
688
+ puts "Updated: #{p}"
689
+
690
+ next if options[:verbose] || options[:quiet]
691
+
692
+ Array(state[:corrected_changes][p]).each do |change|
693
+ puts " - #{format_change_reason(change)}"
622
694
  end
623
695
  end
624
696
  end
625
697
 
626
698
  # Format a structured change record into human-readable CLI output.
627
699
  #
628
- # @private
629
- # @param [Hash] change structured change produced by the inline rewriter
700
+ # @param [Docscribe::CLI::Formatters::change] change structured change produced by the inline rewriter
630
701
  # @return [String] human-readable explanation line
631
702
  def format_change_reason(change)
632
703
  line = change_line_suffix(change)
@@ -640,7 +711,7 @@ module Docscribe
640
711
 
641
712
  # Format the line number suffix for a change reason string.
642
713
  #
643
- # @param [Hash] change structured change record
714
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
644
715
  # @return [String] " at line N" or empty
645
716
  def change_line_suffix(change)
646
717
  change[:line] ? " at line #{change[:line]}" : ''
@@ -648,7 +719,7 @@ module Docscribe
648
719
 
649
720
  # Format the method name suffix for a change reason string.
650
721
  #
651
- # @param [Hash] change structured change record
722
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
652
723
  # @return [String] " for method_name" or empty
653
724
  def change_method_suffix(change)
654
725
  change[:method] ? " for #{change[:method]}" : ''
@@ -656,7 +727,7 @@ module Docscribe
656
727
 
657
728
  # Whether a change type uses its own :message field directly as the reason.
658
729
  #
659
- # @param [Hash] change structured change record
730
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
660
731
  # @return [Boolean]
661
732
  def direct_message_change?(change)
662
733
  %i[
@@ -669,27 +740,14 @@ module Docscribe
669
740
  ].include?(change[:type])
670
741
  end
671
742
 
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
743
  # Print error paths from check summary.
688
744
  #
689
- # @private
690
- # @param [Hash] state
745
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
691
746
  # @return [void]
692
747
  def print_error_paths(state)
748
+ return if state[:error_paths].empty?
749
+
750
+ warn ''
693
751
  state[:error_paths].each do |p|
694
752
  warn "Error processing: #{p}"
695
753
  warn " #{state[:error_messages][p]}" if state[:error_messages][p]