docscribe 1.6.0 → 1.6.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -193
  3. data/exe/docscribe-client +26 -7
  4. data/lib/docscribe/cli/config_builder.rb +37 -2
  5. data/lib/docscribe/cli/coverage.rb +5 -5
  6. data/lib/docscribe/cli/formatters/json.rb +74 -29
  7. data/lib/docscribe/cli/formatters/sarif.rb +20 -3
  8. data/lib/docscribe/cli/options.rb +17 -2
  9. data/lib/docscribe/cli/rbs_gen.rb +4 -4
  10. data/lib/docscribe/cli/run.rb +107 -24
  11. data/lib/docscribe/cli/update_types.rb +61 -17
  12. data/lib/docscribe/cli.rb +19 -13
  13. data/lib/docscribe/config/defaults.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +22 -1
  15. data/lib/docscribe/config/template.rb +3 -0
  16. data/lib/docscribe/config/validation.rb +19 -0
  17. data/lib/docscribe/config.rb +1 -0
  18. data/lib/docscribe/infer/behavior.rb +13 -13
  19. data/lib/docscribe/infer/params.rb +2 -2
  20. data/lib/docscribe/infer/raises.rb +5 -6
  21. data/lib/docscribe/infer/returns.rb +1612 -151
  22. data/lib/docscribe/infer.rb +7 -7
  23. data/lib/docscribe/inline_rewriter/doc_builder.rb +485 -102
  24. data/lib/docscribe/inline_rewriter.rb +263 -97
  25. data/lib/docscribe/plugin/registry.rb +1 -0
  26. data/lib/docscribe/server/base.rb +255 -0
  27. data/lib/docscribe/server/client.rb +95 -0
  28. data/lib/docscribe/server/daemon.rb +678 -0
  29. data/lib/docscribe/server/protocol.rb +50 -0
  30. data/lib/docscribe/server.rb +4 -835
  31. data/lib/docscribe/types/primitive.rb +160 -0
  32. data/lib/docscribe/types/sorbet/base_provider.rb +33 -1
  33. data/lib/docscribe/types/yard/formatter.rb +35 -6
  34. data/lib/docscribe/types/yard/parser.rb +25 -20
  35. data/lib/docscribe/types/yard/validator.rb +131 -0
  36. data/lib/docscribe/validator/generic_compatibility.rb +698 -0
  37. data/lib/docscribe/validator/type_mismatch_validator.rb +287 -0
  38. data/lib/docscribe/version.rb +1 -1
  39. metadata +12 -3
@@ -40,7 +40,7 @@ module Docscribe
40
40
  # @param [Symbol?] strategy :safe or :aggressive
41
41
  # @param [Boolean?] rewrite compatibility alias for aggressive strategy
42
42
  # @param [Boolean?] merge compatibility alias for safe strategy
43
- # @param [Object] options additional keyword arguments forwarded to rewrite_with_report
43
+ # @param [Hash] options additional keyword arguments forwarded to rewrite_with_report
44
44
  # @return [String]
45
45
  def insert_comments(code, strategy: nil, rewrite: nil, merge: nil, **options)
46
46
  strategy = normalize_strategy(strategy: strategy, rewrite: rewrite, merge: merge)
@@ -54,8 +54,8 @@ module Docscribe
54
54
  # @param [Symbol?] strategy :safe or :aggressive
55
55
  # @param [Boolean?] rewrite compatibility alias for aggressive strategy
56
56
  # @param [Boolean?] merge compatibility alias for safe strategy
57
- # @param [Object] options additional keyword arguments forwarded to downstream helpers
58
- # @return [Hash<Symbol, String, Array<Hash<Symbol, Object>>>]
57
+ # @param [Hash] options additional keyword arguments forwarded to downstream helpers
58
+ # @return [Hash<Symbol, String, Array<Docscribe::InlineRewriter::changeRecord>>]
59
59
  def rewrite_with_report(code, strategy: nil, rewrite: nil, merge: nil, **options)
60
60
  strategy = normalize_strategy(strategy: strategy, rewrite: rewrite, merge: merge)
61
61
  validate_strategy!(strategy)
@@ -72,7 +72,7 @@ module Docscribe
72
72
  #
73
73
  # @param [Parser::Source::Buffer] buffer the source buffer being rewritten
74
74
  # @param [Parser::AST::Node] ast the parsed AST of the source code
75
- # @return [Hash<Symbol, Object>]
75
+ # @return [Docscribe::InlineRewriter::pipeline]
76
76
  def build_rewrite_pipeline(buffer, ast)
77
77
  all = collect_insertions(buffer, ast)
78
78
  method_overrides_by_pos = {} #: Hash[Integer, untyped]
@@ -87,26 +87,90 @@ module Docscribe
87
87
 
88
88
  # Dispatch rewrite insertions
89
89
  #
90
- # @param [Hash<Symbol, Object>] pipeline the pipeline hash with rewriter, insertions, and tracking state
90
+ # @param [Docscribe::InlineRewriter::pipeline] pipeline the pipeline hash with rewriter, insertions, and tracking state
91
91
  # @param [Parser::Source::Buffer] buffer the source buffer being rewritten
92
- # @param [Object] options additional kwargs (config, signature_provider, core_rbs_provider, strategy, file)
92
+ # @param [Hash] options additional kwargs (config, signature_provider, core_rbs_provider, strategy, file)
93
93
  # @return [void]
94
94
  def dispatch_rewrite_insertions(pipeline, buffer, **options)
95
95
  pipeline[:all].sort_by { |(kind, ins)| plugin_insertion_pos(kind, ins) }
96
96
  .reverse_each do |kind, ins|
97
- method_name = :"dispatch_#{kind}_insertion"
98
- send(method_name, ins, pipeline, buffer, **options) if respond_to?(method_name, true)
97
+ dispatch_single_insertion(kind, ins, pipeline, buffer, **options)
99
98
  end
