docscribe 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +149 -0
  3. data/lib/docscribe/cli/config_builder.rb +125 -35
  4. data/lib/docscribe/cli/generate.rb +288 -117
  5. data/lib/docscribe/cli/init.rb +49 -13
  6. data/lib/docscribe/cli/options.rb +302 -127
  7. data/lib/docscribe/cli/run.rb +391 -135
  8. data/lib/docscribe/cli.rb +23 -5
  9. data/lib/docscribe/config/defaults.rb +11 -11
  10. data/lib/docscribe/config/emit.rb +1 -0
  11. data/lib/docscribe/config/filtering.rb +24 -11
  12. data/lib/docscribe/config/loader.rb +7 -4
  13. data/lib/docscribe/config/plugin.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +31 -22
  15. data/lib/docscribe/config/sorbet.rb +41 -15
  16. data/lib/docscribe/config/sorting.rb +1 -0
  17. data/lib/docscribe/config/template.rb +1 -0
  18. data/lib/docscribe/config/utils.rb +1 -0
  19. data/lib/docscribe/config.rb +1 -0
  20. data/lib/docscribe/infer/constants.rb +15 -0
  21. data/lib/docscribe/infer/literals.rb +43 -25
  22. data/lib/docscribe/infer/names.rb +24 -15
  23. data/lib/docscribe/infer/params.rb +52 -6
  24. data/lib/docscribe/infer/raises.rb +24 -14
  25. data/lib/docscribe/infer/returns.rb +365 -182
  26. data/lib/docscribe/infer.rb +10 -9
  27. data/lib/docscribe/inline_rewriter/collector.rb +766 -375
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +217 -74
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +1488 -602
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +100 -52
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +109 -48
  32. data/lib/docscribe/inline_rewriter.rb +1009 -595
  33. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -3
  34. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -1
  35. data/lib/docscribe/plugin/registry.rb +34 -7
  36. data/lib/docscribe/plugin.rb +48 -17
  37. data/lib/docscribe/types/rbs/collection_loader.rb +0 -1
  38. data/lib/docscribe/types/rbs/provider.rb +75 -26
  39. data/lib/docscribe/types/rbs/type_formatter.rb +127 -59
  40. data/lib/docscribe/types/sorbet/base_provider.rb +31 -12
  41. data/lib/docscribe/version.rb +1 -1
  42. metadata +2 -2
@@ -17,6 +17,19 @@ module Docscribe
17
17
  # - process exit status
18
18
  module Run
19
19
  class << self
20
+ INITIAL_RUN_STATE = {
21
+ changed: false,
22
+ had_errors: false,
23
+ checked_ok: 0,
24
+ checked_fail: 0,
25
+ corrected: 0,
26
+ fail_paths: [], #: Array[String]
27
+ fail_changes: {}, #: Hash[String, untyped]
28
+ error_paths: [], #: Array[String]
29
+ error_messages: {}, #: Hash[String, String]
30
+ type_mismatch_paths: [], #: Array[String]
31
+ type_mismatch_changes: {} #: Hash[String, untyped]
32
+ }.freeze
20
33
  # Run Docscribe for files or STDIN using the selected mode and strategy.
21
34
  #
22
35
  # Modes:
@@ -32,23 +45,27 @@ module Docscribe
32
45
  # @param [Array<String>] argv remaining path arguments
33
46
  # @return [Integer] process exit code
34
47
  def run(options:, argv:)
35
- conf = Docscribe::Config.load(options[:config])
36
- conf = Docscribe::CLI::ConfigBuilder.build(conf, options)
37
- conf.load_plugins!
48
+ conf = build_config(options)
38
49
 
39
50
  return run_stdin(options: options, conf: conf) if options[:mode] == :stdin
40
51
 
41
- paths = expand_paths(argv)
42
- paths = paths.select { |p| conf.process_file?(p) }
43
-
44
- if paths.empty?
45
- warn 'No files found. Pass files or directories (e.g. `docscribe lib`).'
46
- return 1
47
- end
52
+ paths = filtered_paths(argv, conf)
53
+ return no_files_found unless paths.any?
48
54
 
49
55
  run_files(options: options, conf: conf, paths: paths)
50
56
  end
51
57
 
