docscribe 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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]
@@ -68,8 +75,10 @@ module Docscribe
68
75
  lines << "#{indent}#"
69
76
  end
70
77
 
71
- lines << "#{indent}# #{config.default_message(scope, visibility)}"
72
- lines << "#{indent}#"
78
+ if config.include_default_message?
79
+ lines << "#{indent}# #{config.default_message(scope, visibility)}"
80
+ lines << "#{indent}#"
81
+ end
73
82
 
74
83
  if config.emit_visibility_tags?
75
84
  case visibility
@@ -101,7 +110,10 @@ module Docscribe
101
110
  lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
102
111
  end
103
112
  end
104
-
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))
105
117
  lines.map { |l| "#{l}\n" }.join
106
118
  rescue StandardError => e
107
119
  debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build')
@@ -118,9 +130,12 @@ module Docscribe
118
130
  # @param [Array<String>] existing_lines
119
131
  # @param [Docscribe::Config] config
120
132
  # @param [Object, nil] signature_provider
133
+ # @param [nil] core_rbs_provider Param documentation.
134
+ # @param [nil] param_types Param documentation.
121
135
  # @raise [StandardError]
122
136
  # @return [String, nil]
123
- 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)
124
139
  node = insertion.node
125
140
  name = SourceHelpers.node_name(node)
126
141
  return '' unless name
@@ -139,7 +154,9 @@ module Docscribe
139
154
  returns_spec = Docscribe::Infer.returns_spec_from_node(
140
155
  node,
141
156
  fallback_type: config.fallback_type,
142
- 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
143
160
  )
144
161
 
145
162
  normal_type = external_sig&.return_type || returns_spec[:normal]
@@ -209,9 +226,12 @@ module Docscribe
209
226
  # @param [Array<String>] existing_lines
210
227
  # @param [Docscribe::Config] config
211
228
  # @param [Object, nil] signature_provider
229
+ # @param [nil] core_rbs_provider Param documentation.
230
+ # @param [nil] param_types Param documentation.
212
231
  # @raise [StandardError]
213
232
  # @return [Hash]
214
- def build_missing_merge_result(insertion, existing_lines:, config:, signature_provider: nil)
233
+ def build_missing_merge_result(insertion, existing_lines:, config:, signature_provider: nil,
234
+ core_rbs_provider: nil, param_types: nil)
215
235
  node = insertion.node
216
236
  name = SourceHelpers.node_name(node)
217
237
  return { lines: [], reasons: [] } unless name
@@ -230,7 +250,9 @@ module Docscribe
230
250
  returns_spec = Docscribe::Infer.returns_spec_from_node(
231
251
  node,
232
252
  fallback_type: config.fallback_type,
233
- nil_as_optional: config.nil_as_optional?
253
+ nil_as_optional: config.nil_as_optional?,
254
+ param_types: param_types,
255
+ core_rbs_provider: core_rbs_provider
234
256
  )
235
257
 
236
258
  normal_type = external_sig&.return_type || returns_spec[:normal]
@@ -293,18 +315,30 @@ module Docscribe
293
315
  }
294
316
  end
295
317
  end
318
+ plugin_tags = Docscribe::Plugin.run_tag_plugins(
319
+ build_plugin_context(insertion, normal_type: normal_type)
320
+ )
321
+ plugin_tags.each do |tag|
322
+ next if info[:plugin_tags]&.[](tag.name)
296
323
 
324
+ rendered = render_plugin_tags([tag], indent).first
325
+ lines << "#{rendered}\n"
326
+ reasons << { type: :missing_plugin_tag, message: "missing @#{tag.name}" }
327
+ end
297
328
  { lines: lines, reasons: reasons }
298
329
  rescue StandardError => e
299
330
  debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_missing_merge_result')
300
331
  { lines: [], reasons: [] }
301
332
  end
302
333
 
303
- # Method documentation.
334
+ # Parse existing doc comment lines and extract known YARD tags.
335
+ #
336
+ # Extracts: `@param` names, `@return`, `@raise`, `@private`, `@protected`,
337
+ # `@module_function` notes, and `@option` lines.
304
338
  #