100
99
 
101
100
  apply_merge_inserts!(rewriter: pipeline[:rewriter], buffer: buffer, merge_inserts: pipeline[:merge_inserts])
102
101
  end
103
102
 
103
+ # Dispatch a single insertion, isolating per-method failures.
104
+ #
105
+ # A crash while documenting one method (e.g., unexpected AST shape)
106
+ # must not discard warnings for the rest of the file: the error is
107
+ # reported to stderr and the remaining insertions still apply.
108
+ #
109
+ # @param [Symbol] kind insertion kind (:method, :attr, :plugin)
110
+ # @param [Object] ins the insertion object
111
+ # @param [Docscribe::InlineRewriter::pipeline] pipeline the pipeline hash with rewriter, insertions, and tracking state
112
+ # @param [Parser::Source::Buffer] buffer the source buffer being rewritten
113
+ # @param [Hash] options additional kwargs (config, signature_provider, core_rbs_provider, strategy, file)
114
+ # @raise [StandardError]
115
+ # @return [void]
116
+ # @return [Object] if StandardError
117
+ def dispatch_single_insertion(kind, ins, pipeline, buffer, **options)
118
+ method_name = :"dispatch_#{kind}_insertion"
119
+ return unless respond_to?(method_name, true)
120
+
121
+ send(method_name, ins, pipeline, buffer, **options)
122
+ rescue StandardError => e
123
+ warn_insertion_error(kind, ins, options[:file], e)
124
+ end
125
+
126
+ # Report a skipped insertion to stderr without interrupting the rewrite.
127
+ #
128
+ # @param [Symbol] kind insertion kind (:method, :attr, :plugin)
129
+ # @param [Object] ins the insertion object
130
+ # @param [String, nil] file the file being rewritten
131
+ # @param [StandardError] error the rescued error
132
+ # @return [void]
133
+ def warn_insertion_error(kind, ins, file, error)
134
+ warn "Docscribe: skipping #{insertion_label(kind, ins)} in #{file}: #{error.class}: #{error.message}"
135
+ end
136
+
137
+ # Human-readable label for an insertion used in skip warnings.
138
+ #
139
+ # @param [Symbol] kind insertion kind (:method, :attr, :plugin)
140
+ # @param [Object] ins the insertion object
141
+ # @return [String] label like "method foo at line 12"
142
+ def insertion_label(kind, ins)
143
+ node = insertion_node(ins)
144
+ name = node ? SourceHelpers.node_name(node) : nil
145
+ line = node_line(node)
146
+ label = name ? "#{kind} #{name}" : kind.to_s
147
+ line ? "#{label} at line #{line}" : label
148
+ end
149
+
150
+ # Extract the AST node from an insertion when available.
151
+ #
152
+ # @param [Object] ins the insertion object
153
+ # @return [Parser::AST::Node, nil] the node or nil
154
+ def insertion_node(ins)
155
+ ins.respond_to?(:node) ? ins.node : nil
156
+ end
157
+
158
+ # Source line of a node expression when available.
159
+ #
160
+ # @param [Parser::AST::Node, nil] node an AST node
161
+ # @return [Integer, nil] 1-based line number or nil
162
+ def node_line(node)
163
+ loc = node&.loc
164
+ expr = loc&.expression
165
+ expr&.line
166
+ end
167
+
104
168
  # Dispatch method insertion
105
169
  #
106
170
  # @param [Docscribe::InlineRewriter::Collector::Insertion] ins the attribute insertion object
107
- # @param [Hash<Symbol, Object>] pipeline the pipeline hash with rewriter, insertions, and tracking state
171
+ # @param [Docscribe::InlineRewriter::pipeline] pipeline the pipeline hash with rewriter, insertions, and tracking state
108
172
  # @param [Parser::Source::Buffer] buffer the source buffer
109
- # @param [Object] options the full keyword options hash
173
+ # @param [Hash] options the full keyword options hash
110
174
  # @return [void]
111
175
  def dispatch_method_insertion(ins, pipeline, buffer, **options)
112
176
  pos = plugin_insertion_pos(:method, ins)
@@ -124,9 +188,9 @@ module Docscribe
124
188
  # Dispatch attr insertion
125
189
  #
126
190
  # @param [Docscribe::InlineRewriter::Collector::AttrInsertion] ins the attribute insertion object
127
- # @param [Hash<Symbol, Object>] pipeline the pipeline hash with rewriter, insertions, and tracking state
191
+ # @param [Docscribe::InlineRewriter::pipeline] pipeline the pipeline hash with rewriter, insertions, and tracking state
128
192
  # @param [Parser::Source::Buffer] buffer the source buffer
129
- # @param [Object] options the full keyword options hash
193
+ # @param [Hash] options the full keyword options hash
130
194
  # @return [void]
131
195
  def dispatch_attr_insertion(ins, pipeline, buffer, **options)
