docscribe 1.4.1 → 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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +588 -104
  3. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  4. data/lib/docscribe/cli/config_builder.rb +180 -36
  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 +296 -125
  10. data/lib/docscribe/cli/init.rb +58 -14
  11. data/lib/docscribe/cli/options.rb +410 -133
  12. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  13. data/lib/docscribe/cli/run.rb +503 -189
  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 +35 -9
  17. data/lib/docscribe/config/defaults.rb +16 -12
  18. data/lib/docscribe/config/emit.rb +18 -0
  19. data/lib/docscribe/config/filtering.rb +37 -31
  20. data/lib/docscribe/config/loader.rb +20 -13
  21. data/lib/docscribe/config/plugin.rb +2 -1
  22. data/lib/docscribe/config/rbs.rb +68 -27
  23. data/lib/docscribe/config/sorbet.rb +40 -17
  24. data/lib/docscribe/config/sorting.rb +2 -1
  25. data/lib/docscribe/config/template.rb +10 -1
  26. data/lib/docscribe/config/utils.rb +12 -9
  27. data/lib/docscribe/config.rb +3 -4
  28. data/lib/docscribe/infer/ast_walk.rb +1 -1
  29. data/lib/docscribe/infer/constants.rb +15 -0
  30. data/lib/docscribe/infer/literals.rb +39 -26
  31. data/lib/docscribe/infer/names.rb +24 -16
  32. data/lib/docscribe/infer/params.rb +57 -13
  33. data/lib/docscribe/infer/raises.rb +23 -15
  34. data/lib/docscribe/infer/returns.rb +784 -199
  35. data/lib/docscribe/infer.rb +28 -28
  36. data/lib/docscribe/inline_rewriter/collector.rb +816 -430
  37. data/lib/docscribe/inline_rewriter/doc_block.rb +323 -150
  38. data/lib/docscribe/inline_rewriter/doc_builder.rb +1837 -648
  39. data/lib/docscribe/inline_rewriter/source_helpers.rb +119 -71
  40. data/lib/docscribe/inline_rewriter/tag_sorter.rb +165 -107
  41. data/lib/docscribe/inline_rewriter.rb +1144 -727
  42. data/lib/docscribe/parsing.rb +29 -10
  43. data/lib/docscribe/plugin/base/collector_plugin.rb +3 -3
  44. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -2
  45. data/lib/docscribe/plugin/context.rb +28 -18
  46. data/lib/docscribe/plugin/registry.rb +49 -23
  47. data/lib/docscribe/plugin/tag.rb +9 -14
  48. data/lib/docscribe/plugin.rb +54 -22
  49. data/lib/docscribe/types/provider_chain.rb +4 -2
  50. data/lib/docscribe/types/rbs/collection_loader.rb +2 -3
  51. data/lib/docscribe/types/rbs/provider.rb +127 -62
  52. data/lib/docscribe/types/rbs/type_formatter.rb +286 -77
  53. data/lib/docscribe/types/signature.rb +22 -42
  54. data/lib/docscribe/types/sorbet/base_provider.rb +51 -27
  55. data/lib/docscribe/types/sorbet/rbi_provider.rb +3 -3
  56. data/lib/docscribe/types/sorbet/source_provider.rb +3 -2
  57. data/lib/docscribe/types/yard/formatter.rb +100 -0
  58. data/lib/docscribe/types/yard/parser.rb +240 -0
  59. data/lib/docscribe/types/yard/types.rb +52 -0
  60. data/lib/docscribe/version.rb +1 -1
  61. metadata +34 -2
@@ -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
@@ -17,6 +18,23 @@ module Docscribe
17
18
  # - process exit status
18
19
  module Run
19
20
  class << self
21
+ INITIAL_RUN_STATE = {
22
+ changed: false,
23
+ had_errors: false,
24
+ checked_ok: 0,
25
+ checked_fail: 0,
26
+ corrected: 0,
27
+ corrected_paths: [], #: Array[String]
28
+ corrected_changes: {}, #: Hash[String, untyped]
29
+ fail_paths: [], #: Array[String]
30
+ fail_changes: {}, #: Hash[String, untyped]
31
+ error_paths: [], #: Array[String]
32
+ error_messages: {}, #: Hash[String, String]
33
+ type_mismatch_paths: [], #: Array[String]
34
+ type_mismatch_changes: {}, #: Hash[String, untyped]
35
+ total: 0,
36
+ processed: 0
37
+ }.freeze
20
38
  # Run Docscribe for files or STDIN using the selected mode and strategy.