58
+ # Load and build the effective config from CLI options.
59
+ #
60
+ # @param [Hash] options parsed CLI options
61
+ # @return [Docscribe::Config] effective config with plugins loaded
62
+ def build_config(options)
63
+ conf = Docscribe::Config.load(options[:config])
64
+ conf = Docscribe::CLI::ConfigBuilder.build(conf, options)
65
+ conf.load_plugins!
66
+ conf
67
+ end
68
+
52
69
  # Rewrite code from STDIN using the selected strategy and print the
53
70
  # result.
54
71
  #
@@ -57,18 +74,50 @@ module Docscribe
57
74
  # @raise [StandardError]
58
75
  # @return [Integer] process exit code
59
76
  def run_stdin(options:, conf:)
60
- code = $stdin.read
61
- result = Docscribe::InlineRewriter.rewrite_with_report(
62
- code,
77
+ puts stdin_rewrite_result(options, conf)[:output]
78
+ 0
79
+ rescue StandardError => e
80
+ warn "Docscribe: Error processing stdin: #{e.class}: #{e.message}"
81
+ 1
82
+ end
83
+
84
+ # Rewrite STDIN input and return the result report.
85
+ #
86
+ # @param [Hash] options parsed CLI options
87
+ # @param [Docscribe::Config] conf effective config
88
+ # @return [Hash] rewrite result with :output key
89
+ def stdin_rewrite_result(options, conf)
90
+ Docscribe::InlineRewriter.rewrite_with_report(
91
+ $stdin.read,
63
92
  strategy: options[:strategy],
64
93
  config: conf,
65
- core_rbs_provider: conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil,
94
+ core_rbs_provider: core_rbs_provider_for(conf),
66
95
  file: '(stdin)'
67
96
  )
68
- puts result[:output]
69
- 0
70
- rescue StandardError => e
71
- warn "Docscribe: Error processing stdin: #{e.class}: #{e.message}"
97
+ end
98
+
99
+ # Return the core RBS provider from the config if available.
100
+ #
101
+ # @param [Docscribe::Config] conf effective config
102
+ # @return [Object, nil] core RBS provider or nil
103
+ def core_rbs_provider_for(conf)
104
+ conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
105
+ end
106
+
107
+ # Expand CLI path arguments and filter through config file patterns.
108
+ #
109
+ # @param [Array<String>] argv CLI path arguments
110
+ # @param [Docscribe::Config] conf effective config
111
+ # @return [Array<String>] filtered Ruby file paths
112
+ def filtered_paths(argv, conf)
113
+ expand_paths(argv).select { |path| conf.process_file?(path) }
114
+ end
115
+
116
+ # Warn and return exit code when no matching files were found.
117
+ #
118
+ # @return [Integer] exit code 1
119
+ def no_files_found
120
+ warn 'No files found. Pass files or directories (e.g. `docscribe lib`).'
72
121
  1
73
122
  end
74
123
 
@@ -80,22 +129,31 @@ module Docscribe
80
129
  # @param [Array<String>] args file and/or directory arguments
81
130
  # @return [Array<String>] unique sorted Ruby file paths
82
131
  def expand_paths(args)
83
- files = []
132
+ files = [] #: Array[String]
84
133
  args = ['.'] if args.empty?
85
134
 
86
135
  args.each do |path|
87
- if File.directory?(path)
88
- files.concat(Dir.glob(File.join(path, '**', '*.rb')))
89
- elsif File.file?(path)
90
- files << path
91
- else
92
- warn "Skipping missing path: #{path}"
93
- end
136
+ append_expanded_path(files, path)
94
137
  end
95
138
 
96
139
  files.uniq.sort
97
140
  end
98
141
 
142
+ # Append a file or recursively expand a directory into the files array.
143
+ #
144
+ # @param [Array<String>] files mutable file path accumulator
145
+ # @param [String] path file or directory path to expand
146
+ # @return [void]
147
+ def append_expanded_path(files, path)
148
+ if File.directory?(path)
149
+ files.concat(Dir.glob(File.join(path, '**', '*.rb')))
150
+ elsif File.file?(path)
151
+ files << path
152
+ else
153
+ warn "Skipping missing path: #{path}"
154
+ end
155
+ end
156
+
99
157
  # Process file paths in inspect or write mode.
100
158
  #
101
159
  # In inspect mode:
@@ -120,38 +178,46 @@ module Docscribe
120
178
  process_one_file(path, options: options, conf: conf, pwd: pwd, state: state)
121
179
  end
122
180
 
181
+ finalize_run(options, state)
182
+
183
+ run_exit_code(options, state)
184
+ end
185
+
186
+ private
187
+
188
+ # Print the check or write summary at the end of a run.
189
+ #
190
+ # @private
191
+ # @param [Hash] options CLI options
192
+ # @param [Hash] state shared processing state
193
+ # @return [void]
194
+ def finalize_run(options, state)
123
195
  if options[:mode] == :check
124
196
  print_check_summary(state: state, options: options)
125
197
  elsif options[:mode] == :write
126
198
  print_write_summary(state: state)
127
199
  end
200
+ end
128
201
 
202
+ # Determine the process exit code based on run state and mode.
203
+ #
204
+ # @private
205
+ # @param [Hash] options CLI options
206
+ # @param [Hash] state shared processing state
207
+ # @return [Integer] exit code 0 or 1
208
+ def run_exit_code(options, state)
129
209
  return 1 if state[:had_errors]
130
210
  return 1 if options[:mode] == :check && state[:changed]
131
211
 
132
212
  0
133
213
  end
134
214
 
135
- private
136
-
137
215
  # Initialize the shared state hash used throughout a run.
138
216
  #
139
217
  # @private
140
218
  # @return [Hash] initial state with counters and tracking arrays
141
219
  def initial_run_state
142
- {
143
- changed: false,
144
- had_errors: false,
145
- checked_ok: 0,
146
- checked_fail: 0,
147
- corrected: 0,
148
- fail_paths: [],
149
- fail_changes: {},
150
- error_paths: [],
151
- error_messages: {},
152
- type_mismatch_paths: [],
153
- type_mismatch_changes: {}
154
- }
220
+ Marshal.load(Marshal.dump(INITIAL_RUN_STATE))
155
221
  end
156
222
 
157
223
  # Process a single file: read, rewrite, and dispatch to check/write handler.
@@ -169,33 +235,28 @@ module Docscribe
169
235
  src = read_source_for_path(path, display_path: display_path, options: options, state: state)
170
236
  return unless src
171
237
 
172
- result = rewrite_result_for_path(path, src: src, conf: conf, display_path: display_path, options: options,
173
- state: state)
238
+ ctx = { conf: conf, display_path: display_path, options: options, state: state }
239
+ result = rewrite_result_for_path(path, src: src, ctx: ctx)
174
240
  return unless result
175
241
 
176
- out = result[:output]
177
- file_changes = result[:changes] || []
242
+ dispatch_file_result(path, src: src, out: result[:output], file_changes: result[:changes] || [],
243
+ display_path: display_path, options: options, state: state)
244
+ end
178
245
 
179
- if options[:mode] == :check
180
- handle_check_result(
181
- path,
182
- src: src,
183
- out: out,
184
- file_changes: file_changes,
185
- display_path: display_path,
186
- options: options,
187
- state: state
188
- )
189
- elsif options[:mode] == :write
190
- handle_write_result(
191
- path,
192
- src: src,
193
- out: out,
194
- file_changes: file_changes,
195
- display_path: display_path,
196
- options: options,
197
- state: state
198
- )
246
+ # Dispatch the rewrite result to the check or write handler based on mode.
247
+ #
248
+ # @private
249
+ # @param [String] path file path
250
+ # @param [String] src original source code
251
+ # @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
254
+ # @return [void]
255
+ def dispatch_file_result(path, src:, out:, file_changes:, **ctx)
256
+ if ctx[:options][:mode] == :check
257
+ handle_check_result(path, src: src, out: out, file_changes: file_changes, **ctx)
258
+ elsif ctx[:options][:mode] == :write
259
+ handle_write_result(path, src: src, out: out, file_changes: file_changes, **ctx)
199
260
  end
200
261
  end
201
262
 
@@ -249,23 +310,42 @@ module Docscribe
249
310
  # @param [String] display_path path shown in CLI output
250
311
  # @param [Hash] options CLI options
251
312
  # @param [Hash] state shared processing state
313
+ # @param [Hash] ctx context hash with :conf, :display_path, :options, :state keys
252
314
  # @raise [StandardError]
253
315
  # @return [Hash, nil] rewrite result or nil on error
254
- def rewrite_result_for_path(path, src:, conf:, display_path:, options:, state:)
255
- core_rbs_provider = conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
316
+ def rewrite_result_for_path(path, src:, ctx:)
317
+ conf = ctx[:conf]
318
+
319
+ core_rbs_provider =
320
+ conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
321
+
256
322
  Docscribe::InlineRewriter.rewrite_with_report(
257
- src,
258
- strategy: options[:strategy],
259
- config: conf,
260
- core_rbs_provider: core_rbs_provider,
261
- file: path
323
+ src, strategy: ctx[:options][:strategy], config: conf, core_rbs_provider: core_rbs_provider, file: path
262
324
  )
263
325
  rescue StandardError => e
326
+ record_rewrite_error(path, e, ctx)
327
+ nil
328
+ end
329
+
330
+ # Record a rewrite error in the shared state and print an error indicator.
331
+ #
332
+ # @private
333
+ # @param [String] path file path that caused the error
334
+ # @param [StandardError] error the exception raised during rewriting
335
+ # @param [Hash] ctx context hash with :state, :options, :display_path
336
+ # @return [void]
337
+ def record_rewrite_error(path, error, ctx)
338
+ state = ctx[:state]
339
+
264
340
  state[:had_errors] = true
265
341
  state[:error_paths] << path
266
- state[:error_messages][path] = "#{e.class}: #{e.message}"
267
- options[:verbose] ? warn("ERR #{display_path}: #{state[:error_messages][path]}") : print('E')
268
- nil
342
+ state[:error_messages][path] = "#{error.class}: #{error.message}"
343
+
344
+ if ctx[:options][:verbose]
345
+ warn "ERR #{ctx[:display_path]}: #{state[:error_messages][path]}"
346
+ else
347
+ print('E')
348
+ end
269
349
  end
270
350
 
271
351
  # Handle the result of an inspect (check) run.
@@ -278,30 +358,64 @@ module Docscribe
278
358
  # @param [String] display_path path shown in CLI output
279
359
  # @param [Hash] options CLI options
280
360
  # @param [Hash] state shared processing state
361
+ # @param [Hash] ctx context hash with :display_path, :options, :state keys
281
362
  # @return [void]
282
- def handle_check_result(path, src:, out:, file_changes:, display_path:, options:, state:)
283
- type_mismatches = file_changes.select { |c| %i[updated_param updated_return].include?(c[:type]) }
363
+ def handle_check_result(path, src:, out:, file_changes:, **ctx)
364
+ type_mismatches = type_mismatch_changes(file_changes)
284
365
  has_real_changes = file_changes.any? { |c| !%i[updated_param updated_return].include?(c[:type]) }
285
366
 
286
367
  if out == src && !has_real_changes
287
- if type_mismatches.any?
288
- state[:type_mismatch_paths] << path
289
- state[:type_mismatch_changes][path] = type_mismatches
290
- options[:verbose] ? puts("MT #{display_path}") : print('M')
291
- else
292
- state[:checked_ok] += 1
293
- options[:verbose] ? puts("OK #{display_path}") : print('.')
294
- end
368
+ handle_check_no_changes(path, type_mismatches: type_mismatches, display_path: ctx[:display_path],
369
+ options: ctx[:options], state: ctx[:state])
295
370
  return
296
371
  end
297
372
 
373
+ handle_check_failed(path, file_changes: file_changes, display_path: ctx[:display_path],
374
+ options: ctx[:options], state: ctx[:state])
375
+ end
376
+
377
+ # Extract type mismatch changes from file_changes.
378
+ #
379
+ # @private
380
+ # @param [Array<Hash>] file_changes
381
+ # @return [Array<Hash>]
382
+ def type_mismatch_changes(file_changes)
383
+ file_changes.select { |c| %i[updated_param updated_return].include?(c[:type]) }
384
+ end
385
+
386
+ # Handle check result when there are no real changes.
387
+ #
388
+ # @private
389
+ # @param [String] path
390
+ # @param [Array<Hash>] type_mismatches
391
+ # @param [String] display_path
392
+ # @param [Hash] options
393
+ # @param [Hash] state
394
+ # @return [void]
395
+ def handle_check_no_changes(path, type_mismatches:, display_path:, options:, state:)
396
+ if type_mismatches.any?
397
+ state[:type_mismatch_paths] << path
398
+ state[:type_mismatch_changes][path] = type_mismatches
399
+ log_check_verdict('MT', display_path, options)
400
+ else
401
+ state[:checked_ok] += 1
402
+ log_check_verdict('OK', display_path, options)
403
+ end
404
+ end
405
+
406
+ # Handle a failed check (file needs updates).
407
+ #
408
+ # @private
409
+ # @param [String] path
410
+ # @param [Array<Hash>] file_changes
411
+ # @param [String] display_path
412
+ # @param [Hash] options
413
+ # @param [Hash] state
414
+ # @return [void]
415
+ def handle_check_failed(path, file_changes:, display_path:, options:, state:)
298
416
  if options[:verbose]
299
417
  puts("FAIL #{display_path}")
300
- if options[:explain]
301
- file_changes.each do |change|
302
- puts(" - #{format_change_reason(change)}")
303
- end
304
- end
418
+ print_check_explanations(file_changes, options)
305
419
  else
306
420
  print('F')
307
421
  end
@@ -322,33 +436,87 @@ module Docscribe
322
436
  # @param [String] display_path path shown in CLI output
323
437
  # @param [Hash] options CLI options
324
438
  # @param [Hash] state shared processing state
439
+ # @param [Hash] ctx context hash with :display_path, :options, :state keys
325
440
  # @raise [StandardError]
326
441
  # @return [void]
327
- def handle_write_result(path, src:, out:, file_changes:, display_path:, options:, state:)
442
+ def handle_write_result(path, src:, out:, file_changes:, **ctx)
328
443
  if out == src
329
- options[:verbose] ? puts("OK #{display_path}") : print('.')
444
+ log_check_verdict('OK', ctx[:display_path], ctx[:options])
330
445
  return
331
446
  end
332
447
 
333
448
  File.write(path, out)
449
+ log_write_verdict('CHANGED', ctx[:display_path], file_changes, ctx[:options])
450
+ ctx[:state][:corrected] += 1
451
+ rescue StandardError => e
452
+ record_write_error(path, e, display_path: ctx[:display_path], options: ctx[:options], state: ctx[:state])
453
+ end
334
454
 
455
+ # Log a write-mode verdict.
456
+ #
457
+ # @private
458
+ # @param [String] verdict
459
+ # @param [String] display_path
460
+ # @param [Array<Hash>] file_changes
461
+ # @param [Hash] options
462
+ # @return [void]
463
+ def log_write_verdict(verdict, display_path, file_changes, options)
335
464
  if options[:verbose]
336
- puts("CHANGED #{display_path}")
337
- if options[:explain]
338
- file_changes.each do |change|
339
- puts(" - #{format_change_reason(change)}")
340
- end
341
- end
465
+ puts("#{verdict} #{display_path}")
466
+ print_check_explanations(file_changes, options)
342
467
  else
343
468
  print('C')
344
469
  end
470
+ end
345
471
 
346
- state[:corrected] += 1
347
- rescue StandardError => e
472
+ # Print explanations for file changes.
473
+ #
474
+ # @private
475
+ # @param [Array<Hash>] file_changes
476
+ # @param [Hash] options
477
+ # @return [void]
478
+ def print_check_explanations(file_changes, options)
479
+ return unless options[:explain]
480
+
481
+ file_changes.each do |change|
482
+ puts(" - #{format_change_reason(change)}")
483
+ end
484
+ end
485
+
486
+ # Record a write error in state.
487
+ #
488
+ # @private
489
+ # @param [String] path
490
+ # @param [StandardError] e
491
+ # @param [String] display_path
492
+ # @param [Hash] options
493
+ # @param [Hash] state
494
+ # @param [StandardError] error the exception raised during file write
495
+ # @return [void]
496
+ def record_write_error(path, error, display_path:, options:, state:)
348
497
  state[:had_errors] = true
349
498
  state[:error_paths] << path
350
- state[:error_messages][path] = "#{e.class}: #{e.message}"
351
- options[:verbose] ? warn("ERR #{display_path}: #{state[:error_messages][path]}") : print('E')
499
+ state[:error_messages][path] = "#{error.class}: #{error.message}"
500
+ log_check_verdict('ERR', display_path, options)
501
+ end
502
+
503
+ # Log a per-file check verdict.
504
+ #
505
+ # @private
506
+ # @param [String] verdict
507
+ # @param [String] display_path
508
+ # @param [Hash] options
509
+ # @return [void]
510
+ def log_check_verdict(verdict, display_path, options)
511
+ if options[:verbose]
512
+ puts("#{verdict} #{display_path}")
513
+ else
514
+ print(if verdict == 'FAIL'
515
+ 'F'
516
+ else
517
+ verdict == 'MT' ? 'M' : '.'
518
+ end)
519
+ end
352
520
  end
353
521
 
354
522
  # Print the check-mode summary (files OK / need updates / errors).
@@ -359,25 +527,75 @@ module Docscribe
359
527
  # @return [void]
360
528
  def print_check_summary(state:, options:)
361
529
  puts
530
+ print_check_status_line(state)
531
+ print_fail_paths(state, options)
532
+ print_type_mismatch_paths(state, options)
533
+ print_error_paths(state)
534
+ end
362
535
 
536
+ # Print the check-mode status line.
537
+ #
538
+ # @private
539
+ # @param [Hash] state
540
+ # @return [void]
541
+ def print_check_status_line(state)
363
542
  checked_error = state[:error_paths].size
364
543
  type_mismatch_count = state[:type_mismatch_paths].size
365
544
 
366
- if state[:checked_fail].zero? && checked_error.zero? && type_mismatch_count.zero?
545
+ if all_fine?(state, checked_error, type_mismatch_count)
367
546
  puts "Docscribe: OK (#{state[:checked_ok]} files checked)"
368
- return
369
- end
370
-
371
- if state[:checked_fail].zero? && checked_error.zero?
547
+ elsif mismatch_only?(state, checked_error)
372
548
  puts "Docscribe: OK (#{state[:checked_ok]} files checked, #{type_mismatch_count} with type mismatches)"
373
549
  else
374
- parts = ["#{state[:checked_fail]} need updates"]
375
- parts << "#{type_mismatch_count} type mismatches" if type_mismatch_count.positive?
376
- parts << "#{checked_error} errors"
377
- parts << "#{state[:checked_ok]} ok"
378
- puts "Docscribe: FAILED (#{parts.join(', ')})"
550
+ puts build_failure_line(state, type_mismatch_count, checked_error)
379
551
  end
552
+ end
553
+
554
+ # Whether no failures, errors, or type mismatches occurred.
555
+ #
556
+ # @private
557
+ # @param [Hash] state shared processing state
558
+ # @param [Integer] checked_error number of files with errors
559
+ # @param [Integer] type_mismatch_count number of files with type mismatches
560
+ # @return [Boolean]
561
+ def all_fine?(state, checked_error, type_mismatch_count)
562
+ state[:checked_fail].zero? && checked_error.zero? && type_mismatch_count.zero?
563
+ end
564
+
565
+ # Whether type mismatches exist but no failures or errors.
566
+ #
567
+ # @private
568
+ # @param [Hash] state shared processing state
569
+ # @param [Integer] checked_error number of files with errors
570
+ # @return [Boolean]
571
+ def mismatch_only?(state, checked_error)
572
+ state[:checked_fail].zero? && checked_error.zero?
573
+ end
574
+
575
+ # Build the human-readable failure summary line for check output.
576
+ #
577
+ # @private
578
+ # @param [Hash] state shared processing state
579
+ # @param [Integer] type_mismatch_count number of files with type mismatches
580
+ # @param [Integer] checked_error number of files with errors
581
+ # @return [String]
582
+ def build_failure_line(state, type_mismatch_count, checked_error)
583
+ parts = ["#{state[:checked_fail]} need updates"]
584
+ parts << "#{type_mismatch_count} type mismatches" if type_mismatch_count.positive?
585
+ parts << "#{checked_error} errors"
586
+ parts << "#{state[:checked_ok]} ok"
587
+ "Docscribe: FAILED (#{parts.join(', ')})"
588
+ end
589
+
590
+ public
380
591
 
592
+ # Print fail paths from check summary.
593
+ #
594
+ # @private
595
+ # @param [Hash] state
596
+ # @param [Hash] options
597
+ # @return [void]
598
+ def print_fail_paths(state, options)
381
599
  state[:fail_paths].each do |p|
382
600
  warn "Would update docs: #{p}"
383
601
  next unless options[:explain] && !options[:verbose]
@@ -386,19 +604,22 @@ module Docscribe
386
604
  warn " - #{format_change_reason(change)}"
387
605
  end
388
606
  end
607
+ end
389
608
 
390
- if options[:verbose] || options[:explain]
391
- state[:type_mismatch_paths].each do |p|
392
- warn "Type mismatches: #{p}"
393
- Array(state[:type_mismatch_changes][p]).each do |change|
394
- warn " - #{format_change_reason(change)}"
395
- end
396
- end
397
- end
609
+ # Print type mismatch paths from check summary.
610
+ #
611
+ # @private
612
+ # @param [Hash] state
613
+ # @param [Hash] options
614
+ # @return [void]
615
+ def print_type_mismatch_paths(state, options)
616
+ return unless options[:verbose] || options[:explain]
398
617
 
399
- state[:error_paths].each do |p|
400
- warn "Error processing: #{p}"
401
- warn " #{state[:error_messages][p]}" if state[:error_messages][p]
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)}"
622
+ end
402
623
  end