132
196
  apply_attr_insertion!(
@@ -138,10 +202,10 @@ module Docscribe
138
202
 
139
203
  # Dispatch plugin insertion
140
204
  #
141
- # @param [Hash<Symbol, Object>] ins the attribute insertion object
142
- # @param [Hash<Symbol, Object>] pipeline the pipeline hash with rewriter, insertions, and tracking state
205
+ # @param [Docscribe::InlineRewriter::pluginInsertion] ins the attribute insertion object
206
+ # @param [Docscribe::InlineRewriter::pipeline] pipeline the pipeline hash with rewriter, insertions, and tracking state
143
207
  # @param [Parser::Source::Buffer] buffer the source buffer
144
- # @param [Object] options the full keyword options hash
208
+ # @param [Hash] options the full keyword options hash
145
209
  # @return [void]
146
210
  def dispatch_plugin_insertion(ins, pipeline, buffer, **options)
147
211
  apply_plugin_insertion!(
@@ -158,7 +222,7 @@ module Docscribe
158
222
  # @param [String] code the Ruby source code string to parse and rewrite
159
223
  # @param [Hash<Symbol, Object>] options hash containing :config, :file, and :core_rbs_provider
160
224
  # @raise [Docscribe::ParseError]
161
- # @return [Hash<Symbol, Docscribe::Config, String, Parser::Source::Buffer, Parser::AST::Node, Docscribe::Types::ProviderChain, nil, Object, nil>] rewrite environment with keys
225
+ # @return [Hash<Symbol, Docscribe::Config, String, Parser::Source::Buffer, Parser::AST::Node, Docscribe::Types::ProviderChain, nil, Docscribe::Types::RBS::Provider, nil>] rewrite environment with keys
162
226
  # :config, :file, :buffer, :ast, :core_rbs_provider
163
227
  def setup_rewrite_env(code, options)
164
228
  config = options[:config] || Docscribe::Config.load
@@ -177,9 +241,9 @@ module Docscribe
177
241
  #
178
242
  # @private
179
243
  # @param [Docscribe::Config] config the active Docscribe::Config
180
- # @param [Object, nil] core_rbs_provider optional externally-provided core RBS provider
244
+ # @param [Docscribe::Types::RBS::Provider, nil] core_rbs_provider optional externally-provided core RBS provider
181
245
  # @raise [StandardError]
182
- # @return [Object, nil]
246
+ # @return [Docscribe::Types::RBS::Provider, nil]
183
247
  # @return [nil] if StandardError
184
248
  def load_core_rbs_provider(config, core_rbs_provider)
185
249
  core_rbs_provider || (config.respond_to?(:core_rbs_provider) ? config.core_rbs_provider : nil)
@@ -193,7 +257,7 @@ module Docscribe
193
257
  # @private
194
258
  # @param [Parser::Source::Buffer] buffer the source buffer to collect insertions from
195
259
  # @param [Parser::AST::Node] ast the parsed AST to traverse for collection
196
- # @return [Array<Object>]
260
+ # @return [Array<Docscribe::InlineRewriter::insertionPair>]
197
261
  def collect_insertions(buffer, ast)
198
262
  collector = Docscribe::InlineRewriter::Collector.new(buffer)
199
263
  collector.process(ast)
@@ -208,10 +272,10 @@ module Docscribe
208
272
  # Deduplicate insertions
209
273
  #
210
274
  # @private
211
- # @param [Array<(Symbol, Object)>] insertions insertions to deduplicate
212
- # @param [Hash<Integer, Hash<Symbol, Object>>, nil?] method_overrides_by_pos method-level overrides keyed
275
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] insertions insertions to deduplicate
276
+ # @param [Hash<Integer, Docscribe::InlineRewriter::methodOverride>?] method_overrides_by_pos method-level overrides keyed
213
277
  # by insertion position
214
- # @return [Array<(Symbol, Object)>]
278
+ # @return [Array<Docscribe::InlineRewriter::insertionPair>]
215
279
  def deduplicate_insertions(insertions, method_overrides_by_pos: nil)
216
280
  group_by_position(insertions).each_with_object([]) do |(pos, items), result|
217
281
  process_dedup_group(pos, items, result, method_overrides_by_pos)
@@ -222,9 +286,9 @@ module Docscribe
222
286
  #
223
287
  # @private
224
288
  # @param [Integer] pos the source begin_pos for the group
225
- # @param [Array<(Symbol, Object)>] items grouped items to process
226
- # @param [Array<(Symbol, Object)>] result accumulated result array
227
- # @param [Hash<Integer, Hash<Symbol, Object>>, nil] method_overrides_by_pos hash mapping position to method
289
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] items grouped items to process
290
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] result accumulated result array
291
+ # @param [Hash<Integer, Docscribe::InlineRewriter::methodOverride>, nil] method_overrides_by_pos hash mapping position to method
228
292
  # override data
229
293
  # @return [void]
230
294
  def process_dedup_group(pos, items, result, method_overrides_by_pos)
@@ -243,8 +307,8 @@ module Docscribe
243
307
  # Group by position
244
308
  #
245
309
  # @private
246
- # @param [Array<(Symbol, Object)>] insertions insertions to group
247
- # @return [Hash<Integer, Array<(Symbol, Object)>>]
310
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] insertions insertions to group
311
+ # @return [Hash<Integer, Array<Docscribe::InlineRewriter::insertionPair>>]
248
312
  def group_by_position(insertions)
249
313
  groups = {} #: Hash[Integer, untyped]
250
314
  insertions.each do |kind, ins|
@@ -257,8 +321,8 @@ module Docscribe
257
321
  # Find override items
258
322
  #
259
323
  # @private
260
- # @param [Array<(Symbol, Object)>] plugin_items plugin items to check
261
- # @return [Array<(Symbol, Object)>]
324
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] plugin_items plugin items to check
325
+ # @return [Array<Docscribe::InlineRewriter::insertionPair>]
262
326
  def find_override_items(plugin_items)
263
327
  plugin_items.select do |_k, ins|
264
328
  ins.is_a?(Hash) && ins[:method_override].is_a?(Hash)
@@ -268,10 +332,10 @@ module Docscribe
268
332
  # Handle override case
269
333
  #
270
334
  # @private
271
- # @param [Array<(Symbol, Object)>] result accumulated result array
272
- # @param [Array<(Symbol, Object)>] items all items in group
273
- # @param [Array<(Symbol, Object)>] override_items override plugin items
274
- # @param [Hash<Integer, Hash<Symbol, Object>>, nil] method_overrides_by_pos hash mapping position to
335
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] result accumulated result array
336
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] items all items in group
337
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] override_items override plugin items
338
+ # @param [Hash<Integer, Docscribe::InlineRewriter::methodOverride>, nil] method_overrides_by_pos hash mapping position to
275
339
  # method override data