305
339
  # @note module_function: when included, also defines #parse_existing_doc_tags (instance visibility: private)
306
- # @param [Object] lines Param documentation.
307
- # @return [Hash]
340
+ # @param [Array<String>] lines existing doc comment lines
341
+ # @return [Hash] parsed tag info
308
342
  def parse_existing_doc_tags(lines)
309
343
  param_names = {}
310
344
  has_return = false
@@ -312,8 +346,12 @@ module Docscribe
312
346
  has_protected = false
313
347
  has_module_function_note = false
314
348
  raise_types = {}
349
+ plugin_tags = {}
315
350
 
316
351
  Array(lines).each do |line|
352
+ if (m = line.match(/^\s*#\s*@(\w+)\b/))
353
+ plugin_tags[m[1]] = true
354
+ end
317
355
  if (pname = extract_param_name_from_param_line(line))
318
356
  param_names[pname] = true
319
357
  end
@@ -332,17 +370,18 @@ module Docscribe
332
370
  raise_types: raise_types,
333
371
  has_private: has_private,
334
372
  has_protected: has_protected,
335
- has_module_function_note: has_module_function_note
373
+ has_module_function_note: has_module_function_note,
374
+ plugin_tags: plugin_tags
336
375
  }
337
376
  end
338
377
 
339
- # Method documentation.
378
+ # Extract exception names from a `@raise` doc line.
340
379
  #
341
380
  # @note module_function: when included, also defines #extract_raise_types_from_line (instance visibility: private)
342
- # @param [Object] line Param documentation.
381
+ # @param [String] line a `@raise` doc line
343
382
  # @raise [StandardError]
344
- # @return [Object]
345
- # @return [Array] if StandardError
383
+ # @return [String, nil] the exception name or nil
384
+ # @return [Array] if StandardError or line not matched
346
385
  def extract_raise_types_from_line(line)
347
386
  return [] unless line.match?(/^\s*#\s*@raise\b/)
348
387
 
@@ -357,15 +396,78 @@ module Docscribe
357
396
  []
358
397
  end
359
398
 
360
- # Method documentation.
399
+ # Parse exception names from a `@raise [ExceptionA, ExceptionB]` line.
361
400
  #
362
401
  # @note module_function: when included, also defines #parse_raise_bracket_list (instance visibility: private)
363
- # @param [Object] s Param documentation.
364
- # @return [Object]
402
+ # @param [String] s the `@raise` line text
403
+ # @return [Array<String>, nil] the exception names or nil
365
404
  def parse_raise_bracket_list(s)
366
405
  s.to_s.split(',').map(&:strip).reject(&:empty?)
367
406
  end
368
407
 
408
+ # Build a param name => type map from a method node.
409
+ #
410
+ # @note module_function: when included, also defines #build_param_types_from_node (instance visibility: private)
411
+ # @private
412
+ # @param [Parser::AST::Node] node def or defs node
413
+ # @param [Object, nil] external_sig external signature if available
414
+ # @param [Docscribe::Config] config
415
+ # @return [Hash{String => String}, nil]
416
+ def build_param_types_from_node(node, external_sig:, config:)
417
+ return nil unless node
418
+
419
+ args =
420
+ case node.type
421
+ when :def then node.children[1]
422
+ when :defs then node.children[2]
423
+ end
424
+
425
+ return nil unless args
426
+
427
+ param_types = {}
428
+
429
+ (args.children || []).each do |a|
430
+ case a.type
431
+ when :arg
432
+ pname = a.children.first.to_s
433
+ ty = external_sig&.param_types&.[](pname) ||
434
+ Infer.infer_param_type(
435
+ pname,
436
+ nil,
437
+ fallback_type: config.fallback_type,
438
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
439
+ )
440
+ param_types[pname] = ty
441
+
442
+ when :optarg
443
+ pname, default = *a
444
+ pname = pname.to_s
445
+ default_src = default&.loc&.expression&.source
446
+ ty = external_sig&.param_types&.[](pname) ||
447
+ Infer.infer_param_type(
448
+ pname,
449
+ default_src,
450
+ fallback_type: config.fallback_type,
451
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
452
+ )
453
+ param_types[pname] = ty
454
+
455
+ when :kwarg
456
+ pname = a.children.first.to_s
457
+ ty = external_sig&.param_types&.[](pname) ||
458
+ Infer.infer_param_type(
459
+ "#{pname}:",
460
+ nil,
461
+ fallback_type: config.fallback_type,
462
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
463
+ )
464
+ param_types[pname] = ty
465
+ end
466
+ end
467
+
468
+ param_types.empty? ? nil : param_types
469
+ end
470
+
369
471
  # Build generated `@param` / `@option` lines for a method node.
370
472
  #
371
473
  # External signatures take precedence over inferred parameter types.
@@ -380,7 +482,7 @@ module Docscribe
380
482
  fallback_type = config.fallback_type
381
483
  treat_options_keyword_as_hash = config.treat_options_keyword_as_hash?
382
484
  param_tag_style = config.param_tag_style
383
- param_documentation = config.param_documentation
485
+ param_documentation = config.include_param_documentation? ? config.param_documentation : ''
384
486
 
385
487
  args =
386
488
  case node.type
@@ -499,15 +601,15 @@ module Docscribe
499
601
  params.empty? ? nil : params
500
602
  end
501
603
 
502
- # Method documentation.
604
+ # Format a `@param` tag line using the configured param tag style.
503
605
  #
504
606
  # @note module_function: when included, also defines #format_param_tag (instance visibility: private)
505
- # @param [Object] indent Param documentation.
506
- # @param [Object] name Param documentation.
507
- # @param [Object] type Param documentation.
508
- # @param [Object] documentation Param documentation.
509
- # @param [Object] style Param documentation.
510
- # @return [Object]
607
+ # @param [String] indent leading whitespace
608
+ # @param [String] name parameter name
609
+ # @param [String] type parameter type
610
+ # @param [String] documentation optional documentation text
611
+ # @param [String, Symbol] style param tag style (`"name_type"` or `"type_name"`)
612
+ # @return [String]
511
613
  def format_param_tag(indent, name, type, documentation, style:)
512
614
  doc = documentation.to_s.strip
513
615
  type = type.to_s
@@ -522,22 +624,22 @@ module Docscribe
522
624
  doc.empty? ? line : "#{line} #{doc}"
523
625
  end
524
626
 
525
- # Method documentation.
627
+ # Extract keyword argument option pairs from a hash default value.
526
628
  #
527
629
  # @note module_function: when included, also defines #hash_option_pairs (instance visibility: private)
528
- # @param [Object] node Param documentation.
529
- # @return [Object]
630
+ # @param [Parser::AST::Node, nil] node a `:hash` node
631
+ # @return [Array<Parser::AST::Node>] the `:pair` children
530
632
  def hash_option_pairs(node)
531
633
  return [] unless node&.type == :hash
532
634
 
533
635
  node.children.select { |child| child.is_a?(Parser::AST::Node) && child.type == :pair }
534
636
  end
535
637
 
536
- # Method documentation.
638
+ # Get the symbol name from an option key node.
537
639
  #
538
640
  # @note module_function: when included, also defines #option_key_name (instance visibility: private)
539
- # @param [Object] key_node Param documentation.
540
- # @return [Object]
641
+ # @param [Parser::AST::Node] key_node a `:sym` node
642
+ # @return [String] the option key name
541
643
  def option_key_name(key_node)
542
644
  case key_node&.type
543
645
  when :sym, :str
@@ -547,20 +649,22 @@ module Docscribe
547
649
  end
548
650
  end
549
651
 
550
- # Method documentation.
652
+ # Get the raw source literal for a default value node.
551
653
  #
552
654
  # @note module_function: when included, also defines #node_default_literal (instance visibility: private)
553
- # @param [Object] node Param documentation.
554
- # @return [Object]
655
+ # @param [Parser::AST::Node, nil] node a default value node
656
+ # @return [String, nil] the source literal or nil
555
657
  def node_default_literal(node)
556
658
  node&.loc&.expression&.source
557
659
  end
558
660
 
559
- # Method documentation.
661
+ # Extract the parameter name from a `@param` doc line.
662
+ #
663
+ # Handles both `"@param [Type] name"` and `"@param name [Type]"` styles.
560
664
  #
561
665
  # @note module_function: when included, also defines #extract_param_name_from_param_line (instance visibility: private)
562
- # @param [Object] line Param documentation.
563
- # @return [nil]
666
+ # @param [String] line a `@param` doc line
667
+ # @return [String, nil] the parameter name or nil
564
668
  def extract_param_name_from_param_line(line)
565
669
  return Regexp.last_match(1) if line =~ /@param\b\s+\[[^\]]+\]\s+(\S+)/
566
670
  return Regexp.last_match(1) if line =~ /@param\b\s+(\S+)\s+\[[^\]]+\]/