21
39
  #
22
40
  # Modes:
@@ -28,50 +46,87 @@ module Docscribe
28
46
  # - :safe => merge/add/normalize non-destructively
29
47
  # - :aggressive => rebuild existing doc blocks
30
48
  #
31
- # @param [Hash] options parsed CLI options
49
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
32
50
  # @param [Array<String>] argv remaining path arguments
33
51
  # @return [Integer] process exit code
34
52
  def run(options:, argv:)
35
- conf = Docscribe::Config.load(options[:config])
36
- conf = Docscribe::CLI::ConfigBuilder.build(conf, options)
37
- conf.load_plugins!
53
+ conf = build_config(options)
38
54
 
39
55
  return run_stdin(options: options, conf: conf) if options[:mode] == :stdin
40
56
 
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
57
+ paths = filtered_paths(argv, conf)
58
+ return no_files_found unless paths.any?
48
59
 
49
60
  run_files(options: options, conf: conf, paths: paths)
50
61
  end
51
62
 
63
+ # Load and build the effective config from CLI options.
64
+ #
65
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
66
+ # @return [Docscribe::Config] effective config with plugins loaded
67
+ def build_config(options)
68
+ conf = Docscribe::Config.load(options[:config])
69
+ conf = Docscribe::CLI::ConfigBuilder.build(conf, options)
70
+ conf.load_plugins!
71
+ conf
72
+ end
73
+
52
74
  # Rewrite code from STDIN using the selected strategy and print the
53
75
  # result.
54
76
  #
55
- # @param [Hash] options parsed CLI options
77
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
56
78
  # @param [Docscribe::Config] conf effective config
57
79
  # @raise [StandardError]
58
- # @return [Integer] process exit code
80
+ # @return [Integer] if StandardError
81
+ # @return [Integer] if StandardError
59
82
  def run_stdin(options:, conf:)
60
- code = $stdin.read
61
- result = Docscribe::InlineRewriter.rewrite_with_report(
62
- code,
63
- strategy: options[:strategy],
64
- config: conf,
65
- core_rbs_provider: conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil,
66
- file: '(stdin)'
67
- )
68
- puts result[:output]
83
+ puts stdin_rewrite_result(options, conf)[:output]
69
84
  0
70
85
  rescue StandardError => e
71
86
  warn "Docscribe: Error processing stdin: #{e.class}: #{e.message}"
72
87
  1
73
88
  end
74
89
 
90
+ # Rewrite STDIN input and return the result report.
91
+ #
92
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
93
+ # @param [Docscribe::Config] conf effective config
94
+ # @return [Hash<Symbol, Object>] rewrite result with :output key
95
+ def stdin_rewrite_result(options, conf)
96
+ Docscribe::InlineRewriter.rewrite_with_report(
97
+ $stdin.read,
98
+ strategy: options[:strategy],
99
+ config: conf,
100
+ core_rbs_provider: core_rbs_provider_for(conf),
101
+ file: '(stdin)'
102
+ )
103
+ end
104
+
105
+ # Return the core RBS provider from the config if available.
106
+ #
107
+ # @param [Docscribe::Config] conf effective config
108
+ # @return [Docscribe::Types::RBS::Provider, nil] core RBS provider or nil
109
+ def core_rbs_provider_for(conf)
110
+ conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
111
+ end
112
+
113
+ # Expand CLI path arguments and filter through config file patterns.
114
+ #
115
+ # @param [Array<String>] argv CLI path arguments
116
+ # @param [Docscribe::Config] conf effective config
117
+ # @return [Array<String>] filtered Ruby file paths
118
+ def filtered_paths(argv, conf)
119
+ expand_paths(argv).select { |path| conf.process_file?(path) }
120
+ end
121
+
122
+ # Warn and return exit code when no matching files were found.
123
+ #
124
+ # @return [Integer] exit code 2
125
+ def no_files_found
126
+ warn 'No files found. Pass files or directories (e.g. `docscribe lib`).'
127
+ 2
128
+ end
129
+
75
130
  # Expand CLI path arguments into a sorted list of Ruby files.
76
131
  #
77
132
  # Directories are expanded recursively to `**/*.rb`.
@@ -80,22 +135,31 @@ module Docscribe
80
135
  # @param [Array<String>] args file and/or directory arguments
