docscribe 1.2.1 → 1.3.1

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.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'docscribe/plugin'
3
4
  require 'docscribe/infer'
4
5
  require 'docscribe/inline_rewriter/source_helpers'
5
6
 
@@ -27,9 +28,11 @@ module Docscribe
27
28
  # @param [Docscribe::Config] config
28
29
  # @param [Object, nil] signature_provider provider responding to
29
30
  # `signature_for(container:, scope:, name:)`
31
+ # @param [nil] core_rbs_provider Param documentation.
32
+ # @param [nil] param_types Param documentation.
30
33
  # @raise [StandardError]
31
34
  # @return [String, nil]
32
- def build(insertion, config:, signature_provider: nil)
35
+ def build(insertion, config:, signature_provider: nil, core_rbs_provider: nil, param_types: nil)
33
36
  node = insertion.node
34
37
  name = SourceHelpers.node_name(node)
35
38
  return nil unless name
@@ -46,16 +49,20 @@ module Docscribe
46
49
  name: name
47
50
  )
48
51
 
52
+ effective_param_types =
53
+ param_types || build_param_types_from_node(node, external_sig: external_sig, config: config)
54
+
49
55
  if config.emit_param_tags?
50
- params_lines = build_params_lines(node, indent, external_sig: external_sig,
51
- config: config)
56
+ params_lines = build_params_lines(node, indent, external_sig: external_sig, config: config)
52
57
  end
53
58
  raise_types = config.emit_raise_tags? ? Docscribe::Infer.infer_raises_from_node(node) : []
54
59
 
55
60
  returns_spec = Docscribe::Infer.returns_spec_from_node(
56
61
  node,
57
62
  fallback_type: config.fallback_type,
58
- nil_as_optional: config.nil_as_optional?
63
+ nil_as_optional: config.nil_as_optional?,
64
+ param_types: effective_param_types,
65
+ core_rbs_provider: core_rbs_provider
59
66
  )
60
67
 
61
68
  normal_type = external_sig&.return_type || returns_spec[:normal]
@@ -103,7 +110,10 @@ module Docscribe
103
110
  lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
104
111
  end
105
112
  end
106
-
113
+ plugin_tags = Docscribe::Plugin.run_tag_plugins(
114
+ build_plugin_context(insertion, normal_type: normal_type)
115
+ )
116
+ lines.concat(render_plugin_tags(plugin_tags, indent))
107
117
  lines.map { |l| "#{l}\n" }.join
108
118
  rescue StandardError => e
109
119
  debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build')
@@ -120,9 +130,12 @@ module Docscribe
120
130
  # @param [Array<String>] existing_lines
121
131
  # @param [Docscribe::Config] config
122
132
  # @param [Object, nil] signature_provider
133
+ # @param [nil] core_rbs_provider Param documentation.
134
+ # @param [nil] param_types Param documentation.
123
135
  # @raise [StandardError]
124
136
  # @return [String, nil]
125
- def build_merge_additions(insertion, existing_lines:, config:, signature_provider: nil)
137
+ def build_merge_additions(insertion, existing_lines:, config:, signature_provider: nil, core_rbs_provider: nil,
138
+ param_types: nil)
126
139
  node = insertion.node
127
140
  name = SourceHelpers.node_name(node)
128
141
  return '' unless name
@@ -141,7 +154,9 @@ module Docscribe
141
154
  returns_spec = Docscribe::Infer.returns_spec_from_node(
142
155
  node,
143
156
  fallback_type: config.fallback_type,
144
- nil_as_optional: config.nil_as_optional?
157
+ nil_as_optional: config.nil_as_optional?,
158
+ param_types: param_types,
159
+ core_rbs_provider: core_rbs_provider
145
160
  )
146
161
 
147
162
  normal_type = external_sig&.return_type || returns_spec[:normal]
@@ -211,9 +226,13 @@ module Docscribe
211
226
  # @param [Array<String>] existing_lines
212
227
  # @param [Docscribe::Config] config
213
228
  # @param [Object, nil] signature_provider
229
+ # @param [nil] core_rbs_provider Param documentation.
230
+ # @param [nil] param_types Param documentation.
231
+ # @param [nil] strategy Param documentation.
214
232
  # @raise [StandardError]
215
233
  # @return [Hash]