276
340
  # @param [Integer] pos the source position of the conflict
277
341
  # @return [void]
@@ -288,11 +352,11 @@ module Docscribe
288
352
  # Deduplicate items
289
353
  #
290
354
  # @private
291
- # @param [Array<(Symbol, Object)>] items all items in group
292
- # @param [Array<(Symbol, Object)>] plugin_items plugin items in group
355
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] items all items in group
356
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] plugin_items plugin items in group
293
357
  # @param [Integer] pos the source position of the conflict
294
- # @param [Array<(Symbol, Object)>] _method_items method items in group
295
- # @return [Array<(Symbol, Object)>]
358
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] _method_items method items in group
359
+ # @return [Array<Docscribe::InlineRewriter::insertionPair>]
296
360
  def deduplicate_items(items, plugin_items, pos, _method_items)
297
361
  plugin_doc_items = plugin_items.select { |pair| plugin_doc_item?(pair) }
298
362
 
@@ -306,7 +370,7 @@ module Docscribe
306
370
  # Plugin doc item
307
371
  #
308
372
  # @private
309
- # @param [(Symbol, Object)] pair insertion pair to check
373
+ # @param [Docscribe::InlineRewriter::insertionPair] pair insertion pair to check
310
374
  # @return [Boolean]
311
375
  def plugin_doc_item?(pair)
312
376
  _k, ins = pair
@@ -316,10 +380,10 @@ module Docscribe
316
380
  # Deduplicate plugin doc case
317
381
  #
318
382
  # @private
319
- # @param [Array<(Symbol, Object)>] items all items in group
320
- # @param [Array<(Symbol, Object)>] plugin_doc_items plugin doc items
383
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] items all items in group
384
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] plugin_doc_items plugin doc items
321
385
  # @param [Integer] pos the source position of the conflict
322
- # @return [Array<(Symbol, Object)>]
386
+ # @return [Array<Docscribe::InlineRewriter::insertionPair>]
323
387
  def deduplicate_plugin_doc_case(items, plugin_doc_items, pos)
324
388
  items = items.reject { |k, _| k == :method }
325
389
  items = items.reject { |pair| override_or_plugin_method?(pair) }
@@ -336,7 +400,7 @@ module Docscribe
336
400
  # Override or plugin method
337
401
  #
338
402
  # @private
339
- # @param [(Symbol, Object)] pair insertion pair to check
403
+ # @param [Docscribe::InlineRewriter::insertionPair] pair insertion pair to check
340
404
  # @return [Boolean]
341
405
  def override_or_plugin_method?(pair)
342
406
  k, ins = pair
@@ -346,7 +410,7 @@ module Docscribe
346
410
  # Max plugin priority
347
411
  #
348
412
  # @private
349
- # @param [Array<(Symbol, Object)>] plugin_items plugin items to scan
413
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] plugin_items plugin items to scan
350
414
  # @return [Integer]
351
415
  def max_plugin_priority(plugin_items)
352
416
  plugin_items.map { |_k, ins| plugin_insertion_priority(ins) }.max || 0
@@ -355,9 +419,9 @@ module Docscribe
355
419
  # Filter lower priority plugins
356
420
  #
357
421
  # @private
358
- # @param [Array<(Symbol, Object)>] items items to filter
422
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] items items to filter
359
423
  # @param [Integer] threshold minimum priority threshold
360
- # @return [Array<(Symbol, Object)>]
424
+ # @return [Array<Docscribe::InlineRewriter::insertionPair>]
361
425
  def filter_lower_priority_plugins(items, threshold)
362
426
  items.select do |k, ins|
363
427
  k == :plugin && ins.is_a?(Hash) && ins[:doc] && plugin_insertion_priority(ins) < threshold
@@ -367,8 +431,8 @@ module Docscribe
367
431
  # Warn plugin conflict
368
432
  #
369
433
  # @private
370
- # @param [Array<(Symbol, Object)>] dropped dropped plugin items
371
- # @param [Array<(Symbol, Object)>] plugin_items kept plugin items
434
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] dropped dropped plugin items
435
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] plugin_items kept plugin items
372
436
  # @param [Integer] max_prio the maximum priority value
373
437
  # @param [Integer] pos the source position of the conflict
374
438
  # @return [void]
@@ -386,7 +450,7 @@ module Docscribe
386
450
  #
387
451
  # @private
388
452
  # @param [Integer] pos the source position of the conflict
389
- # @param [Array<(Symbol, Object)>] plugin_items plugin items for location
453
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] plugin_items plugin items for location
390
454
  # @return [String]
391
455
  def conflict_location_str(pos, plugin_items)
392
456
  line = plugin_insertion_line(plugin_items.first[1])
@@ -396,9 +460,9 @@ module Docscribe
396
460
  # Pick highest priority override insertion
397
461
  #
398
462
  # @private
399
- # @param [Array<(Symbol, Object)>] override_items override items to prioritize
463
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] override_items override items to prioritize
400
464
  # @param [Integer] pos begin_pos (used only for debug output)
401
- # @return [Hash<Symbol, Object>, nil] winning insertion hash (the one whose override will be applied)
465
+ # @return [Docscribe::InlineRewriter::pluginInsertion, nil] winning insertion hash (the one whose override will be applied)
402
466
  def pick_highest_priority_override_insertion(override_items, pos:)
403
467
  return nil if override_items.empty?
404
468
 
@@ -414,7 +478,7 @@ module Docscribe
414
478
  # Max plugin priority for
415
479
  #
416
480
  # @private
417
- # @param [Array<(Symbol, Object)>] override_items override items to evaluate
481
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] override_items override items to evaluate
418
482
  # @return [Integer]
419
483
  def max_plugin_priority_for(override_items)
420
484
  override_items.map { |_k, ins| plugin_insertion_priority(ins) }.max || 0