81
136
  # @return [Array<String>] unique sorted Ruby file paths
82
137
  def expand_paths(args)
83
- files = []
138
+ files = [] #: Array[String]
84
139
  args = ['.'] if args.empty?
85
140
 
86
141
  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
142
+ append_expanded_path(files, path)
94
143
  end
95
144
 
96
145
  files.uniq.sort
97
146
  end
98
147
 
148
+ # Append a file or recursively expand a directory into the files array.
149
+ #
150
+ # @param [Array<String>] files mutable file path accumulator
151
+ # @param [String] path file or directory path to expand
152
+ # @return [void]
153
+ def append_expanded_path(files, path)
154
+ if File.directory?(path)
155
+ files.concat(Dir.glob(File.join(path, '**', '*.rb')))
156
+ elsif File.file?(path)
157
+ files << path
158
+ else
159
+ warn "Skipping missing path: #{path}"
160
+ end
161
+ end
162
+
99
163
  # Process file paths in inspect or write mode.
100
164
  #
101
165
  # In inspect mode:
@@ -106,7 +170,7 @@ module Docscribe
106
170
  # - rewrites changed files in place
107
171
  # - exits non-zero only if errors occurred
108
172
  #
109
- # @param [Hash] options parsed CLI options
173
+ # @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
110
174
  # @param [Docscribe::Config] conf effective config
111
175
  # @param [Array<String>] paths Ruby file paths to process
112
176
  # @return [Integer] process exit code
@@ -114,88 +178,109 @@ module Docscribe
114
178
  $stdout.sync = true
115
179
 
116
180
  state = initial_run_state
181
+ state[:total] = paths.size
117
182
  pwd = Pathname.pwd
118
183
 
119
184
  paths.each do |path|
120
185
  process_one_file(path, options: options, conf: conf, pwd: pwd, state: state)
121
186
  end
122
187
 
188
+ finalize_run(options, state)
189
+
190
+ run_exit_code(options, state)
191
+ end
192
+
193
+ private
194
+
195
+ # Print the check or write summary at the end of a run.
196
+ #
197
+ # @private
198
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
199
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
200
+ # @return [void]
201
+ def finalize_run(options, state)
202
+ formatter = Formatters.for(options[:format])
203
+
123
204
  if options[:mode] == :check
124
- print_check_summary(state: state, options: options)
205
+ formatter.format_check_summary(state: state, options: options)
125
206
  elsif options[:mode] == :write
126
- print_write_summary(state: state)
207
+ formatter.format_write_summary(state: state, options: options)
127
208
  end
209
+ end
128
210
 
129
- return 1 if state[:had_errors]
211
+ # Determine the process exit code based on run state and mode.
212
+ #
213
+ # @private
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
217
+ def run_exit_code(options, state)
218
+ return 2 if state[:had_errors]
130
219
  return 1 if options[:mode] == :check && state[:changed]
131
220
 
132
221
  0
133
222
  end
134
223
 
135
- private
136
-
137
224
  # Initialize the shared state hash used throughout a run.
138
225
  #
139
226
  # @private
140
- # @return [Hash] initial state with counters and tracking arrays
227
+ # @return [Docscribe::CLI::Formatters::state] initial state with counters and tracking arrays
141
228
  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
- }
229
+ Marshal.load(Marshal.dump(INITIAL_RUN_STATE))
155
230
  end
156
231
 
157
232
  # Process a single file: read, rewrite, and dispatch to check/write handler.
158
233
  #
159
234
  # @private
160
235
  # @param [String] path file path
161
- # @param [Hash] options CLI options
236
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
162
237
  # @param [Docscribe::Config] conf configuration
163
238
  # @param [Pathname] pwd current working directory
164
- # @param [Hash] state shared processing state
239
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
165
240
  # @return [void]
166
241
  def process_one_file(path, options:, conf:, pwd:, state:)
167
242
  display_path = display_path_for(path, pwd: pwd)
243
+ report_progress(state, options, display_path)
168
244
 
169
245
  src = read_source_for_path(path, display_path: display_path, options: options, state: state)
170
246
  return unless src
171
247
 
172
- result = rewrite_result_for_path(path, src: src, conf: conf, display_path: display_path, options: options,
173
- state: state)
248
+ ctx = { conf: conf, display_path: display_path, options: options, state: state }
249
+ result = rewrite_result_for_path(path, src: src, ctx: ctx)
174
250
  return unless result