216
- def build_missing_merge_result(insertion, existing_lines:, config:, signature_provider: nil)
234
+ def build_missing_merge_result(insertion, existing_lines:, config:, signature_provider: nil,
235
+ core_rbs_provider: nil, param_types: nil, strategy: nil)
217
236
  node = insertion.node
218
237
  name = SourceHelpers.node_name(node)
219
238
  return { lines: [], reasons: [] } unless name
@@ -232,7 +251,9 @@ module Docscribe
232
251
  returns_spec = Docscribe::Infer.returns_spec_from_node(
233
252
  node,
234
253
  fallback_type: config.fallback_type,
235
- nil_as_optional: config.nil_as_optional?
254
+ nil_as_optional: config.nil_as_optional?,
255
+ param_types: param_types,
256
+ core_rbs_provider: core_rbs_provider
236
257
  )
237
258
 
238
259
  normal_type = external_sig&.return_type || returns_spec[:normal]
@@ -263,10 +284,22 @@ module Docscribe
263
284
 
264
285
  all_params&.each do |pl|
265
286
  pname = extract_param_name_from_param_line(pl)
266
- next if pname.nil? || info[:param_names].include?(pname)
267
-
268
- lines << "#{pl}\n"
269
- reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
287
+ next unless pname
288
+
289
+ if !info[:param_names].include?(pname)
290
+ lines << "#{pl}\n"
291
+ reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
292
+ elsif info[:param_types][pname] && strategy != :safe
293
+ new_type = extract_param_type_from_param_line(pl)
294
+ if new_type && info[:param_types][pname] != new_type
295
+ lines << "#{pl}\n"
296
+ reasons << {
297
+ type: :updated_param,
298
+ message: "updated @param #{pname} from #{info[:param_types][pname]} to #{new_type}",
299
+ extra: { param: pname }
300
+ }
301
+ end
302
+ end
270
303
  end
271
304
  end
272
305
 
@@ -281,9 +314,17 @@ module Docscribe
281
314
  end
282
315
  end
283
316
 
284
- if config.emit_return_tag?(scope, visibility) && !info[:has_return]
285
- lines << "#{indent}# @return [#{normal_type}]\n"
286
- reasons << { type: :missing_return, message: 'missing @return' }
317
+ if config.emit_return_tag?(scope, visibility)
318
+ if !info[:has_return]
319
+ lines << "#{indent}# @return [#{normal_type}]\n"
320
+ reasons << { type: :missing_return, message: 'missing @return' }
321
+ elsif info[:return_type] && info[:return_type] != normal_type && strategy != :safe
322
+ lines << "#{indent}# @return [#{normal_type}]\n"
323
+ reasons << {
324
+ type: :updated_return,
325
+ message: "updated @return from #{info[:return_type]} to #{normal_type}"
326
+ }
327
+ end
287
328
  end
288
329
 
289
330
  if config.emit_rescue_conditional_returns? && !info[:has_return]
@@ -295,32 +336,58 @@ module Docscribe
295
336
  }
296
337
  end
297
338
  end
339
+ plugin_tags = Docscribe::Plugin.run_tag_plugins(
340
+ build_plugin_context(insertion, normal_type: normal_type)
341
+ )
342
+ plugin_tags.each do |tag|
343
+ next if info[:plugin_tags]&.[](tag.name)
298
344
 
345
+ rendered = render_plugin_tags([tag], indent).first
346
+ lines << "#{rendered}\n"
347
+ reasons << { type: :missing_plugin_tag, message: "missing @#{tag.name}" }
348
+ end
299
349
  { lines: lines, reasons: reasons }
300
350
  rescue StandardError => e
301
351
  debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_missing_merge_result')
302
352
  { lines: [], reasons: [] }
303
353
  end
304
354
 
305
- # Method documentation.
355
+ # Parse existing doc comment lines and extract known YARD tags.
356
+ #
357
+ # Extracts: `@param` names, `@return`, `@raise`, `@private`, `@protected`,
358
+ # `@module_function` notes, and `@option` lines.
306
359
  #
307
360
  # @note module_function: when included, also defines #parse_existing_doc_tags (instance visibility: private)
308
- # @param [Object] lines Param documentation.
309
- # @return [Hash]
361
+ # @param [Array<String>] lines existing doc comment lines
362
+ # @return [Hash] parsed tag info
310
363
  def parse_existing_doc_tags(lines)
311
364
  param_names = {}
365
+ param_types = {}
312
366
  has_return = false