@@ -423,8 +487,8 @@ module Docscribe
423
487
  # Sort winners by order
424
488
  #
425
489
  # @private
426
- # @param [Array<(Symbol, Object)>] winners winning items to sort
427
- # @return [Array<(Symbol, Object)>]
490
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] winners winning items to sort
491
+ # @return [Array<Docscribe::InlineRewriter::insertionPair>]
428
492
  def sort_winners_by_order(winners)
429
493
  winners.sort_by do |_k, ins|
430
494
  order = ins.is_a?(Hash) ? ins[:__docscribe_plugin_order] : nil
@@ -435,7 +499,7 @@ module Docscribe
435
499
  # Warn override conflict
436
500
  #
437
501
  # @private
438
- # @param [Array<(Symbol, Object)>] winners_sorted sorted winning items
502
+ # @param [Array<Docscribe::InlineRewriter::insertionPair>] winners_sorted sorted winning items
439
503
  # @param [Integer] max_prio the maximum priority value
440
504
  # @param [Integer] pos the source position of the conflict
441
505
  # @return [void]
@@ -455,7 +519,7 @@ module Docscribe
455
519
  # Plugin insertion priority
456
520
  #
457
521
  # @private
458
- # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
522
+ # @param [Docscribe::InlineRewriter::pluginInsertion, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
459
523
  # method insertion (plugin hash or collected insertion object)
460
524
  # @raise [StandardError]
461
525
  # @return [Integer]
@@ -471,7 +535,7 @@ module Docscribe
471
535
  # Plugin insertion label
472
536
  #
473
537
  # @private
474
- # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
538
+ # @param [Docscribe::InlineRewriter::pluginInsertion, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
475
539
  # method insertion (plugin hash or collected insertion object)
476
540
  # @raise [StandardError]
477
541
  # @return [String]
@@ -488,7 +552,7 @@ module Docscribe
488
552
  # Plugin insertion line
489
553
  #
490
554
  # @private
491
- # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
555
+ # @param [Docscribe::InlineRewriter::pluginInsertion, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
492
556
  # method insertion (plugin hash or collected insertion object)
493
557
  # @raise [StandardError]
494
558
  # @return [Integer, nil]
@@ -507,7 +571,7 @@ module Docscribe
507
571
  #
508
572
  # @private
509
573
  # @param [Symbol] kind :method, :attr, or :plugin
510
- # @param [Hash<Symbol, Object>] ins insertion to locate
574
+ # @param [Docscribe::InlineRewriter::pluginInsertion] ins insertion to locate
511
575
  # @return [Integer]
512
576
  def plugin_insertion_pos(kind, ins)
513
577
  case kind
@@ -525,7 +589,7 @@ module Docscribe
525
589
  # @private
526
590
  # @param [Parser::Source::TreeRewriter] rewriter the TreeRewriter accumulating source transformations
527
591
  # @param [Parser::Source::Buffer] buffer the source buffer
528
- # @param [Hash<Symbol, Object>] insertion { anchor_node:, doc: }
592
+ # @param [Docscribe::InlineRewriter::pluginInsertion] insertion { anchor_node:, doc: }
529
593
  # @param [Symbol] strategy :safe or :aggressive rewrite mode
530
594
  # @param [Docscribe::Config] config the active configuration
531
595
  # @return [void]
@@ -725,7 +789,7 @@ module Docscribe
725
789
  # Apply method insertion
726
790
  #
727
791
  # @private
728
- # @param [Object] options kwargs with insertion, config, rewriter, buffer, strategy, changes, file, doc params
792
+ # @param [Hash] options kwargs with insertion, config, rewriter, buffer, strategy, changes, file, doc params
729
793
  # @return [void]
730
794
  def apply_method_insertion!(**options)
731
795
  insertion = options[:insertion]
@@ -806,8 +870,8 @@ module Docscribe
806
870
  # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion
807
871
  # @param [Docscribe::Config] config the active configuration
808
872
  # @param [Docscribe::Types::ProviderChain, nil] signature_provider RBS signature provider
809
- # @param [Object, nil] core_rbs_provider optional externally-provided core RBS provider
810
- # @param [Hash<Symbol, Object>, nil] method_override the raw override data
873
+ # @param [Docscribe::Types::RBS::Provider, nil] core_rbs_provider optional externally-provided core RBS provider
874
+ # @param [Docscribe::InlineRewriter::methodOverride, nil] method_override the raw override data
811
875
  # @return [Hash<Symbol, Object>]
812
876
  def build_method_insertion_params(insertion, config, signature_provider, core_rbs_provider, method_override)
813
877
  override = extract_method_override!(method_override)
@@ -821,7 +885,7 @@ module Docscribe
821
885
  #
822
886
  # @private
823
887
  # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion
824
- # @param [Object] options keyword options
888
+ # @param [Hash] options keyword options
825
889
  # @return [Hash<Symbol, Hash<String, String>, nil, String, nil, Array<Docscribe::Plugin::Tag>>]
826
890
  def build_effective_params(insertion, **options)
827
891
  external_sig = resolve_external_signature(insertion, options[:signature_provider])
@@ -852,8 +916,8 @@ module Docscribe
852
916
  end
853
917
 
854
918
  # @private
855
- # @param [Object] insertion
856
- # @return [(Integer?, Array<Object>)]
919
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
920
+ # @return [(Integer?, Array<String>)]
857
921
  def extract_param_info(insertion)
858
922
  args = DocBuilder.extract_args_from_node(insertion.node)
859
923
  empty = [] #: Array[untyped]
@@ -876,7 +940,7 @@ module Docscribe
876
940
  # Apply method insertion aggressive
877
941
  #
878
942
  # @private
879
- # @param [Object] options keyword options
943
+ # @param [Hash] options keyword options
880
944
  # @return [void]
881
945
  def apply_method_insertion_aggressive!(**options)