175
251
 
176
- out = result[:output]
177
- file_changes = result[:changes] || []
252
+ dispatch_file_result(path, src: src, out: result[:output], file_changes: result[:changes] || [],
253
+ display_path: display_path, options: options, state: state)
254
+ end
178
255
 
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
- )
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
+
270
+ # Dispatch the rewrite result to the check or write handler based on mode.
271
+ #
272
+ # @private
273
+ # @param [String] path file path
274
+ # @param [String] src original source code
275
+ # @param [String] out rewritten source code
276
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
277
+ # @param [Object] ctx context hash with :options, :state, :display_path, :conf
278
+ # @return [void]
279
+ def dispatch_file_result(path, src:, out:, file_changes:, **ctx)
280
+ if ctx[:options][:mode] == :check
281
+ handle_check_result(path, src: src, out: out, file_changes: file_changes, **ctx)
282
+ elsif ctx[:options][:mode] == :write
283
+ handle_write_result(path, src: src, out: out, file_changes: file_changes, **ctx)
199
284
  end
200
285
  end
201
286
 
@@ -208,7 +293,8 @@ module Docscribe
208
293
  # @param [String] path file path to display
209
294
  # @param [Pathname] pwd current working directory
210
295
  # @raise [StandardError]
211
- # @return [String] path shown in CLI output
296
+ # @return [String] if StandardError
297
+ # @return [Object] if StandardError
212
298
  def display_path_for(path, pwd:)
213
299
  abs = Pathname.new(path).expand_path
214
300
 
@@ -226,17 +312,18 @@ module Docscribe
226
312
  # @private
227
313
  # @param [String] path file path to read
228
314
  # @param [String] display_path path shown in CLI output
229
- # @param [Hash] options CLI options
230
- # @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
231
317
  # @raise [StandardError]
232
- # @return [String, nil] file contents or nil on error
318
+ # @return [String, nil] if StandardError
319
+ # @return [nil] if StandardError
233
320
  def read_source_for_path(path, display_path:, options:, state:)
234
321
  File.read(path)
235
322
  rescue StandardError => e
236
323
  state[:had_errors] = true
237
324
  state[:error_paths] << path
238
325
  state[:error_messages][path] = "#{e.class}: #{e.message}"
239
- 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')
240
327
  nil
241
328
  end
242
329
 
@@ -245,27 +332,43 @@ module Docscribe
245
332
  # @private
246
333
  # @param [String] path file path
247
334
  # @param [String] src source code
248
- # @param [Docscribe::Config] conf configuration
249
- # @param [String] display_path path shown in CLI output
250
- # @param [Hash] options CLI options
251
- # @param [Hash] state shared processing state
335
+ # @param [Hash<Symbol, Object>] ctx context hash with :conf, :display_path, :options, :state keys
252
336
  # @raise [StandardError]
253
- # @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
337
+ # @return [Hash<Symbol, Object>, nil] if StandardError
338
+ # @return [nil] if StandardError
339
+ def rewrite_result_for_path(path, src:, ctx:)
340
+ conf = ctx[:conf]
341
+
342
+ core_rbs_provider =
343
+ conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
344
+
256
345
  Docscribe::InlineRewriter.rewrite_with_report(
257
- src,
258
- strategy: options[:strategy],
259
- config: conf,
260
- core_rbs_provider: core_rbs_provider,
261
- file: path
346
+ src, strategy: ctx[:options][:strategy], config: conf, core_rbs_provider: core_rbs_provider, file: path
262
347
  )
263
348
  rescue StandardError => e
349
+ record_rewrite_error(path, e, ctx)
350
+ nil
351
+ end
352
+
353
+ # Record a rewrite error in the shared state and print an error indicator.
354
+ #
355
+ # @private
356
+ # @param [String] path file path that caused the error
357
+ # @param [StandardError] error the exception raised during rewriting
358
+ # @param [Hash<Symbol, Object>] ctx context hash with :state, :options, :display_path
359
+ # @return [void]
360
+ def record_rewrite_error(path, error, ctx)
361
+ state = ctx[:state]
362
+
264
363
  state[:had_errors] = true
265
364
  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
365
+ state[:error_messages][path] = "#{error.class}: #{error.message}"
366
+
367
+ if ctx[:options][:verbose]
368
+ warn "ERR #{ctx[:display_path]}: #{state[:error_messages][path]}"
369
+ else
370
+ $stderr.print('E')
371
+ end
269
372
  end
