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
@@ -3,6 +3,9 @@
3
3
  require 'docscribe/plugin'
4
4
  require 'docscribe/infer'
5
5
  require 'docscribe/inline_rewriter/source_helpers'
6
+ require 'docscribe/types/yard/validator'
7
+ require 'docscribe/validator/type_mismatch_validator'
8
+ require 'docscribe/validator/generic_compatibility'
6
9
 
7
10
  module Docscribe
8
11
  module InlineRewriter
@@ -63,9 +66,9 @@ module Docscribe
63
66
  # Build
64
67
  #
65
68
  # @note module_function: defines #build (visibility: private)
66
- # @param [Object] insertion the collected method insertion object
69
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
67
70
  # @param [Docscribe::Config] config Docscribe configuration object
68
- # @param [Object] opts additional keyword options forwarded to doc_setup
71
+ # @param [Hash<Symbol, Object>] opts additional keyword options forwarded to doc_setup
69
72
  # @raise [StandardError]
70
73
  # @return [String, nil]
71
74
  # @return [nil] if StandardError
@@ -82,10 +85,10 @@ module Docscribe
82
85
  # Build merge additions
83
86
  #
84
87
  # @note module_function: defines #build_merge_additions (visibility: private)
85
- # @param [Object] insertion the collected method insertion object
88
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
86
89
  # @param [Array<String>] existing_lines existing doc comment lines being merged
87
90
  # @param [Docscribe::Config] config Docscribe configuration object
88
- # @param [Object] options additional keyword options forwarded to downstream methods
91
+ # @param [Hash<Symbol, Object>] options additional keyword options forwarded to downstream methods
89
92
  # @raise [StandardError]
90
93
  # @return [String, nil]
91
94
  # @return [nil] if StandardError
@@ -105,12 +108,12 @@ module Docscribe
105
108
  # Build missing merge result
106
109
  #
107
110
  # @note module_function: defines #build_missing_merge_result (visibility: private)
108
- # @param [Object] insertion the collected method insertion object
111
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
109
112
  # @param [Array<String>] existing_lines existing doc comment lines being merged
110
113
  # @param [Docscribe::Config] config Docscribe configuration object
111
- # @param [Object] options additional keyword options forwarded to downstream methods
114
+ # @param [Hash<Symbol, Object>] options additional keyword options forwarded to downstream methods
112
115
  # @raise [StandardError]
113
- # @return [Hash<Symbol, Object>]
116
+ # @return [Docscribe::InlineRewriter::DocBuilder::missingMergeResult]
114
117
  # @return [Hash] if StandardError
115
118
  def build_missing_merge_result(insertion, existing_lines:, config:, **options)
116
119
  setup = doc_setup(insertion, config: config, **options)
@@ -127,10 +130,10 @@ module Docscribe
127
130
  # Doc setup
128
131
  #
129
132
  # @note module_function: defines #doc_setup (visibility: private)
130
- # @param [Object] insertion the collected method insertion object
133
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
131
134
  # @param [Docscribe::Config] config Docscribe configuration object
132
- # @param [Object] opts additional options
133
- # @return [Hash<Symbol, Object>, nil]
135
+ # @param [Hash<Symbol, Object>] opts additional options
136
+ # @return [Docscribe::InlineRewriter::DocBuilder::setup, nil]
134
137
  def doc_setup(insertion, config:, **opts)
135
138
  node = insertion.node
136
139
  name = SourceHelpers.node_name(node)
@@ -143,10 +146,10 @@ module Docscribe
143
146
  # Build unsafe
144
147
  #
145
148
  # @note module_function: defines #build_unsafe (visibility: private)
146
- # @param [Object] insertion the collected method insertion object
149
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
147
150
  # @param [Docscribe::Config] config Docscribe configuration object
148
- # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
149
- # @param [Object] opts additional options including infer_default, fallback_type, treat_options_keyword_as_hash
151
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with name, normal_type, scope, visibility
152
+ # @param [Hash<Symbol, Object>] opts additional options including infer_default, fallback_type, treat_options_keyword_as_hash
150
153
  # @return [String]
151
154
  def build_unsafe(insertion, config:, setup:, **opts)
152
155
  _, pl, rt = build_param_and_raise_info(setup, config, opts)
@@ -160,7 +163,7 @@ module Docscribe
160
163
  # Build param and raise info
161
164
  #
162
165
  # @note module_function: defines #build_param_and_raise_info (visibility: private)
163
- # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
166
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with name, normal_type, scope, visibility
164
167
  # @param [Docscribe::Config] config Docscribe configuration object
165
168
  # @param [Hash<Symbol, Object>] opts additional options including
166
169
  # @return [(Hash<String, String>, nil, Array<String>, nil, Array<String>)]
@@ -179,12 +182,12 @@ module Docscribe
179
182
  # Resolve doc setup
180
183
  #
181
184
  # @note module_function: defines #resolve_doc_setup! (visibility: private)
182
- # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
185
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with name, normal_type, scope, visibility
183
186
  # @param [Parser::AST::Node] node AST node whose source text to extract
184
187
  # @param [Symbol] name the method name string
185
188
  # @param [Docscribe::Config] config Docscribe configuration object
186
189
  # @param [Hash<Symbol, Object>] opts additional options including
187
- # @return [Hash<Symbol, Object>]
190
+ # @return [Docscribe::InlineRewriter::DocBuilder::setup]
188
191
  def resolve_doc_setup!(setup, node, name, config, opts)
189
192
  external_sig = resolve_external_sig(setup[:container], setup[:scope], name, opts[:signature_provider], node)