403
624
  end
404
625
 
@@ -408,18 +629,44 @@ module Docscribe
408
629
  # @param [Hash] change structured change produced by the inline rewriter
409
630
  # @return [String] human-readable explanation line
410
631
  def format_change_reason(change)
411
- line = change[:line] ? " at line #{change[:line]}" : ''
412
- method = change[:method] ? " for #{change[:method]}" : ''
413
-
414
- case change[:type]
415
- when :unsorted_tags
416
- "unsorted tags#{line}"
417
- when :missing_param, :missing_return, :missing_raise, :missing_visibility, :missing_module_function_note,
418
- :insert_full_doc_block
419
- "#{change[:message]}#{method}#{line}"
420
- else
421
- "#{change[:message] || change[:type].to_s.tr('_', ' ')}#{method}#{line}"
422
- end
632
+ line = change_line_suffix(change)
633
+ method = change_method_suffix(change)
634
+
635
+ return "unsorted tags#{line}" if change[:type] == :unsorted_tags
636
+ return "#{change[:message]}#{method}#{line}" if direct_message_change?(change)
637
+
638
+ "#{change[:message] || change[:type].to_s.tr('_', ' ')}#{method}#{line}"
639
+ end
640
+
641
+ # Format the line number suffix for a change reason string.
642
+ #
643
+ # @param [Hash] change structured change record
644
+ # @return [String] " at line N" or empty
645
+ def change_line_suffix(change)
646
+ change[:line] ? " at line #{change[:line]}" : ''
647
+ end
648
+
649
+ # Format the method name suffix for a change reason string.
650
+ #
651
+ # @param [Hash] change structured change record
652
+ # @return [String] " for method_name" or empty
653
+ def change_method_suffix(change)
654
+ change[:method] ? " for #{change[:method]}" : ''
655
+ end
656
+
657
+ # Whether a change type uses its own :message field directly as the reason.
658
+ #
659
+ # @param [Hash] change structured change record
660
+ # @return [Boolean]
661
+ def direct_message_change?(change)
662
+ %i[
663
+ missing_param
664
+ missing_return
665
+ missing_raise
666
+ missing_visibility
667
+ missing_module_function_note
668
+ insert_full_doc_block
669
+ ].include?(change[:type])
423
670
  end
424
671
 
425
672
  # Print the write-mode summary (files corrected, errors).
@@ -434,6 +681,15 @@ module Docscribe
434
681
  return unless state[:had_errors]
435
682
 
436
683
  warn "Docscribe: #{state[:error_paths].size} file(s) had errors"
684
+ print_error_paths(state)
685
+ end
686
+
687
+ # Print error paths from check summary.
688
+ #
689
+ # @private
690
+ # @param [Hash] state
691
+ # @return [void]
692
+ def print_error_paths(state)
437
693
  state[:error_paths].each do |p|
438
694
  warn "Error processing: #{p}"
439
695
  warn " #{state[:error_messages][p]}" if state[:error_messages][p]