270
373
 
271
374
  # Handle the result of an inspect (check) run.
@@ -274,36 +377,68 @@ module Docscribe
274
377
  # @param [String] path file path
275
378
  # @param [String] src original source code
276
379
  # @param [String] out rewritten source code
277
- # @param [Array<Hash>] file_changes structured change records
278
- # @param [String] display_path path shown in CLI output
279
- # @param [Hash] options CLI options
280
- # @param [Hash] state shared processing state
380
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
381
+ # @param [Object] ctx context hash with :display_path, :options, :state keys
281
382
  # @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]) }
383
+ def handle_check_result(path, src:, out:, file_changes:, **ctx)
384
+ type_mismatches = type_mismatch_changes(file_changes)
284
385
  has_real_changes = file_changes.any? { |c| !%i[updated_param updated_return].include?(c[:type]) }
285
386
 
286
387
  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
388
+ handle_check_no_changes(path, type_mismatches: type_mismatches, display_path: ctx[:display_path],
389
+ options: ctx[:options], state: ctx[:state])
295
390
  return
296
391
  end
297
392
 
393
+ handle_check_failed(path, file_changes: file_changes, display_path: ctx[:display_path],
394
+ options: ctx[:options], state: ctx[:state])
395
+ end
396
+
397
+ # Extract type mismatch changes from file_changes.
398
+ #
399
+ # @private
400
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
401
+ # @return [Array<Docscribe::CLI::Formatters::change>]
402
+ def type_mismatch_changes(file_changes)
403
+ file_changes.select { |c| %i[updated_param updated_return].include?(c[:type]) }
404
+ end
405
+
406
+ # Handle check result when there are no real changes.
407
+ #
408
+ # @private
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
414
+ # @return [void]
415
+ def handle_check_no_changes(path, type_mismatches:, display_path:, options:, state:)
416
+ if type_mismatches.any?
417
+ state[:type_mismatch_paths] << path
418
+ state[:type_mismatch_changes][path] = type_mismatches
419
+ log_check_verdict('MT', display_path, options)
420
+ else
421
+ state[:checked_ok] += 1
422
+ log_check_verdict('OK', display_path, options)
423
+ end
424
+ end
425
+
426
+ # Handle a failed check (file needs updates).
427
+ # With --verbose, prints the per-file verdict and all change reasons.
428
+ #
429
+ # @private
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
435
+ # @return [void]
436
+ def handle_check_failed(path, file_changes:, display_path:, options:, state:)
298
437
  if options[:verbose]
299
- puts("FAIL #{display_path}")
300
- if options[:explain]
301
- file_changes.each do |change|
302
- puts(" - #{format_change_reason(change)}")
303
- end
304
- end
438
+ warn("FAIL #{display_path}")
439
+ print_check_explanations(file_changes)
305
440
  else
306
- print('F')
441
+ $stderr.print('F')
307
442
  end
308
443
 
309
444
  state[:checked_fail] += 1
@@ -318,122 +453,301 @@ module Docscribe
318
453
  # @param [String] path file path
319
454
  # @param [String] src original source code
320
455
  # @param [String] out rewritten source code
321
- # @param [Array<Hash>] file_changes structured change records
322
- # @param [String] display_path path shown in CLI output
323
- # @param [Hash] options CLI options
324
- # @param [Hash] state shared processing state
456
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
457
+ # @param [Object] ctx context hash with :display_path, :options, :state keys
325
458
  # @raise [StandardError]
326
- # @return [void]
327
- def handle_write_result(path, src:, out:, file_changes:, display_path:, options:, state:)
328
- if out == src
329
- options[:verbose] ? puts("OK #{display_path}") : print('.')
330
- return
331
- end
459
+ # @return [void] if StandardError
460
+ # @return [Object] if StandardError
461
+ def handle_write_result(path, src:, out:, file_changes:, **ctx)
462
+ return log_check_verdict('OK', ctx[:display_path], ctx[:options]) if out == src
463
+
464
+ apply_correction(path, out, file_changes, ctx)
465
+ rescue StandardError => e
466
+ record_write_error(path, e, display_path: ctx[:display_path], options: ctx[:options], state: ctx[:state])
467
+ end
332
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)
333
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
334
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
+
496
+ # Log a write-mode verdict.
497
+ #
498
+ # @private
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
503
+ # @return [void]
504
+ def log_write_verdict(verdict, display_path, file_changes, options)
335
505
  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