@@ -568,14 +672,57 @@ module Docscribe
568
672
  nil
569
673
  end
570
674
 
571
- # Method documentation.
675
+ # Build a Plugin::Context from a collected insertion.
676
+ #
677
+ # @note module_function
678
+ # @note module_function: when included, also defines #build_plugin_context (instance visibility: private)
679
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
680
+ # @param [String] normal_type resolved return type
681
+ # @raise [StandardError]
682
+ # @return [Docscribe::Plugin::Context]
683
+ def build_plugin_context(insertion, normal_type:)
684
+ node = insertion.node
685
+ source = begin
686
+ node.loc.expression.source
687
+ rescue StandardError
688
+ ''
689
+ end
690
+
691
+ Docscribe::Plugin::Context.new(
692
+ node: node,
693
+ container: insertion.container,
694
+ scope: insertion.scope,
695
+ visibility: insertion.visibility,
696
+ method_name: SourceHelpers.node_name(node),
697
+ inferred_params: {},
698
+ inferred_return: normal_type,
699
+ source: source
700
+ )
701
+ end
702
+
703
+ # Render plugin tags as indented comment lines.
704
+ #
705
+ # @note module_function
706
+ # @note module_function: when included, also defines #render_plugin_tags (instance visibility: private)
707
+ # @param [Array<Docscribe::Plugin::Tag>] tags
708
+ # @param [String] indent
709
+ # @return [Array<String>]
710
+ def render_plugin_tags(tags, indent)
711
+ tags.map do |tag|
712
+ type_part = tag.types&.any? ? " [#{tag.types.join(', ')}]" : ''
713
+ text_part = tag.text ? " #{tag.text}" : ''
714
+ "#{indent}# @#{tag.name}#{type_part}#{text_part}"
715
+ end
716
+ end
717
+
718
+ # Print a debug warning for a failed doc build phase.
572
719
  #
573
720
  # @note module_function: when included, also defines #debug_warn (instance visibility: private)
574
- # @param [Object] e Param documentation.
575
- # @param [Object] insertion Param documentation.
576
- # @param [Object] name Param documentation.
577
- # @param [Object] phase Param documentation.
578
- # @return [Object]
721
+ # @param [StandardError] e the error that occurred
722
+ # @param [Collector::Insertion] insertion the method insertion being processed
723
+ # @param [String] name the method name
724
+ # @param [String] phase the processing phase
725
+ # @return [void]
579
726
  def debug_warn(e, insertion:, name:, phase:)
580
727
  return unless debug?
581
728
 
@@ -593,10 +740,10 @@ module Docscribe
593
740
  warn "Docscribe DEBUG: #{phase} failed at #{where}: #{e.class}: #{e.message}"
594
741
  end
595
742
 
596
- # Method documentation.
743
+ # Check whether debug mode is enabled.
597
744
  #
598
745
  # @note module_function: when included, also defines #debug? (instance visibility: private)
599
- # @return [Object]
746
+ # @return [Boolean]
600
747
  def debug?
601
748
  ENV['DOCSCRIBE_DEBUG'] == '1'
602
749
  end