882
946
  rewriter = options[:rewriter]
@@ -907,7 +971,7 @@ module Docscribe
907
971
  # Apply method insertion safe
908
972
  #
909
973
  # @private
910
- # @param [Object] options keyword options
974
+ # @param [Hash] options keyword options
911
975
  # @return [void]
912
976
  def apply_method_insertion_safe!(**options)
913
977
  info = method_doc_comment_info(options[:buffer], options[:insertion])
@@ -922,7 +986,7 @@ module Docscribe
922
986
  # Apply method insertion safe with info
923
987
  #
924
988
  # @private
925
- # @param [Object] options keyword options
989
+ # @param [Hash] options keyword options
926
990
  # @return [void]
927
991
  def apply_method_insertion_safe_with_info!(**options)
928
992
  i = options[:info]
@@ -941,9 +1005,9 @@ module Docscribe
941
1005
  # @private
942
1006
  # @param [Parser::Source::TreeRewriter] rewriter the TreeRewriter accumulating source transformations
943
1007
  # @param [Parser::Source::Buffer] buffer the source buffer being rewritten
944
- # @param [Hash<Symbol, Object>] info hash containing existing doc comment block data
1008
+ # @param [Docscribe::InlineRewriter::docInfo] info hash containing existing doc comment block data
945
1009
  # @param [String] new_block the newly constructed replacement doc block string
946
- # @param [Object] rest additional kwargs (old_block, merge_result,
1010
+ # @param [Hash] rest additional kwargs (old_block, merge_result,
947
1011
  # @return [void]
948
1012
  def commit_safe_doc_outcome(rewriter, buffer, info, new_block, **rest)