506
+ warn("#{verdict} #{display_path}")
507
+ print_check_explanations(file_changes)
342
508
  else
343
- print('C')
509
+ $stderr.print('C')
344
510
  end
511
+ end
345
512
 
346
- state[:corrected] += 1
347
- rescue StandardError => e
513
+ # Print explanations for file changes.
514
+ #
515
+ # Callers are responsible for gating on --verbose / --explain.
516
+ #
517
+ # @private
518
+ # @param [Array<Docscribe::CLI::Formatters::change>] file_changes structured change records
519
+ # @return [void]
520
+ def print_check_explanations(file_changes)
521
+ file_changes.each do |change|
522
+ warn(" - #{format_change_reason(change)}")
523
+ end
524
+ end
525
+
526
+ # Record a write error in state.
527
+ #
528
+ # @private
529
+ # @param [String] path file path
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
534
+ # @return [void]
535
+ def record_write_error(path, error, display_path:, options:, state:)
348
536
  state[:had_errors] = true
349
537
  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')
538
+ state[:error_messages][path] = "#{error.class}: #{error.message}"
539
+ log_check_verdict('ERR', display_path, options)
352
540
  end
353
541
 
354
- # Print the check-mode summary (files OK / need updates / errors).
542
+ # Log a per-file check verdict.
355
543
  #
356
544
  # @private
357
- # @param [Hash] state shared processing state
358
- # @param [Hash] options CLI 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
548
+ # @return [void]
549
+ def log_check_verdict(verdict, display_path, options)
550
+ if options[:verbose]
551
+ warn("#{verdict} #{display_path}")
552
+ else
553
+ $stderr.print(if verdict == 'FAIL'
554
+ 'F'
555
+ else
556
+ verdict == 'MT' ? 'M' : '.'
557
+ end)
558
+ end
559
+ end
560
+
561
+ # Print the check-mode summary (fail paths, then status line).
562
+ #
563
+ # @private
564
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
565
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
359
566
  # @return [void]
360
567
  def print_check_summary(state:, options:)
361
568
  puts
569
+ print_fail_paths(state, options)
570
+ print_check_status_line(state)
571
+ print_type_mismatch_paths(state, options)
572
+ print_error_paths(state)
573
+ end
362
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
+
596
+ # Print the check-mode status line.
597
+ #
598
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
599
+ # @return [void]
600
+ def print_check_status_line(state)
363
601
  checked_error = state[:error_paths].size
364
602
  type_mismatch_count = state[:type_mismatch_paths].size
365
603
 
366
- if state[:checked_fail].zero? && checked_error.zero? && type_mismatch_count.zero?
604
+ if all_fine?(state, checked_error, type_mismatch_count)
367
605
  puts "Docscribe: OK (#{state[:checked_ok]} files checked)"
368
- return
369
- end
370
-
371
- if state[:checked_fail].zero? && checked_error.zero?
606
+ elsif mismatch_only?(state, checked_error)
372
607
  puts "Docscribe: OK (#{state[:checked_ok]} files checked, #{type_mismatch_count} with type mismatches)"
373
608
  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(', ')})"
609
+ puts build_failure_line(state, type_mismatch_count, checked_error)
379
610
  end
611
+ end
380
612
 
381
- state[:fail_paths].each do |p|
382
- warn "Would update docs: #{p}"
383
- next unless options[:explain] && !options[:verbose]
613
+ # Whether no failures, errors, or type mismatches occurred.
614
+ #
615
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
616
+ # @param [Integer] checked_error number of files with errors
617
+ # @param [Integer] type_mismatch_count number of files with type mismatches
618
+ # @return [Boolean]
619
+ def all_fine?(state, checked_error, type_mismatch_count)
620
+ state[:checked_fail].zero? && checked_error.zero? && type_mismatch_count.zero?
621
+ end
384
622
 