367
+ return_type = nil
313
368
  has_private = false
314
369
  has_protected = false
315
370
  has_module_function_note = false
316
371
  raise_types = {}
372
+ plugin_tags = {}
317
373
 
318
374
  Array(lines).each do |line|
375
+ if (m = line.match(/^\s*#\s*@(\w+)\b/))
376
+ plugin_tags[m[1]] = true
377
+ end
319
378
  if (pname = extract_param_name_from_param_line(line))
320
379
  param_names[pname] = true
380
+ if (type_match = line.match(/@param\s+\[([^\]]+)\]\s+\S+/) || line.match(/@param\s+\S+\s+\[([^\]]+)\]/))
381
+ param_types[pname] = type_match[1]
382
+ end
321
383
  end
322
384
 
323
- has_return ||= line.match?(/^\s*#\s*@return\b/)
385
+ if line.match?(/^\s*#\s*@return\b/)
386
+ has_return = true
387
+ if (m = line.match(/@return\s+\[([^\]]+)\]/))
388
+ return_type = m[1]
389
+ end
390
+ end
324
391
  has_private ||= line.match?(/^\s*#\s*@private\b/)
325
392
  has_protected ||= line.match?(/^\s*#\s*@protected\b/)
326
393
  has_module_function_note ||= line.match?(/^\s*#\s*@note\s+module_function:/)
@@ -330,21 +397,24 @@ module Docscribe
330
397
 
331
398
  {
332
399
  param_names: param_names,
400
+ param_types: param_types,
333
401
  has_return: has_return,
402
+ return_type: return_type,
334
403
  raise_types: raise_types,
335
404
  has_private: has_private,
336
405
  has_protected: has_protected,
337
- has_module_function_note: has_module_function_note
406
+ has_module_function_note: has_module_function_note,
407
+ plugin_tags: plugin_tags
338
408
  }
339
409
  end
340
410
 
341
- # Method documentation.
411
+ # Extract exception names from a `@raise` doc line.
342
412
  #
343
413
  # @note module_function: when included, also defines #extract_raise_types_from_line (instance visibility: private)
344
- # @param [Object] line Param documentation.
414
+ # @param [String] line a `@raise` doc line
345
415
  # @raise [StandardError]
346
- # @return [Object]
347
- # @return [Array] if StandardError
416
+ # @return [String, nil] the exception name or nil
417
+ # @return [Array] if StandardError or line not matched
348
418
  def extract_raise_types_from_line(line)
349
419
  return [] unless line.match?(/^\s*#\s*@raise\b/)
350
420
 
@@ -359,15 +429,91 @@ module Docscribe
359
429
  []
360
430
  end
361
431
 
362
- # Method documentation.
432
+ # Parse exception names from a `@raise [ExceptionA, ExceptionB]` line.
363
433
  #
364
434
  # @note module_function: when included, also defines #parse_raise_bracket_list (instance visibility: private)
365
- # @param [Object] s Param documentation.
366
- # @return [Object]
435
+ # @param [String] s the `@raise` line text
436
+ # @return [Array<String>, nil] the exception names or nil
367
437
  def parse_raise_bracket_list(s)
368
438
  s.to_s.split(',').map(&:strip).reject(&:empty?)
369
439
  end
370
440
 
441
+ # Build a param name => type map from a method node.
442
+ #
443
+ # @note module_function: when included, also defines #build_param_types_from_node (instance visibility: private)
444
+ # @private
445
+ # @param [Parser::AST::Node] node def or defs node
446
+ # @param [Object, nil] external_sig external signature if available
447
+ # @param [Docscribe::Config] config
448
+ # @return [Hash{String => String}, nil]
449
+ def build_param_types_from_node(node, external_sig:, config:)
450
+ return nil unless node
451
+
452
+ args =
453
+ case node.type
454
+ when :def then node.children[1]
455
+ when :defs then node.children[2]
456
+ end
457
+
458
+ return nil unless args
459
+
460
+ param_types = {}
461
+
462
+ (args.children || []).each do |a|
463
+ case a.type
464
+ when :arg
465
+ pname = a.children.first.to_s
466
+ ty = external_sig&.param_types&.[](pname) ||
467
+ Infer.infer_param_type(
468
+ pname,
469
+ nil,
470
+ fallback_type: config.fallback_type,
471
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
472
+ )
473
+ param_types[pname] = ty
474
+
475
+ when :optarg
476
+ pname, default = *a
477
+ pname = pname.to_s
478
+ default_src = default&.loc&.expression&.source
479
+ ty = external_sig&.param_types&.[](pname) ||
480
+ Infer.infer_param_type(
481
+ pname,
482
+ default_src,
483
+ fallback_type: config.fallback_type,
484
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
485
+ )
486
+ param_types[pname] = ty
487
+
488
+ when :kwarg
489
+ pname = a.children.first.to_s
490
+ ty = external_sig&.param_types&.[](pname) ||
491
+ Infer.infer_param_type(
492
+ "#{pname}:",
493
+ nil,
494
+ fallback_type: config.fallback_type,
495
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
496
+ )
497
+ param_types[pname] = ty
498
+
499
+ when :kwoptarg
500
+ pname, default = *a
501
+ pname = pname.to_s
502
+ default_src = default&.loc&.expression&.source
503
+ ty = external_sig&.param_types&.[](pname) ||
504
+ Infer.infer_param_type(
505
+ "#{pname}:",
506
+ default_src,
507
+ fallback_type: config.fallback_type,
508
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
509
+ )
510
+ param_types[pname] = ty
511
+ end
512
+ end
513
+
514
+ param_types.empty? ? nil : param_types
515
+ end
516
+
371
517
  # Build generated `@param` / `@option` lines for a method node.
372
518
  #
373
519
  # External signatures take precedence over inferred parameter types.
@@ -501,15 +647,15 @@ module Docscribe
501
647
  params.empty? ? nil : params
502
648
  end
503
649
 
504
- # Method documentation.
650
+ # Format a `@param` tag line using the configured param tag style.
505
651
  #
506
652
  # @note module_function: when included, also defines #format_param_tag (instance visibility: private)
507
- # @param [Object] indent Param documentation.
508
- # @param [Object] name Param documentation.
509
- # @param [Object] type Param documentation.
510
- # @param [Object] documentation Param documentation.
511
- # @param [Object] style Param documentation.
512
- # @return [Object]
653
+ # @param [String] indent leading whitespace
654
+ # @param [String] name parameter name
655
+ # @param [String] type parameter type
656
+ # @param [String] documentation optional documentation text
657
+ # @param [String, Symbol] style param tag style (`"name_type"` or `"type_name"`)
658
+ # @return [String]
513
659
  def format_param_tag(indent, name, type, documentation, style:)
514
660
  doc = documentation.to_s.strip
515
661
  type = type.to_s
@@ -524,22 +670,22 @@ module Docscribe
524
670
  doc.empty? ? line : "#{line} #{doc}"
525
671
  end
526
672
 
527
- # Method documentation.
673
+ # Extract keyword argument option pairs from a hash default value.
528
674
  #
529
675
  # @note module_function: when included, also defines #hash_option_pairs (instance visibility: private)
530
- # @param [Object] node Param documentation.
531
- # @return [Object]
676
+ # @param [Parser::AST::Node, nil] node a `:hash` node
677
+ # @return [Array<Parser::AST::Node>] the `:pair` children
532
678
  def hash_option_pairs(node)
533
679
  return [] unless node&.type == :hash
534
680
 
535
681
  node.children.select { |child| child.is_a?(Parser::AST::Node) && child.type == :pair }
536
682
  end
537
683
 
538
- # Method documentation.
684
+ # Get the symbol name from an option key node.
539
685
  #
540
686
  # @note module_function: when included, also defines #option_key_name (instance visibility: private)
541
- # @param [Object] key_node Param documentation.
542
- # @return [Object]
687
+ # @param [Parser::AST::Node] key_node a `:sym` node
688
+ # @return [String] the option key name
543
689
  def option_key_name(key_node)
544
690
  case key_node&.type
545
691
  when :sym, :str
@@ -549,20 +695,22 @@ module Docscribe
549
695
  end
550
696
  end
551
697
 
552
- # Method documentation.
698
+ # Get the raw source literal for a default value node.
553
699
  #
554
700
  # @note module_function: when included, also defines #node_default_literal (instance visibility: private)
555
- # @param [Object] node Param documentation.
556
- # @return [Object]
701
+ # @param [Parser::AST::Node, nil] node a default value node
702
+ # @return [String, nil] the source literal or nil
557
703
  def node_default_literal(node)
558
704
  node&.loc&.expression&.source
559
705
  end
560
706
 
561
- # Method documentation.
707
+ # Extract the parameter name from a `@param` doc line.
708
+ #
709
+ # Handles both `"@param [Type] name"` and `"@param name [Type]"` styles.
562
710
  #
563
711
  # @note module_function: when included, also defines #extract_param_name_from_param_line (instance visibility: private)
564
- # @param [Object] line Param documentation.
565
- # @return [nil]
712
+ # @param [String] line a `@param` doc line
713
+ # @return [String, nil] the parameter name or nil
566
714
  def extract_param_name_from_param_line(line)
567
715
  return Regexp.last_match(1) if line =~ /@param\b\s+\[[^\]]+\]\s+(\S+)/
568
716
  return Regexp.last_match(1) if line =~ /@param\b\s+(\S+)\s+\[[^\]]+\]/
@@ -572,12 +720,66 @@ module Docscribe
572
720
 
573
721
  # Method documentation.
574
722
  #
575
- # @note module_function: when included, also defines #debug_warn (instance visibility: private)
576
- # @param [Object] e Param documentation.
577
- # @param [Object] insertion Param documentation.
578
- # @param [Object] name Param documentation.
579
- # @param [Object] phase Param documentation.
723
+ # @note module_function: when included, also defines #extract_param_type_from_param_line (instance visibility: private)
724
+ # @param [Object] line Param documentation.
580
725
  # @return [Object]
726
+ def extract_param_type_from_param_line(line)
727
+ if (m = line.match(/@param\s+\[([^\]]+)\]\s+\S+/) || line.match(/@param\s+\S+\s+\[([^\]]+)\]/))
728
+ m[1]
729
+ end
730
+ end
731
+
732
+ # Build a Plugin::Context from a collected insertion.
733
+ #
734
+ # @note module_function
735
+ # @note module_function: when included, also defines #build_plugin_context (instance visibility: private)
736
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
737
+ # @param [String] normal_type resolved return type
738
+ # @raise [StandardError]
739
+ # @return [Docscribe::Plugin::Context]
740
+ def build_plugin_context(insertion, normal_type:)
741
+ node = insertion.node
742
+ source = begin
743
+ node.loc.expression.source
744
+ rescue StandardError
745
+ ''
746
+ end
747
+
748
+ Docscribe::Plugin::Context.new(
749
+ node: node,
750
+ container: insertion.container,
751
+ scope: insertion.scope,
752
+ visibility: insertion.visibility,
753
+ method_name: SourceHelpers.node_name(node),
754
+ inferred_params: {},
755
+ inferred_return: normal_type,
756
+ source: source
757
+ )
758
+ end
759
+
760
+ # Render plugin tags as indented comment lines.
761
+ #
762
+ # @note module_function
763
+ # @note module_function: when included, also defines #render_plugin_tags (instance visibility: private)
764
+ # @param [Array<Docscribe::Plugin::Tag>] tags
765
+ # @param [String] indent
766
+ # @return [Array<String>]
767
+ def render_plugin_tags(tags, indent)
768
+ tags.map do |tag|
769
+ type_part = tag.types&.any? ? " [#{tag.types.join(', ')}]" : ''
770
+ text_part = tag.text ? " #{tag.text}" : ''
771
+ "#{indent}# @#{tag.name}#{type_part}#{text_part}"
772
+ end
773
+ end
774
+
775
+ # Print a debug warning for a failed doc build phase.
776
+ #
777
+ # @note module_function: when included, also defines #debug_warn (instance visibility: private)
778
+ # @param [StandardError] e the error that occurred
779
+ # @param [Collector::Insertion] insertion the method insertion being processed
780
+ # @param [String] name the method name
781
+ # @param [String] phase the processing phase
782
+ # @return [void]
581
783
  def debug_warn(e, insertion:, name:, phase:)
582
784
  return unless debug?
583
785
 
@@ -595,10 +797,10 @@ module Docscribe
595
797
  warn "Docscribe DEBUG: #{phase} failed at #{where}: #{e.class}: #{e.message}"
596
798
  end
597
799
 
598
- # Method documentation.
800
+ # Check whether debug mode is enabled.
599
801
  #
600
802
  # @note module_function: when included, also defines #debug? (instance visibility: private)
601
- # @return [Object]
803
+ # @return [Boolean]
602
804
  def debug?
603
805
  ENV['DOCSCRIBE_DEBUG'] == '1'
604
806
  end