949
1013
  handle_doc_replacement(rewriter, buffer, info, new_block,
@@ -969,9 +1033,9 @@ module Docscribe
969
1033
  # @private
970
1034
  # @param [Parser::Source::TreeRewriter] rewriter the TreeRewriter accumulating source transformations
971
1035
  # @param [Parser::Source::Buffer] buffer the source buffer being rewritten
972
- # @param [Hash<Symbol, Object>] info hash containing existing doc comment block data (start_pos, end_pos, lines)
1036
+ # @param [Docscribe::InlineRewriter::docInfo] info hash containing existing doc comment block data (start_pos, end_pos, lines)
973
1037
  # @param [String] new_block the newly constructed replacement doc block string
974
- # @param [Object] log_opts additional keyword arguments for logging and recording changes
1038
+ # @param [Hash] log_opts additional keyword arguments for logging and recording changes
975
1039
  # @return [void]
976
1040
  def handle_doc_replacement(rewriter, buffer, info, new_block, **log_opts)
977
1041
  range = Parser::Source::Range.new(buffer, info[:start_pos], info[:end_pos])
@@ -987,44 +1051,146 @@ module Docscribe
987
1051
  # Compute doc replacement
988
1052
  #
989
1053
  # @private
990
- # @param [Hash<Symbol, Object>] info existing doc info
1054
+ # @param [Docscribe::InlineRewriter::docInfo] info existing doc info
991
1055
  # @param [Array<String>] missing_lines new doc lines to add
992
- # @param [Object] options keyword options
1056
+ # @param [Hash] options keyword options
993
1057
  # @return [(Boolean, String, String)]
994
1058
  def compute_doc_replacement(info, missing_lines, **options)
995
1059
  dc = options[:config]
1060
+ filter = filter_for_missing(info, missing_lines)
996
1061
  sorted = Docscribe::InlineRewriter::DocBlock.merge(
997
1062
  info[:doc_lines], missing_lines: [], sort_tags: dc.sort_tags?, tag_order: dc.tag_order
998
1063
  )
999
1064
  merged = Docscribe::InlineRewriter::DocBlock.merge(
1000
- info[:doc_lines], missing_lines: missing_lines, sort_tags: dc.sort_tags?, tag_order: dc.tag_order
1065
+ info[:doc_lines], missing_lines: missing_lines, sort_tags: dc.sort_tags?, tag_order: dc.tag_order,
1066
+ filter_existing: filter
1001
1067
  )
1002
1068
  [sorted != info[:doc_lines], (info[:preserved_lines] + merged).join, info[:lines].join]
1003
1069
  end
1004
1070
 
1071
+ # Build filter for DocBlock.merge when missing lines replace existing invalid tags.
1072
+ #
1073
+ # @private
1074
+ # @param [Docscribe::InlineRewriter::docInfo] info parsed doc info from method_doc_comment_info
1075
+ # @param [Array<String>] missing_lines generated missing lines
1076
+ # @return [Hash<Symbol, Object>] filter for existing entries
1077
+ def filter_for_missing(info, missing_lines)
1078
+ filter = {} #: Hash[Symbol, untyped]
1079
+ doc_lines = Array(info[:doc_lines]) #: Array[String]
1080
+ filter[:return] = true if return_filter_needed?(doc_lines, missing_lines)
1081
+ names = param_names_for_filter(doc_lines, missing_lines)
1082
+ filter[:param_names] = names if names.any?
1083
+ filter
1084
+ end
1085
+
1086
+ # Whether return filter is needed.
1087
+ #
1088
+ # @private
1089
+ # @param [Array<String>] doc_lines existing doc lines
1090
+ # @param [Array<String>] missing_lines generated missing lines
1091
+ # @return [Boolean]
1092
+ def return_filter_needed?(doc_lines, missing_lines)
1093
+ doc_lines.any? { |l| l.include?('@return') } && missing_lines.any? { |l| l.include?('@return') }
1094
+ end
1095
+
1096
+ # Param names that need filtering.
1097
+ #
1098
+ # @private
1099
+ # @param [Array<String>] doc_lines existing doc lines
1100
+ # @param [Array<String>] missing_lines generated missing lines
1101
+ # @return [Array<String>]
1102
+ def param_names_for_filter(doc_lines, missing_lines)
1103
+ missing_names = missing_lines.filter_map { |l| extract_param_name_for_filter(l) }
1104
+ existing_names = doc_lines.filter_map { |l| extract_param_name_for_filter(l) }
1105
+ missing_names & existing_names
1106
+ end
1107
+
1108
+ # Extract param name from a generated @param line for filtering.
1109
+ #
1110
+ # @private
1111
+ # @param [String] line generated param line
1112
+ # @return [String, nil]
1113
+ def extract_param_name_for_filter(line)
1114
+ content = line.sub(/^\s*#\s*/, '')
1115
+ if (m = content.match(/@param\s+(\S+)\s+\[/))
1116
+ return m[1]
1117
+ elsif (m = content.match(/@param\s+\[/))
1118
+ rest = content[(m.end(0) - 1)..] # steep:ignore
1119
+ type_end = rest.index(']') # steep:ignore
1120
+ return rest[(type_end + 1)..].to_s.strip.split(/\s+/).first if type_end # steep:ignore
1121
+ end
1122
+
1123
+ nil
1124
+ end
1125
+
1005
1126
  # Log method doc changes
1006
1127
  #
1007
1128
  # @private
1008
1129
  # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion
1009
1130
  # @param [Hash<Symbol, Object>] merge_result merge operation result
1010
- # @param [Object] rest additional keyword arguments forwarded to add_change
1131
+ # @param [Hash] rest additional keyword arguments forwarded to add_change
1011
1132
  # @return [void]
1012
1133
  def log_method_doc_changes!(insertion:, merge_result:, **rest)
1013
1134
  reason_specs = merge_result[:reasons] || []
1014
- type_mismatch_reasons = reason_specs.select { |r| %i[updated_param updated_return].include?(r[:type]) }
1135
+ return unless doc_changed?(rest, reason_specs)
1015
1136
 
1016
- return unless rest[:new_block] != rest[:old_block] || type_mismatch_reasons.any?
1137
+ log_reasons(reason_specs, insertion, rest)
1138
+ end
1017
1139
 
1018
- reason_specs.each do |reason|
1019
- add_change(changes: rest[:changes], type: reason[:type], insertion: insertion,
1020
- file: rest[:file], message: reason[:message], extra: reason[:extra] || {})
1021
- end
1140
+ # @private
1141
+ # @param [Hash<Symbol, Object>] rest
1142
+ # @param [Array<Hash<Symbol, Object>>] reason_specs
1143
+ # @return [Boolean]
1144
+ def doc_changed?(rest, reason_specs)
1145
+ rest[:new_block] != rest[:old_block] || type_mismatch_reasons?(reason_specs)
1146
+ end
1147
+
1148
+ # @private
1149
+ # @param [Array<Hash<Symbol, Object>>] reason_specs
1150
+ # @return [Boolean]
1151
+ def type_mismatch_reasons?(reason_specs)
1152
+ reason_specs.any? { |r| type_mismatch_type?(r[:type]) }
1153
+ end
1154
+
1155
+ # @private
1156
+ # @param [Symbol] type
1157
+ # @return [Boolean]
1158
+ def type_mismatch_type?(type)
1159
+ %i[updated_param updated_return invalid_type].include?(type)
1160
+ end
1161
+
1162
+ # @private
1163
+ # @param [Array<Hash<Symbol, Object>>] reason_specs
1164
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
1165
+ # @param [Hash<Symbol, Object>] rest
1166
+ # @return [void]
1167
+ def log_reasons(reason_specs, insertion, rest)
1168
+ reason_specs.each { |reason| log_single_reason(reason, insertion, rest) }
1169
+ end
1170
+
1171
+ # @private
1172
+ # @param [Hash<Symbol, Object>] reason
1173
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
1174
+ # @param [Hash<Symbol, Object>] rest
1175
+ # @return [void]
1176
+ def log_single_reason(reason, insertion, rest)
1177
+ add_change(changes: rest[:changes], type: reason[:type], insertion: insertion,
1178
+ file: rest[:file], message: reason[:message], extra: extra_for_reason(reason))
1179
+ end
1180
+
1181
+ # @private
1182
+ # @param [Hash<Symbol, Object>] reason
1183
+ # @return [Hash<Symbol, Object>]
1184
+ def extra_for_reason(reason)
1185
+ extra = (reason[:extra] || {}).dup
1186
+ extra[:source] = reason[:source] if reason[:source]
1187
+ extra
1022
1188
  end
1023
1189
 
1024
1190
  # Apply method insertion safe without info
1025
1191
  #
1026
1192
  # @private
1027
- # @param [Object] options keyword options
1193
+ # @param [Hash] options keyword options
1028
1194
  # @return [void]
1029
1195
  def apply_method_insertion_safe_without_info!(**options)
1030
1196
  rewriter = options[:rewriter]
@@ -1042,13 +1208,13 @@ module Docscribe
1042
1208
 
1043
1209
  # Filter options to keep only doc-building params for safe-without-info mode.
1044
1210
  # @private
1045
- # @param [Object] options the full options hash to filter
1211
+ # @param [Hash[Symbol, untyped]] options the full options hash to filter
1046
1212
  # @return [Object]
1047
1213
 
1048
1214
  # Add change
1049
1215
  #
1050
1216
  # @private
1051
- # @param [Object] options kwargs for change record (type, file, line, method, message, insertion, changes, extra)
1217
+ # @param [Hash] options kwargs for change record (type, file, line, method, message, insertion, changes, extra)
1052
1218
  # @return [void]
1053
1219
  def add_change(**options)
1054
1220
  changes = options[:changes]
@@ -1074,7 +1240,7 @@ module Docscribe
1074
1240
  # Apply attr insertion
1075
1241
  #
1076
1242
  # @private
1077
- # @param [Object] options kwargs (insertion, config, rewriter, buffer, strategy,
1243
+ # @param [Hash] options kwargs (insertion, config, rewriter, buffer, strategy,
1078
1244
  # @return [void]
1079
1245
  def apply_attr_insertion!(**options)
1080
1246
  config = options[:config]
@@ -1166,7 +1332,7 @@ module Docscribe
1166
1332
  # @param [Hash<Integer, Array<(Integer, String)>>] merge_inserts deferred merge inserts
1167
1333
  # @param [Docscribe::Config] config the active Docscribe::Config
1168
1334
  # @param [Docscribe::Types::ProviderChain, nil] signature_provider external RBS signature provider
1169
- # @return [void]
1335
+ # @return [Array<(Integer, String)>, nil]
1170
1336
  def merge_attr_additions!(insertion:, info:, merge_inserts:, config:, signature_provider:)
1171
1337
  additions = build_attr_merge_additions(ins: insertion, existing_lines: info[:lines],
1172
1338
  config: config, signature_provider: signature_provider)
@@ -1262,7 +1428,7 @@ module Docscribe
1262
1428
  end
1263
1429
 
1264
1430
  # @private
1265
- # @param [Object] ins
1431
+ # @param [Docscribe::InlineRewriter::Collector::AttrInsertion] ins
1266
1432
  # @param [Array<String>] existing_lines
1267
1433
  # @param [String] indent
1268
1434
  # @param [Docscribe::Config] config
@@ -1282,7 +1448,7 @@ module Docscribe
1282
1448
  end
1283
1449
 
1284
1450
  # @private
1285
- # @param [Object] ins
1451
+ # @param [Docscribe::InlineRewriter::Collector::AttrInsertion] ins
1286
1452
  # @param [Array<String>] existing_lines
1287
1453
  # @param [String] indent
1288
1454
  # @param [Docscribe::Config] config
@@ -1313,7 +1479,7 @@ module Docscribe
1313
1479
  #
1314
1480
  # @private
1315
1481
  # @param [Array<String>] lines array of existing doc comment lines
1316
- # @return [Hash<String, nil, Boolean>]
1482
+ # @return [Hash<String, Boolean>]
1317
1483
  def existing_attr_names(lines)
1318
1484
  names = {} #: Hash[String, bool]
1319
1485
 
@@ -1480,7 +1646,7 @@ module Docscribe
1480
1646
  # @param [Docscribe::InlineRewriter::Collector::AttrInsertion] ins the attribute insertion object
1481
1647
  # @param [Symbol] name_sym the attribute name as a Symbol
1482
1648
  # @param [String] indent whitespace indentation prefix
1483
- # @param [Object] opts additional keyword arguments forwarded from build_attr_doc_lines
1649
+ # @param [Hash] opts additional keyword arguments forwarded from build_attr_doc_lines
1484
1650
  # @return [Array<String>]
1485
1651
  def build_single_attr_lines(ins, name_sym, indent:, **opts)
1486
1652
  cfg = opts[:config]
@@ -1500,7 +1666,7 @@ module Docscribe
1500
1666
  # @param [String] indent whitespace indentation prefix
1501
1667
  # @param [String] attr_type the resolved type string for the attribute
1502
1668
  # @param [Symbol] access the access level (:r, :w, or :rw)
1503
- # @return [void]
1669
+ # @return [Array<String>, nil]
1504
1670
  def append_attr_return_tag(lines, indent, attr_type, access)
1505
1671
  lines << "#{indent}# @return [#{attr_type}]" if %i[r rw].include?(access)
1506
1672
  end
@@ -1513,7 +1679,7 @@ module Docscribe
1513
1679
  # @param [String] attr_type the resolved type string for the attribute
1514
1680
  # @param [Symbol] access the access level (:r, :w, or :rw)
1515
1681
  # @param [Docscribe::Config] cfg the active Docscribe::Config
1516
- # @return [void]
1682
+ # @return [Array<String>, nil]
1517
1683
  def append_attr_param_tag(lines, indent, attr_type, access, cfg)
1518
1684
  return unless %i[w rw].include?(access)
1519
1685
 
@@ -1582,7 +1748,7 @@ module Docscribe
1582
1748
  # @param [String] code the source code being processed
1583
1749
  # @param [String] file the file name
1584
1750
  # @raise [StandardError]
1585
- # @return [Object, nil]
1751
+ # @return [Docscribe::Types::ProviderChain, Docscribe::Types::RBS::Provider, nil]
1586
1752
  # @return [Docscribe::Types::RBS::Provider, nil?] if StandardError
1587
1753
  def build_signature_provider(config, code, file)
1588
1754
  if config.respond_to?(:signature_provider_for)
@@ -1601,7 +1767,7 @@ module Docscribe
1601
1767
  # @private
1602
1768
  # @param [Parser::Source::Buffer] buffer the source buffer
1603
1769
  # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion
1604
- # @return [Hash<Symbol, Object>, nil] doc comment block info or nil
1770
+ # @return [Docscribe::InlineRewriter::docInfo, nil] doc comment block info or nil
1605
1771
  def method_doc_comment_info(buffer, insertion)
1606
1772
  anchor_bol_range, def_bol_range = method_bol_ranges(buffer, insertion)
1607
1773
 
@@ -1665,8 +1831,8 @@ module Docscribe
1665
1831
  # Extract method override
1666
1832
  #
1667
1833
  # @private
1668
- # @param [Hash<Symbol, Object>, nil] method_override the raw override data
1669
- # @return [Hash<Symbol, Object>] normalized override hash
1834
+ # @param [Docscribe::InlineRewriter::methodOverride, nil] method_override the raw override data
1835
+ # @return [Docscribe::InlineRewriter::methodOverride] normalized override hash
1670
1836
  def extract_method_override!(method_override)
1671
1837
  return {} unless method_override.is_a?(Hash)
1672
1838