385
- Array(state[:fail_changes][p]).each do |change|
623
+ # Whether type mismatches exist but no failures or errors.
624
+ #
625
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
626
+ # @param [Integer] checked_error number of files with errors
627
+ # @return [Boolean]
628
+ def mismatch_only?(state, checked_error)
629
+ state[:checked_fail].zero? && checked_error.zero?
630
+ end
631
+
632
+ # Build the human-readable failure summary line for check output.
633
+ #
634
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
635
+ # @param [Integer] type_mismatch_count number of files with type mismatches
636
+ # @param [Integer] checked_error number of files with errors
637
+ # @return [String]
638
+ def build_failure_line(state, type_mismatch_count, checked_error)
639
+ parts = ["#{state[:checked_fail]} need updates"]
640
+ parts << "#{type_mismatch_count} type mismatches" if type_mismatch_count.positive?
641
+ parts << "#{checked_error} errors"
642
+ parts << "#{state[:checked_ok]} ok"
643
+ "Docscribe: FAILED (#{parts.join(', ')})"
644
+ end
645
+
646
+ # Print type mismatch paths from check summary.
647
+ #
648
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
649
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
650
+ # @return [void]
651
+ def print_type_mismatch_paths(state, options)
652
+ return if options[:quiet]
653
+ return unless options[:verbose] || options[:explain]
654
+
655
+ state[:type_mismatch_paths].each do |p|
656
+ warn "Type mismatches: #{p}"
657
+ Array(state[:type_mismatch_changes][p]).each do |change|
386
658
  warn " - #{format_change_reason(change)}"
387
659
  end
388
660
  end
661
+ end
389
662
 
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
663
+ # Print the write-mode summary (files corrected, errors).
664
+ #
665
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
666
+ # @param [Docscribe::CLI::Formatters::opts] options CLI options
667
+ # @return [void]
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)
398
672
 
399
- state[:error_paths].each do |p|
400
- warn "Error processing: #{p}"
401
- warn " #{state[:error_messages][p]}" if state[:error_messages][p]
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)}"
694
+ end
402
695
  end
403
696
  end
404
697
 
405
698
  # Format a structured change record into human-readable CLI output.
406
699
  #
407
- # @private
408
- # @param [Hash] change structured change produced by the inline rewriter
700
+ # @param [Docscribe::CLI::Formatters::change] change structured change produced by the inline rewriter
409
701
  # @return [String] human-readable explanation line
410
702
  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
703
+ line = change_line_suffix(change)
704
+ method = change_method_suffix(change)
705
+
706
+ return "unsorted tags#{line}" if change[:type] == :unsorted_tags
707
+ return "#{change[:message]}#{method}#{line}" if direct_message_change?(change)
708
+
709
+ "#{change[:message] || change[:type].to_s.tr('_', ' ')}#{method}#{line}"
423
710
  end
424
711
 
425
- # Print the write-mode summary (files corrected, errors).
712
+ # Format the line number suffix for a change reason string.
426
713
  #
427
- # @private
428
- # @param [Hash] state shared processing state
429
- # @return [void]
430
- def print_write_summary(state:)
431
- puts
432
- puts "Docscribe: updated #{state[:corrected]} file(s)" if state[:corrected].positive?
714
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
715
+ # @return [String] " at line N" or empty
716
+ def change_line_suffix(change)
717
+ change[:line] ? " at line #{change[:line]}" : ''
718
+ end
433
719
 
434
- return unless state[:had_errors]
720
+ # Format the method name suffix for a change reason string.
721
+ #
722
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
723
+ # @return [String] " for method_name" or empty
724
+ def change_method_suffix(change)
725
+ change[:method] ? " for #{change[:method]}" : ''
726
+ end
435
727
 
436
- warn "Docscribe: #{state[:error_paths].size} file(s) had errors"
728
+ # Whether a change type uses its own :message field directly as the reason.
729
+ #
730
+ # @param [Docscribe::CLI::Formatters::change] change structured change record
731
+ # @return [Boolean]
732
+ def direct_message_change?(change)
733
+ %i[
734
+ missing_param
735
+ missing_return
736
+ missing_raise
737
+ missing_visibility
738
+ missing_module_function_note
739
+ insert_full_doc_block
740
+ ].include?(change[:type])
741
+ end
742
+
743
+ # Print error paths from check summary.
744
+ #
745
+ # @param [Docscribe::CLI::Formatters::state] state shared processing state
746
+ # @return [void]
747
+ def print_error_paths(state)
748
+ return if state[:error_paths].empty?
749
+
750
+ warn ''
437
751
  state[:error_paths].each do |p|
438
752
  warn "Error processing: #{p}"
439
753
  warn " #{state[:error_messages][p]}" if state[:error_messages][p]