190
193
  returns_spec = compute_returns_spec(node, config, opts[:param_types], opts[:core_rbs_provider],
@@ -202,9 +205,9 @@ module Docscribe
202
205
  # Extract base setup
203
206
  #
204
207
  # @note module_function: defines #extract_base_setup (visibility: private)
205
- # @param [Object] insertion the collected method insertion object
208
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
206
209
  # @param [Symbol] name the method name string
207
- # @return [Hash<Symbol, Object>]
210
+ # @return [Docscribe::InlineRewriter::DocBuilder::setup]
208
211
  def extract_base_setup(insertion, name)
209
212
  n = insertion.node
210
213
  { node: n, name: name, indent: SourceHelpers.line_indent(n), scope: insertion.scope,
@@ -247,10 +250,10 @@ module Docscribe
247
250
  # @param [Parser::AST::Node] node AST node whose source text to extract
248
251
  # @param [Docscribe::Config] config Docscribe configuration object
249
252
  # @param [Hash<String, String>, nil] param_types hash accumulating parameter name-to-type mappings
250
- # @param [Object] core_rbs_provider RBS type provider
253
+ # @param [Docscribe::Types::RBS::Provider, nil] core_rbs_provider RBS type provider
251
254
  # @param [Docscribe::Types::ProviderChain?] signature_provider
252
255
  # @param [String?] container
253
- # @return [Hash<Symbol, Object>]
256
+ # @return [Docscribe::InlineRewriter::DocBuilder::returnsSpec]
254
257
  def compute_returns_spec(node, config, param_types, core_rbs_provider, # rubocop:disable Metrics/ParameterLists
255
258
  signature_provider: nil, container: nil)
256
259
  Docscribe::Infer.returns_spec_from_node(
@@ -264,7 +267,7 @@ module Docscribe
264
267
  #
265
268
  # @note module_function: defines #parse_existing_doc_tags (visibility: private)
266
269
  # @param [Array<String>] lines existing doc comment lines
267
- # @return [Hash<Symbol, Object>] parsed tag info
270
+ # @return [Docscribe::InlineRewriter::DocBuilder::parseInfo] parsed tag info
268
271
  def parse_existing_doc_tags(lines)
269
272
  init = init_parse_info
270
273
  tags_started = false
@@ -343,7 +346,7 @@ module Docscribe
343
346
  #
344
347
  # @note module_function: defines #parse_existing_tag_line (visibility: private)
345
348
  # @param [String] line the doc comment line
346
- # @param [Hash<Symbol, Object>] info mutable parse info accumulator
349
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info mutable parse info accumulator
347
350
  # @param [Boolean] tags_started whether @tags have been seen
348
351
  # @return [Boolean] updated tags_started
349
352
  def parse_existing_tag_line(line, info, tags_started)
@@ -363,7 +366,7 @@ module Docscribe
363
366
  #
364
367
  # @note module_function: defines #start_note_tag (visibility: private)
365
368
  # @param [String] line doc comment line
366
- # @param [Hash<Symbol, Object>] info parse info hash
369
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash
367
370
  # @return [void]
368
371
  def start_note_tag(line, info)
369
372
  return if line.match?(/^\s*#\s*@note\s+module_function:/)
@@ -377,7 +380,7 @@ module Docscribe
377
380
  #
378
381
  # @note module_function: defines #append_note_continuation (visibility: private)
379
382
  # @param [String] line doc comment line
380
- # @param [Hash<Symbol, Object>] info parse info hash
383
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash
381
384
  # @return [Object]
382
385
  def append_note_continuation(line, info)
383
386
  return unless info[:last_tag] == :note && info[:note_lines].any?
@@ -388,7 +391,7 @@ module Docscribe
388
391
  # Init parse info
389
392
  #
390
393
  # @note module_function: defines #init_parse_info (visibility: private)
391
- # @return [Hash<Symbol, Object>]
394
+ # @return [Docscribe::InlineRewriter::DocBuilder::parseInfo]
392
395
  def init_parse_info
393
396
  {
394
397
  param_names: {}, param_types: {}, param_descriptions: {},
@@ -405,7 +408,7 @@ module Docscribe
405
408
  #
406
409
  # @note module_function: defines #merge_dest_lines (visibility: private)
407
410
  # @param [Array<String>] existing_lines existing doc comment lines to merge into
408
- # @param [Object] ctx merge context hash (setup, insertion, config, info, param_types)
411
+ # @param [Hash<Symbol, Object>] ctx merge context hash (setup, insertion, config, info, param_types)
409
412
  # @return [String, nil]
410
413
  def merge_dest_lines(existing_lines, **ctx)
411
414
  merge_lines_with_context(existing_lines, **ctx)
@@ -415,7 +418,7 @@ module Docscribe
415
418
  #
416
419
  # @note module_function: defines #merge_lines_with_context (visibility: private)
417
420
  # @param [Array<String>] existing_lines existing doc comment lines being merged
418
- # @param [Object] ctx merge context (setup, insertion, config, info, param_types)
421
+ # @param [Hash<Symbol, Object>] ctx merge context (setup, insertion, config, info, param_types)
419
422
  # @return [String]
420
423
  def merge_lines_with_context(existing_lines, **ctx)
421
424
  s = ctx[:setup]
@@ -445,7 +448,7 @@ module Docscribe
445
448
  #
446
449
  # @note module_function: defines #merge_all_tag_lines (visibility: private)
447
450
  # @param [Array<String>] base_ary initial line array
448
- # @param [Object] ctx context hash with setup, config, info, insertion, param_types
451
+ # @param [Hash<Symbol, Object>] ctx context hash with setup, config, info, insertion, param_types
449
452
  # @return [Array<String>]
450
453
  def merge_all_tag_lines(base_ary, **ctx)
451
454
  line_ary = base_ary.dup
@@ -497,9 +500,9 @@ module Docscribe
497
500
  # @note module_function: defines #merge_return_line (visibility: private)
498
501
  # @param [Array<String>] line_ary output line array
499
502
  # @param [String] indent indentation string for doc comment lines
500
- # @param [Hash<Symbol, Object>] setup method setup hash with node, name, types, scope
503
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with node, name, types, scope
501
504
  # @param [Docscribe::Config] config Docscribe configuration object
502
- # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
505
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash to update with visibility flags
503
506
  # @return [void]
504
507
  def merge_return_line(line_ary, indent, setup, config, info)
505
508
  emit_ret = config.emit_return_tag?(setup[:scope], setup[:visibility])
@@ -512,12 +515,12 @@ module Docscribe
512
515
  # Collect all missing
513
516
  #
514
517
  # @note module_function: defines #collect_all_missing (visibility: private)
515
- # @param [Hash<Symbol, Object>] setup resolved setup hash with node, name, indent, types
516
- # @param [Hash<Symbol, Object>] info parsed existing doc tag information
517
- # @param [Object] insertion the collected method insertion object
518
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup resolved setup hash with node, name, indent, types
519
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parsed existing doc tag information
520
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
518
521
  # @param [Docscribe::Config] config Docscribe configuration object
519
522
  # @param [Hash<Symbol, Object>] options additional options hash forwarded to missing collector
520
- # @return [Hash<Symbol, Object>]
523
+ # @return [Docscribe::InlineRewriter::DocBuilder::missingMergeResult]
521
524
  def collect_all_missing(setup, info, insertion, config, options)
522
525
  s = setup
523
526
  ctx = { node: s[:node], indent: s[:indent], config: config, external_sig: s[:external_sig],
@@ -531,7 +534,7 @@ module Docscribe
531
534
  #
532
535
  # @note module_function: defines #collect_missing_all (visibility: private)
533
536
  # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
534
- # @return [Hash<Symbol, Object>]
537
+ # @return [Docscribe::InlineRewriter::DocBuilder::missingMergeResult]
535
538
  def collect_missing_all(ctx)
536
539
  lines = [] #: Array[String]
537
540
  reasons = [] #: Array[Hash[Symbol, untyped]]
@@ -549,7 +552,7 @@ module Docscribe
549
552
  #
550
553
  # @note module_function: defines #extract_all_comment_tags (visibility: private)
551
554
  # @param [String] line single comment line
552
- # @param [Hash<Symbol, Object>] info parse info hash
555
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash
553
556
  # @return [void]
554
557
  def extract_all_comment_tags(line, info)
555
558
  extract_param_info(line, info[:param_names], info[:param_types], info[:param_descriptions])
@@ -585,7 +588,7 @@ module Docscribe
585
588
  #
586
589
  # @note module_function: defines #extract_return_info (visibility: private)
587
590
  # @param [String] line a single doc comment line to parse
588
- # @param [Hash<Symbol, Object>] info parse info hash to update with return data
591
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash to update with return data
589
592
  # @return [void]
590
593
  def extract_return_info(line, info)
591
594
  return unless line.match?(/^\s*#\s*@return\b/)
@@ -595,10 +598,26 @@ module Docscribe
595
598
  return unless (m = content.match(/@return\s+/))
596
599
 
597
600
  return_type, return_desc = parse_return_rest(m.post_match)
598
- info[:return_type] = return_type if return_type
601
+ return unless return_type
602
+ # Rescue-conditional `@return [X] if Error` tags describe rescue
603
+ # branches (see rescue_conditional_returns) and must not overwrite
604
+ # the main return type — otherwise check demands the conditional
605
+ # type while update_types regenerates it, ping-ponging forever.
606
+ return if conditional_return_desc?(return_desc)
607
+
608
+ info[:return_type] = return_type
599
609
  info[:return_description] = return_desc if return_desc
600
610
  end
601
611
 
612
+ # Whether a return description marks a rescue-conditional tag.
613
+ #
614
+ # @note module_function: defines #conditional_return_desc? (visibility: private)
615
+ # @param [String, nil] desc description after the type brackets
616
+ # @return [Boolean] true for "if Error" suffixes
617
+ def conditional_return_desc?(desc)
618
+ desc.to_s.start_with?('if ')
619
+ end
620
+
602
621
  # Parse return type from rest string
603
622
  #
604
623
  # @note module_function: defines #parse_return_rest (visibility: private)
@@ -618,7 +637,7 @@ module Docscribe
618
637
  #
619
638
  # @note module_function: defines #track_last_tag (visibility: private)
620
639
  # @param [String] content
621
- # @param [Hash<Symbol, Object>] info parse info hash
640
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash
622
641
  # @return [void]
623
642
  def track_last_tag(content, info)
624
643
  tag = content.match(/@(\w+)/)&.[](1)&.to_sym
@@ -633,7 +652,7 @@ module Docscribe
633
652
  #
634
653
  # @note module_function: defines #append_tag_continuation (visibility: private)
635
654
  # @param [String] content tag continuation text
636
- # @param [Hash<Symbol, Object>] info parse info hash
655
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash
637
656
  # @return [void]
638
657
  def append_tag_continuation(content, info)
639
658
  text = content.strip
@@ -647,7 +666,7 @@ module Docscribe
647
666
  #
648
667
  # @note module_function: defines #append_to_return_description (visibility: private)
649
668
  # @param [String] text text to append
650
- # @param [Hash<Symbol, Object>] info parse info hash
669
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash
651
670
  # @return [void]
652
671
  def append_to_return_description(text, info)
653
672
  if info[:return_description]
@@ -661,7 +680,7 @@ module Docscribe
661
680
  #
662
681
  # @note module_function: defines #append_to_param_description (visibility: private)
663
682
  # @param [String] text text to append
664
- # @param [Hash<Symbol, Object>] info parse info hash
683
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash
665
684
  # @return [void]
666
685
  def append_to_param_description(text, info)
667
686
  pname = info[:last_param]
@@ -678,7 +697,7 @@ module Docscribe
678
697
  #
679
698
  # @note module_function: defines #extract_visibility_info (visibility: private)
680
699
  # @param [String] line a single doc comment line to parse
681
- # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
700
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash to update with visibility flags
682
701
  # @return [void]
683
702
  def extract_visibility_info(line, info)
684
703
  info[:has_private] ||= line.match?(/^\s*#\s*@private\b/)
@@ -718,17 +737,37 @@ module Docscribe
718
737
  def extract_raise_types_from_line(line)
719
738
  return [] unless line.match?(/^\s*#\s*@raise\b/)
720
739
 
721
- if (m = line.match(/^\s*#\s*@raise\s*\[([^\]]+)\]/))
722
- parse_raise_bracket_list(m[1]) # steep:ignore ArgumentTypeMismatch
723
- elsif (m = line.match(/^\s*#\s*@raise\s+([A-Z]\w*(?:::[A-Z]\w*)*)/))
724
- [m[1]]
725
- else
726
- []
727
- end
740
+ bracketed_raise_types(line) || bare_raise_type(line) || []
728
741
  rescue StandardError
729
742
  []
730
743
  end
731
744
 
745
+ # Bracketed raise types from line
746
+ #
747
+ # @note module_function: defines #bracketed_raise_types (visibility: private)
748
+ # @param [String] line a `@raise` doc line
749
+ # @return [Array<String>, nil]
750
+ def bracketed_raise_types(line)
751
+ m = line.match(/^\s*#\s*@raise\s*\[([^\]]+)\]/)
752
+ return nil unless m
753
+
754
+ captured = m[1]
755
+ captured ? parse_raise_bracket_list(captured) : []
756
+ end
757
+
758
+ # Bare raise type from line
759
+ #
760
+ # @note module_function: defines #bare_raise_type (visibility: private)
761
+ # @param [String] line a `@raise` doc line
762
+ # @return [String, nil]
763
+ def bare_raise_type(line)
764
+ m = line.match(/^\s*#\s*@raise\s+([A-Z]\w*(?:::[A-Z]\w*)*)/)
765
+ return nil unless m
766
+
767
+ captured = m[1]
768
+ captured ? [captured] : []
769
+ end
770
+
732
771
  # Parse raise bracket list
733
772
  #
734
773
  # @note module_function: defines #parse_raise_bracket_list (visibility: private)
@@ -826,7 +865,7 @@ module Docscribe
826
865
  # @param [String] indent indentation string for the doc line
827
866
  # @param [Symbol] visibility method visibility symbol
828
867
  # @param [Docscribe::Config] config Docscribe configuration object
829
- # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
868
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash to update with visibility flags
830
869
  # @return [Array<String>]
831
870
  def merge_visibility_tag_lines(indent, visibility, config, info)
832
871
  return [] unless config.emit_visibility_tags?
@@ -844,9 +883,9 @@ module Docscribe
844
883
  #
845
884
  # @note module_function: defines #merge_module_function_note_lines (visibility: private)
846
885
  # @param [String] indent indentation string for the doc line
847
- # @param [Object] insertion the collected method insertion object
886
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
848
887
  # @param [String] name the method name string
849
- # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
888
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash to update with visibility flags
850
889
  # @return [Array<String>]
851
890
  def merge_module_function_note_lines(indent, insertion, name, info)
852
891
  return [] unless insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
@@ -861,7 +900,7 @@ module Docscribe
861
900
  # @param [Parser::AST::Node] node AST node whose source text to extract
862
901
  # @param [String] indent indentation string for the doc line
863
902
  # @param [Docscribe::Config] config Docscribe configuration object
864
- # @param [Object] opts additional options including external_sig, param_types, info
903
+ # @param [Hash<Symbol, Object>] opts additional options including external_sig, param_types, info
865
904
  # @return [Array<String>]
866
905
  def merge_param_lines(node, indent, config:, **opts)
867
906
  return [] unless config.emit_param_tags?
@@ -885,7 +924,7 @@ module Docscribe
885
924
  # @param [Parser::AST::Node] node AST node whose source text to extract
886
925
  # @param [String] indent indentation string for the doc line
887
926
  # @param [Docscribe::Config] config Docscribe configuration object
888
- # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
927
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash to update with visibility flags
889
928
  # @return [Array<String>]
890
929
  def merge_raise_tag_lines(node, indent, config, info)
891
930
  return [] unless config.emit_raise_tags?
@@ -902,7 +941,7 @@ module Docscribe
902
941
  # @param [String] indent indentation string for the doc line
903
942
  # @param [String] normal_type resolved return type
904
943
  # @param [Docscribe::Config] config Docscribe configuration object
905
- # @param [Object] opts additional options including scope, visibility, info
944
+ # @param [Hash<Symbol, Object>] opts additional options including scope, visibility, info
906
945
  # @return [String, nil]
907
946
  def merge_return_tag_line(indent, normal_type, config:, **opts)
908
947
  return unless config.emit_return_tag?(opts[:scope], opts[:visibility])
@@ -917,7 +956,7 @@ module Docscribe
917
956
  # @param [String] indent indentation string for the doc line
918
957
  # @param [Array<(Array<String>, String)>] rescue_specs rescue type specs
919
958
  # @param [Docscribe::Config] config Docscribe configuration object
920
- # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
959
+ # @param [Docscribe::InlineRewriter::DocBuilder::parseInfo] info parse info hash to update with visibility flags
921
960
  # @return [Array<String>]
922
961
  def merge_rescue_return_lines(indent, rescue_specs, config, info)
923
962
  return [] unless config.emit_rescue_conditional_returns?
@@ -933,7 +972,7 @@ module Docscribe
933
972
  # @note module_function: defines #collect_missing_visibility! (visibility: private)
934
973
  # @param [Array<String>] lines array of output doc lines being accumulated
935
974
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
936
- # @param [Object] ctx merged context hash with info and indent
975
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
937
976
  # @return [void]
938
977
  def collect_missing_visibility!(lines, reasons, **ctx)
939
978
  return unless ctx[:config].emit_visibility_tags?
@@ -975,7 +1014,7 @@ module Docscribe
975
1014
  # @note module_function: defines #collect_missing_module_function_note! (visibility: private)
976
1015
  # @param [Array<String>] lines array of output doc lines being accumulated
977
1016
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
978
- # @param [Object] ctx merged context hash with info and indent
1017
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
979
1018
  # @return [void]
980
1019
  def collect_missing_module_function_note!(lines, reasons, **ctx)
981
1020
  insertion = ctx[:insertion]
@@ -994,7 +1033,7 @@ module Docscribe
994
1033
  # @note module_function: defines #collect_missing_params! (visibility: private)
995
1034
  # @param [Array<String>] lines array of output doc lines being accumulated
996
1035
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
997
- # @param [Object] ctx merged context hash with info and indent
1036
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
998
1037
  # @return [void]
999
1038
  def collect_missing_params!(lines, reasons, **ctx)
1000
1039
  return unless ctx[:config].emit_param_tags?
@@ -1019,14 +1058,105 @@ module Docscribe
1019
1058
  pname = extract_param_name_from_param_line(param_line)
1020
1059
  return unless pname
1021
1060
 
1022
- if !ctx[:info][:param_names].include?(pname)
1023
- lines << "#{param_line}\n"
1024
- reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
1025
- elsif ctx[:external_sig] && ctx[:info][:param_types][pname]
1061
+ if missing_param?(pname, ctx)
1062
+ handle_missing_param(pname, param_line, lines, reasons)
1063
+ elsif existing_param_type?(pname, ctx)
1064
+ handle_existing_param(pname, param_line, lines, reasons, ctx)
1065
+ end
1066
+ end
1067
+
1068
+ # @note module_function: defines #missing_param? (visibility: private)
1069
+ # @param [String] pname
1070
+ # @param [Hash<Symbol, Object>] ctx
1071
+ # @return [Boolean]
1072
+ def missing_param?(pname, ctx)
1073
+ !ctx[:info][:param_names].include?(pname)
1074
+ end
1075
+
1076
+ # @note module_function: defines #existing_param_type? (visibility: private)
1077
+ # @param [String] pname
1078
+ # @param [Hash<Symbol, Object>] ctx
1079
+ # @return [Boolean]
1080
+ def existing_param_type?(pname, ctx)
1081
+ !!ctx[:info][:param_types][pname]
1082
+ end
1083
+
1084
+ # @note module_function: defines #handle_missing_param (visibility: private)
1085
+ # @param [String] pname
1086
+ # @param [String] param_line
1087
+ # @param [Array<String>] lines
1088
+ # @param [Array<Hash<Symbol, Object>>] reasons
1089
+ # @return [void]
1090
+ def handle_missing_param(pname, param_line, lines, reasons)
1091
+ lines << "#{param_line}\n"
1092
+ reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
1093
+ end
1094
+
1095
+ # @note module_function: defines #handle_existing_param (visibility: private)
1096
+ # @param [String] pname
1097
+ # @param [String] param_line
1098
+ # @param [Array<String>] lines
1099
+ # @param [Array<Hash<Symbol, Object>>] reasons
1100
+ # @param [Hash<Symbol, Object>] ctx
1101
+ # @return [void]
1102
+ def handle_existing_param(pname, param_line, lines, reasons, ctx)
1103
+ yard_type = ctx[:info][:param_types][pname]
1104
+ if invalid_yard_type?(yard_type)
1105
+ handle_invalid_param(pname, param_line, yard_type, lines, reasons)
1106
+ elsif param_needs_update?(ctx)
1026
1107
  collect_updated_param(param_line, pname, lines, reasons, ctx)
1027
1108
  end
1028
1109
  end
1029
1110
 
1111
+ # @note module_function: defines #handle_invalid_param (visibility: private)
1112
+ # @param [String] pname
1113
+ # @param [String] param_line
1114
+ # @param [String] yard_type
1115
+ # @param [Array<String>] lines
1116
+ # @param [Array<Hash<Symbol, Object>>] reasons
1117
+ # @return [void]
1118
+ def handle_invalid_param(pname, param_line, yard_type, lines, reasons)
1119
+ lines << "#{param_line}\n"
1120
+ reasons << {
1121
+ type: :invalid_type,
1122
+ message: "invalid YARD type [#{yard_type}] for @param #{pname}",
1123
+ source: 'syntax',
1124
+ extra: { param: pname }
1125
+ }
1126
+ end
1127
+
1128
+ # @note module_function: defines #param_needs_update? (visibility: private)
1129
+ # @param [Hash<Symbol, Object>] ctx
1130
+ # @return [Boolean]
1131
+ def param_needs_update?(ctx)
1132
+ should_validate_param?(ctx) || !!ctx[:external_sig]
1133
+ end
1134
+
1135
+ # Whether a YARD type string has invalid syntax.
1136
+ #
1137
+ # Called from both param (above) and return (below) validation, so no
1138
+ # waterfall placement satisfies the ordering cop — exempt.
1139
+ #
1140
+ # @note module_function: defines #invalid_yard_type? (visibility: private)
1141
+ # @param [String?] type_str
1142
+ # @return [Boolean]
1143
+ def invalid_yard_type?(type_str) # rubocop:disable SortedMethodsByCall/Waterfall
1144
+ return false if type_str.nil? || type_str.strip.empty?
1145
+ return true if type_str.match?(/\d/)
1146
+ return true if type_str.match?(/[^\x00-\x7F]/)
1147
+
1148
+ !Types::Yard::Validator.valid?(type_str)
1149
+ end
1150
+
1151
+ # Whether param validation should run via inferred/external types.
1152
+ #
1153
+ # @note module_function: defines #should_validate_param? (visibility: private)
1154
+ # @param [Hash<Symbol, Object>] ctx
1155
+ # @return [Boolean]
1156
+ def should_validate_param?(ctx)
1157
+ ctx[:config].respond_to?(:validate_types?) && ctx[:config].validate_types?
1158
+ end
1159
+
1030
1160
  # Collect updated param
1031
1161
  #
1032
1162
  # @note module_function: defines #collect_updated_param (visibility: private)
@@ -1038,12 +1168,48 @@ module Docscribe
1038
1168
  # @return [void]
1039
1169
  def collect_updated_param(param_line, pname, lines, reasons, ctx)
1040
1170
  new_type = extract_param_type_from_param_line(param_line)
1041
- return unless new_type && ctx[:info][:param_types][pname] != new_type
1171
+ return unless param_type_changed?(pname, new_type, ctx)
1172
+ return if fallback_skipped?(new_type, ctx)
1173
+
1174
+ append_param_update(param_line, pname, new_type, lines, reasons, ctx)
1175
+ end
1176
+
1177
+ # @note module_function: defines #param_type_changed? (visibility: private)
1178
+ # @param [String] pname
1179
+ # @param [String, nil] new_type
1180
+ # @param [Hash<Symbol, Object>] ctx
1181
+ # @return [Boolean]
1182
+ def param_type_changed?(pname, new_type, ctx)
1183
+ yard = ctx[:info][:param_types][pname]
1184
+ return false unless new_type && yard
1185
+ return false if normalize_type(yard) == normalize_type(new_type)
1186
+ return false if generic_compatible?(yard, new_type)
1187
+
1188
+ yard != new_type
1189
+ end
1042
1190
 
1191
+ # @note module_function: defines #fallback_skipped? (visibility: private)
1192
+ # @param [String, nil] new_type
1193
+ # @param [Hash<Symbol, Object>] ctx
1194
+ # @return [Boolean]
1195
+ def fallback_skipped?(new_type, ctx)
1196
+ ctx[:config].respond_to?(:validate_types?) && ctx[:config].validate_types? && (new_type == ctx[:config].fallback_type)
1197
+ end
1198
+
1199
+ # @note module_function: defines #append_param_update (visibility: private)
1200
+ # @param [String] param_line
1201
+ # @param [String] pname
1202
+ # @param [String, nil] new_type
1203
+ # @param [Array<String>] lines
1204
+ # @param [Array<Hash<Symbol, Object>>] reasons
1205
+ # @param [Hash<Symbol, Object>] ctx
1206
+ # @return [void]
1207
+ def append_param_update(param_line, pname, new_type, lines, reasons, ctx) # rubocop:disable Metrics/ParameterLists
1043
1208
  lines << "#{param_line}\n" unless ctx[:strategy] == :safe
1044
1209
  reasons << {
1045
1210
  type: :updated_param,
1046
1211
  message: "updated @param #{pname} from #{ctx[:info][:param_types][pname]} to #{new_type}",
1212
+ source: ctx[:external_sig] ? 'rbs' : 'infer',
1047
1213
  extra: { param: pname }
1048
1214
  }
1049
1215
  end
@@ -1071,7 +1237,7 @@ module Docscribe
1071
1237
  # @param [String] indent indentation string for the doc line
1072
1238
  # @param [Docscribe::Config] config Docscribe configuration object
1073
1239
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1074
- # @param [Object] kwargs additional keyword args including insertion, params_lines, raise_types, override_tags
1240
+ # @param [Hash<Symbol, Object>] kwargs additional keyword args including insertion, params_lines, raise_types, override_tags
1075
1241
  # @return [Array<String>, nil]
1076
1242
  def build_all_param_lines(args, indent, config, external_sig: nil, **kwargs)
1077
1243
  param_lines = [] #: Array[String]
@@ -1101,9 +1267,9 @@ module Docscribe
1101
1267
  # Build doc lines
1102
1268
  #
1103
1269
  # @note module_function: defines #build_doc_lines (visibility: private)
1104
- # @param [Hash<Symbol, Object>] setup method setup hash with indent, name, types, scope
1270
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with indent, name, types, scope
1105
1271
  # @param [Docscribe::Config] config Docscribe configuration object
1106
- # @param [Object] kwargs additional keyword args including insertion, params_lines, raise_types, override_tags
1272
+ # @param [Hash<Symbol, Object>] kwargs additional keyword args including insertion, params_lines, raise_types, override_tags
1107
1273
  # @return [Array<String>]
1108
1274
  def build_doc_lines(setup, config:, **kwargs)
1109
1275
  i = setup[:indent]
@@ -1118,8 +1284,8 @@ module Docscribe
1118
1284
  #
1119
1285
  # @note module_function: defines #assemble_doc_lines (visibility: private)
1120
1286
  # @param [String] indent indent
1121
- # @param [Hash<Symbol, Object>] setup setup
1122
- # @param [Object] ctx context hash with config, insertion, params_lines, raise_types, override_tags
1287
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup setup
1288
+ # @param [Hash<Symbol, Object>] ctx context hash with config, insertion, params_lines, raise_types, override_tags
1123
1289
  # @return [Array<String>]
1124
1290
  def assemble_doc_lines(indent, setup, **ctx)
1125
1291
  line_ary = build_header_lines(
@@ -1138,7 +1304,7 @@ module Docscribe
1138
1304
  # @note module_function: defines #append_assemble_body_lines (visibility: private)
1139
1305
  # @param [Array<String>] line_ary output line array
1140
1306
  # @param [String] indent indentation string for doc comment lines
1141
- # @param [Hash<Symbol, Object>] setup method setup hash with name, types, scope
1307
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with name, types, scope
1142
1308
  # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1143
1309
  # @return [void]
1144
1310
  def append_assemble_body_lines(line_ary, indent, setup, ctx)
@@ -1149,7 +1315,7 @@ module Docscribe
1149
1315
  #
1150
1316
  # @note module_function: defines #build_all_body_tags (visibility: private)
1151
1317
  # @param [String] indent indentation string for doc comment lines
1152
- # @param [Hash<Symbol, Object>] setup method setup hash with name, types, scope
1318
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with name, types, scope
1153
1319
  # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1154
1320
  # @return [Array<String>]
1155
1321
  def build_all_body_tags(indent, setup, ctx)
@@ -1162,9 +1328,9 @@ module Docscribe
1162
1328
  #
1163
1329
  # @note module_function: defines #core_body_tags (visibility: private)
1164
1330
  # @param [String] indent indentation string for doc comment lines
1165
- # @param [Hash<Symbol, Object>] setup method setup hash with name, types, scope
1331
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with name, types, scope
1166
1332
  # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1167
- # @return [Array<Object>]
1333
+ # @return [Array<String>]
1168
1334
  def core_body_tags(indent, setup, ctx)
1169
1335
  config, insertion = ctx.values_at(:config, :insertion)
1170
1336
  [
@@ -1198,7 +1364,7 @@ module Docscribe
1198
1364
  #
1199
1365
  # @note module_function: defines #build_return_line_if_needed (visibility: private)
1200
1366
  # @param [String] indent indentation string for doc comment lines
1201
- # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
1367
+ # @param [Docscribe::InlineRewriter::DocBuilder::setup] setup method setup hash with name, normal_type, scope, visibility
1202
1368
  # @param [Docscribe::Config] config Docscribe configuration object
1203
1369
  # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1204
1370
  # @return [Array<String>]
@@ -1232,7 +1398,7 @@ module Docscribe
1232
1398
  # @param [String] indent indentation string for doc comment lines
1233
1399
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1234
1400
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1235
- # @param [Object] opts additional options for param formatting (fallback_type, param_tag_style, etc.)
1401
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting (fallback_type, param_tag_style, etc.)
1236
1402
  # @return [Array<String>]
1237
1403
  def build_param_line(arg_node, indent, external_sig, param_types_override, **opts)
1238
1404
  method_name = :"build_#{arg_node.type}_line"
@@ -1253,7 +1419,7 @@ module Docscribe
1253
1419
  # @note module_function: defines #build_header_lines (visibility: private)
1254
1420
  # @param [String] indent indentation string for the doc line
1255
1421
  # @param [Docscribe::Config] config Docscribe configuration object
1256
- # @param [Object] opts additional options including container, method_symbol, name, normal_type
1422
+ # @param [Hash<Symbol, Object>] opts additional options including container, method_symbol, name, normal_type
1257
1423
  # @return [Array<String>]
1258
1424
  def build_header_lines(indent, config:, **opts)
1259
1425
  if config.emit_header?
@@ -1309,7 +1475,7 @@ module Docscribe
1309
1475
  #
1310
1476
  # @note module_function: defines #build_module_function_note_lines (visibility: private)
1311
1477
  # @param [String] indent indentation string for the doc line
1312
- # @param [Object] insertion the collected method insertion object
1478
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
1313
1479
  # @param [String] name the method name string
1314
1480
  # @return [Array<String>]
1315
1481
  def build_module_function_note_lines(indent, insertion, name)
@@ -1371,10 +1537,10 @@ module Docscribe
1371
1537
  # Build plugin tag lines
1372
1538
  #
1373
1539
  # @note module_function: defines #build_plugin_tag_lines (visibility: private)
1374
- # @param [Object] insertion the collected method insertion object
1540
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
1375
1541
  # @param [String] indent indentation string for the doc line
1376
1542
  # @param [String] normal_type resolved return type
1377
- # @param [Array<Object>, nil] override_tags plugin tag overrides
1543
+ # @param [Array<Docscribe::Plugin::Tag>, nil] override_tags plugin tag overrides
1378
1544
  # @return [Array<String>]
1379
1545
  def build_plugin_tag_lines(insertion, indent, normal_type, override_tags)
1380
1546
  plugin_tags = Docscribe::Plugin.run_tag_plugins(build_plugin_context(insertion, normal_type: normal_type))
@@ -1389,7 +1555,7 @@ module Docscribe
1389
1555
  # @param [String] indent indentation string for doc comment lines
1390
1556
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1391
1557
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1392
- # @param [Object] opts additional options for param formatting
1558
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting
1393
1559
  # @return [String]
1394
1560
  def build_arg_line(arg_node, indent, external_sig, param_types_override, **opts)
1395
1561
  pname = arg_node.children.first.to_s
@@ -1407,7 +1573,7 @@ module Docscribe
1407
1573
  # @param [String] indent indentation string for doc comment lines
1408
1574
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1409
1575
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1410
- # @param [Object] opts additional options for param formatting
1576
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting
1411
1577
  # @return [Array<String>]
1412
1578
  def build_optarg_lines(arg_node, indent, external_sig, param_types_override, **opts)
1413
1579
  pname, default = *arg_node
@@ -1463,7 +1629,7 @@ module Docscribe
1463
1629
  # @param [String] indent indentation string for doc comment lines
1464
1630
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1465
1631
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1466
- # @param [Object] opts additional options for param formatting
1632
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting
1467
1633
  # @return [String]
1468
1634
  def build_kwarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1469
1635
  pname = arg_node.children.first.to_s
@@ -1481,7 +1647,7 @@ module Docscribe
1481
1647
  # @param [String] indent indentation string for doc comment lines
1482
1648
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1483
1649
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1484
- # @param [Object] opts additional options for param formatting
1650
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting
1485
1651
  # @return [String]
1486
1652
  def build_kwoptarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1487
1653
  pname, default = *arg_node
@@ -1502,7 +1668,7 @@ module Docscribe
1502
1668
  # @param [String] indent indentation string for doc comment lines
1503
1669
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1504
1670
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1505
- # @param [Object] opts additional options for param formatting
1671
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting
1506
1672
  # @return [String]
1507
1673
  def build_restarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1508
1674
  pname = (arg_node.children.first || 'args').to_s
@@ -1523,7 +1689,7 @@ module Docscribe
1523
1689
  # @param [String] indent indentation string for doc comment lines
1524
1690
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1525
1691
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1526
- # @param [Object] opts additional options for param formatting
1692
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting
1527
1693
  # @return [String]
1528
1694
  def build_kwrestarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1529
1695
  pname = (arg_node.children.first || 'kwargs').to_s
@@ -1540,7 +1706,7 @@ module Docscribe
1540
1706
  # @param [String] indent indentation string for doc comment lines
1541
1707
  # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1542
1708
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1543
- # @param [Object] opts additional options for param formatting
1709
+ # @param [Hash<Symbol, Object>] opts additional options for param formatting
1544
1710
  # @return [String]
1545
1711
  def build_blockarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1546
1712
  pname = (arg_node.children.first || 'block').to_s
@@ -1558,7 +1724,7 @@ module Docscribe
1558
1724
  # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1559
1725
  # @param [String] pname the parameter name string
1560
1726
  # @param [String] infer_name parameter name string or transformed version for inference
1561
- # @param [Object] opts additional options including infer_default, fallback_type, treat_options_keyword_as_hash
1727
+ # @param [Hash<Symbol, Object>] opts additional options including infer_default, fallback_type, treat_options_keyword_as_hash
1562
1728
  # @return [String]
1563
1729
  def lookup_param_type(external_sig, param_types_override, pname, infer_name, **opts)
1564
1730
  external_sig&.param_types&.[](pname) ||
@@ -1705,7 +1871,7 @@ module Docscribe
1705
1871
  #
1706
1872
  # @note module_function: defines #override_param_type_for (visibility: private)
1707
1873
  # @param [String] pname the parameter name to look up
1708
- # @param [Hash<Object, Object>, nil] override_map hash map of parameter name to override type
1874
+ # @param [Hash<Object, String>, nil] override_map hash map of parameter name to override type
1709
1875
  # @return [String, nil]
1710
1876
  def override_param_type_for(pname, override_map)
1711
1877
  return nil unless override_map
@@ -1823,7 +1989,7 @@ module Docscribe
1823
1989
  # @note module_function: defines #collect_missing_raises! (visibility: private)
1824
1990
  # @param [Array<String>] lines array of output doc lines being accumulated
1825
1991
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1826
- # @param [Object] ctx merged context hash with info and indent
1992
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1827
1993
  # @return [void]
1828
1994
  def collect_missing_raises!(lines, reasons, **ctx)
1829
1995
  return unless ctx[:config].emit_raise_tags?
@@ -1843,18 +2009,234 @@ module Docscribe
1843
2009
  # @note module_function: defines #collect_missing_return! (visibility: private)
1844
2010
  # @param [Array<String>] lines array of output doc lines being accumulated
1845
2011
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1846
- # @param [Object] ctx merged context hash with info and indent
2012
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1847
2013
  # @return [void]
1848
2014
  def collect_missing_return!(lines, reasons, **ctx)
1849
2015
  return unless ctx[:config].emit_return_tag?(ctx[:scope], ctx[:visibility])
1850
2016
 
1851
2017
  if !ctx[:info][:has_return]
1852
2018
  record_missing_return(lines, reasons, ctx)
2019
+ elsif invalid_yard_return?(ctx)
2020
+ record_invalid_return(lines, reasons, ctx)
1853
2021
  elsif return_type_changed?(ctx)
1854
2022
  record_updated_return(lines, reasons, ctx)
2023
+ elsif should_validate_return?(ctx) && mismatched_return?(ctx) # rubocop:disable Lint/DuplicateBranch
2024
+ record_updated_return(lines, reasons, ctx)
1855
2025
  end
1856
2026
  end
1857
2027
 
2028
+ # Whether YARD return type has invalid syntax.
2029
+ #
2030
+ # @note module_function: defines #invalid_yard_return? (visibility: private)
2031
+ # @param [Hash<Symbol, Object>] ctx
2032
+ # @return [Boolean]
2033
+ def invalid_yard_return?(ctx)
2034
+ yard = ctx[:info][:return_type]
2035
+ return false unless yard
2036
+
2037
+ invalid_yard_type?(yard)
2038
+ end
2039
+
2040
+ # Record invalid return type.
2041
+ #
2042
+ # @note module_function: defines #record_invalid_return (visibility: private)
2043
+ # @param [Array<String>] lines
2044
+ # @param [Array<Hash<Symbol, Object>>] reasons
2045
+ # @param [Hash<Symbol, Object>] ctx
2046
+ # @return [void]
2047
+ def record_invalid_return(lines, reasons, ctx)
2048
+ yard = ctx[:info][:return_type]
2049
+ lines << "#{ctx[:indent]}# @return [#{ctx[:normal_type]}]\n"
2050
+ reasons << {
2051
+ type: :invalid_type,
2052
+ message: "invalid YARD type [#{yard}] for @return, expected [#{ctx[:normal_type]}]",
2053
+ source: 'syntax'
2054
+ }
2055
+ end
2056
+
2057
+ # Whether return validation should run via inferred types.
2058
+ #
2059
+ # @note module_function: defines #should_validate_return? (visibility: private)
2060
+ # @param [Hash<Symbol, Object>] ctx
2061
+ # @return [Boolean]
2062
+ def should_validate_return?(ctx)
2063
+ ctx[:config].respond_to?(:validate_types?) && ctx[:config].validate_types?
2064
+ end
2065
+
2066
+ # Whether YARD return mismatches expected inferred/external type.
2067
+ #
2068
+ # Silences when expected is fallback (uncertain).
2069
+ #
2070
+ # @note module_function: defines #mismatched_return? (visibility: private)
2071
+ # @param [Hash<Symbol, Object>] ctx
2072
+ # @return [Boolean]
2073
+ def mismatched_return?(ctx)
2074
+ yard, expected, fallback = mismatched_return_types(ctx)
2075
+ return false unless yard && expected
2076
+ return false if expected_suppressed?(expected, fallback)
2077
+
2078
+ method_name = extract_method_name(ctx)
2079
+ return false if yard_compatible?(yard, expected, fallback, method_name: method_name)
2080
+ return false if types_normalized_equal?(yard, expected)
2081
+
2082
+ true
2083
+ end
2084
+
2085
+ # Extract method name for void compatibility dynamic check.
2086
+ #
2087
+ # @note module_function: defines #extract_method_name (visibility: private)
2088
+ # @param [Hash<Symbol, Object>] ctx context hash with insertion or node
2089
+ # @raise [StandardError]
2090
+ # @return [Symbol, nil]
2091
+ # @return [nil] if StandardError
2092
+ def extract_method_name(ctx)
2093
+ insertion = ctx[:insertion]
2094
+ node = insertion&.node || ctx[:node]
2095
+ return nil unless node
2096
+
2097
+ SourceHelpers.node_name(node)
2098
+ rescue StandardError
2099
+ nil
2100
+ end
2101
+
2102
+ # Extract yard/expected/fallback triple for return mismatch check.
2103
+ #
2104
+ # @note module_function: defines #mismatched_return_types (visibility: private)
2105
+ # @param [Hash<Symbol, Object>] ctx
2106
+ # @return [(String?, String?, String)]
2107
+ def mismatched_return_types(ctx)
2108
+ [ctx[:info][:return_type], ctx[:normal_type], ctx[:config].fallback_type]
2109
+ end
2110
+
2111
+ # Whether expected type is suppressed as fallback.
2112
+ #
2113
+ # @note module_function: defines #expected_suppressed? (visibility: private)
2114
+ # @param [String?] expected
2115
+ # @param [String] fallback
2116
+ # @return [Boolean]
2117
+ def expected_suppressed?(expected, fallback)
2118
+ expected == fallback || fallback_union?(expected, fallback)
2119
+ end
2120
+
2121
+ # Whether yard type is compatible with expected via void/union/generic.
2122
+ #
2123
+ # @note module_function: defines #yard_compatible? (visibility: private)
2124
+ # @param [String?] yard
2125
+ # @param [String?] expected
2126
+ # @param [String] fallback
2127
+ # @param [String, Symbol, nil] method_name method name for void compatibility
2128
+ # @return [Boolean]
2129
+ def yard_compatible?(yard, expected, fallback, method_name: nil)
2130
+ void_compatible?(yard, expected, fallback, method_name: method_name) ||
2131
+ yard_in_expected_union?(yard, expected) ||
2132
+ generic_compatible?(yard, expected, method_name: method_name)
2133
+ end
2134
+
2135
+ # Whether types are equal after normalization (including optional "?").
2136
+ #
2137
+ # @note module_function: defines #types_normalized_equal? (visibility: private)
2138
+ # @param [String?] yard
2139
+ # @param [String?] expected
2140
+ # @return [Boolean]
2141
+ def types_normalized_equal?(yard, expected)
2142
+ normalized_equal?(yard, expected) || optional_normalized_equal?(yard, expected)
2143
+ end
2144
+
2145
+ # Whether normalized types are equal.
2146
+ #
2147
+ # @note module_function: defines #normalized_equal? (visibility: private)
2148
+ # @param [String?] yard
2149
+ # @param [String?] expected
2150
+ # @return [Boolean]
2151
+ def normalized_equal?(yard, expected)
2152
+ normalize_type(yard) == normalize_type(expected)
2153
+ end
2154
+
2155
+ # Whether optional-normalized types are equal.
2156
+ #
2157
+ # @note module_function: defines #optional_normalized_equal? (visibility: private)
2158
+ # @param [String?] yard
2159
+ # @param [String?] expected
2160
+ # @return [Boolean]
2161
+ def optional_normalized_equal?(yard, expected)
2162
+ normalize_type(yard).delete_suffix('?') == normalize_type(expected).delete_suffix('?')
2163
+ end
2164
+
2165
+ # Delegates to GenericCompatibility service (dynamic, map-dispatched, no hardcodes).
2166
+ #
2167
+ # @note module_function: defines #generic_compatible? (visibility: private)
2168
+ # @param [String] yard
2169
+ # @param [String] expected
2170
+ # @param [String, Symbol, nil] method_name method name for void compatibility threading
2171
+ # @return [Boolean]
2172
+ def generic_compatible?(yard, expected, method_name: nil)
2173
+ Docscribe::Validator::GenericCompatibility.compatible?(yard, expected, fallback_type: 'Object', method_name: method_name)
2174
+ end
2175
+
2176
+ # Whether yard type is included in expected union (e.g. Boolean in Object, Boolean).
2177
+ #
2178
+ # @note module_function: defines #yard_in_expected_union? (visibility: private)
2179
+ # @param [String] yard
2180
+ # @param [String] expected
2181
+ # @return [Boolean]
2182
+ def yard_in_expected_union?(yard, expected)
2183
+ normalized_yard = normalize_type(yard)
2184
+ expected.split(',').any? { |part| normalize_type(part) == normalized_yard }
2185
+ end
2186
+
2187
+ # Whether void YARD type is compatible with fallback union or initialize/setup dynamic.
2188
+ #
2189
+ # @note module_function: defines #void_compatible? (visibility: private)
2190
+ # @param [String, nil] yard
2191
+ # @param [String, nil] expected
2192
+ # @param [String] fallback
2193
+ # @param [String, Symbol, nil] method_name method name for dynamic check
2194
+ # @return [Boolean]
2195
+ def void_compatible?(yard, expected, fallback, method_name: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
2196
+ return false unless normalize_type(yard) == 'void'
2197
+
2198
+ return true if fallback_union?(expected, fallback) ||
2199
+ %w[nil void].include?(normalize_type(expected))
2200
+
2201
+ if method_name.to_s =~ /initialize|setup/
2202
+ norm = normalize_type(expected).delete_suffix('?').strip
2203
+ return true if norm == 'Hash' || norm.start_with?('Hash<') || norm.start_with?('Hash[')
2204
+ return true if %w[self Boolean].include?(norm)
2205
+ end
2206
+
2207
+ if method_name.to_s.end_with?('?')
2208
+ norm = normalize_type(expected).delete_suffix('?').strip
2209
+ return true if norm == 'Boolean'
2210
+ end
2211
+
2212
+ false
2213
+ end
2214
+
2215
+ # Whether a type string is a union of only fallback types (with optional `?`).
2216
+ #
2217
+ # @note module_function: defines #fallback_union? (visibility: private)
2218
+ # @param [String, nil] type_str
2219
+ # @param [String] fallback
2220
+ # @return [Boolean]
2221
+ def fallback_union?(type_str, fallback)
2222
+ return false if type_str.nil? || type_str.strip.empty?
2223
+
2224
+ fallback_norm = normalize_type(fallback)
2225
+ parts = type_str.to_s.split(',').map { |p| normalize_type(p.strip.delete_suffix('?').strip) }
2226
+ parts.all? { |p| p == fallback_norm || p.empty? }
2227
+ end
2228
+
2229
+ # Normalize type string for comparison (unify RBS/YARD syntax).
2230
+ #
2231
+ # @note module_function: defines #normalize_type (visibility: private)
2232
+ # @param [String, nil] type_str
2233
+ # @return [String]
2234
+ def normalize_type(type_str)
2235
+ s = type_str.to_s
2236
+ s = s.sub(/#.*\z/m, '').strip unless s.lstrip.start_with?('#')
2237
+ s.strip.squeeze(' ').gsub('[', '<').gsub(']', '>').gsub(/\buntyped\b/, 'Object').gsub(/\bFALLBACK_TYPE\b/, 'Object')
2238
+ end
2239
+
1858
2240
  # Record missing return
1859
2241
  #
1860
2242
  # @note module_function: defines #record_missing_return (visibility: private)
@@ -1877,7 +2259,8 @@ module Docscribe
1877
2259
  def record_updated_return(lines, reasons, ctx)
1878
2260
  lines << "#{ctx[:indent]}# @return [#{ctx[:normal_type]}]\n" unless ctx[:strategy] == :safe
1879
2261
  reasons << { type: :updated_return,
1880
- message: "updated @return from #{ctx[:info][:return_type]} to #{ctx[:normal_type]}" }
2262
+ message: "updated @return from #{ctx[:info][:return_type]} to #{ctx[:normal_type]}",
2263
+ source: ctx[:external_sig] ? 'rbs' : 'infer' }
1881
2264
  end
1882
2265
 
1883
2266
  # Return type changed
@@ -1894,7 +2277,7 @@ module Docscribe
1894
2277
  # @note module_function: defines #collect_missing_rescue_returns! (visibility: private)
1895
2278
  # @param [Array<String>] lines array of output doc lines being accumulated
1896
2279
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1897
- # @param [Object] ctx merged context hash with info and indent
2280
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1898
2281
  # @return [void]
1899
2282
  def collect_missing_rescue_returns!(lines, reasons, **ctx)
1900
2283
  return unless ctx[:config].emit_rescue_conditional_returns?
@@ -1914,7 +2297,7 @@ module Docscribe
1914
2297
  # @note module_function: defines #collect_missing_plugin_tags! (visibility: private)
1915
2298
  # @param [Array<String>] lines array of output doc lines being accumulated
1916
2299
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1917
- # @param [Object] ctx merged context hash with info and indent
2300
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1918
2301
  # @return [void]
1919
2302
  def collect_missing_plugin_tags!(lines, reasons, **ctx)
1920
2303
  plugin_tags = Docscribe::Plugin.run_tag_plugins(build_plugin_context(ctx[:insertion],
@@ -1927,7 +2310,7 @@ module Docscribe
1927
2310
  # Record plugin tag
1928
2311
  #
1929
2312
  # @note module_function: defines #record_plugin_tag (visibility: private)
1930
- # @param [Object] tag plugin tag object to render and record
2313
+ # @param [Docscribe::Plugin::Tag] tag plugin tag object to render and record
1931
2314
  # @param [Array<String>] lines array of output doc lines being accumulated
1932
2315
  # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1933
2316
  # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
@@ -1944,7 +2327,7 @@ module Docscribe
1944
2327
  #
1945
2328
  # @note module_function: defines #debug_warn (visibility: private)
1946
2329
  # @param [StandardError] error the error that occurred
1947
- # @param [Object] insertion the method insertion being processed
2330
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the method insertion being processed
1948
2331
  # @param [String] name the method name
1949
2332
  # @param [String] phase the processing phase
1950
2333
  # @return [void]
@@ -1958,7 +2341,7 @@ module Docscribe
1958
2341
  # Build debug location
1959
2342
  #
1960
2343
  # @note module_function: defines #build_debug_location (visibility: private)
1961
- # @param [Object] insertion the collected method insertion object
2344
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
1962
2345
  # @param [String] name the method name string
1963
2346
  # @return [String]
1964
2347
  def build_debug_location(insertion, name)
@@ -1982,9 +2365,9 @@ module Docscribe
1982
2365
  # Build plugin context
1983
2366
  #
1984
2367
  # @note module_function: defines #build_plugin_context (visibility: private)
1985
- # @param [Object] insertion the collected method insertion object
2368
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
1986
2369
  # @param [String] normal_type resolved return type
1987
- # @return [Object]
2370
+ # @return [Docscribe::Plugin::Context]
1988
2371
  def build_plugin_context(insertion, normal_type:)
1989
2372
  node = insertion.node
1990
2373
  source = safe_node_source(node)
@@ -1994,11 +2377,11 @@ module Docscribe
1994
2377
  # New plugin context
1995
2378
  #
1996
2379
  # @note module_function: defines #new_plugin_context (visibility: private)
1997
- # @param [Object] insertion the collected method insertion object
2380
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion the collected method insertion object
1998
2381
  # @param [Parser::AST::Node] node AST node whose source text to extract
1999
2382
  # @param [String] source method source text
2000
2383
  # @param [String] normal_type resolved return type
2001
- # @return [Object]
2384
+ # @return [Docscribe::Plugin::Context]
2002
2385
  def new_plugin_context(insertion, node, source, normal_type)
2003
2386
  Docscribe::Plugin::Context.new(
2004
2387
  node: node,
@@ -2028,7 +2411,7 @@ module Docscribe
2028
2411
  # Render plugin tags
2029
2412
  #
2030
2413
  # @note module_function: defines #render_plugin_tags (visibility: private)
2031
- # @param [Array<Object>] tags plugin tag objects
2414
+ # @param [Array<Docscribe::Plugin::Tag>] tags plugin tag objects
2032
2415
  # @param [String] indent indentation string for the doc line
2033
2416
  # @return [Array<String>]
2034
2417
  def render_plugin_tags(tags, indent)