docscribe 1.2.1 → 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]
@@ -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,12 @@ 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.
214
231
  # @raise [StandardError]
215
232
  # @return [Hash]
216
- 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)
217
235
  node = insertion.node
218
236
  name = SourceHelpers.node_name(node)
219
237
  return { lines: [], reasons: [] } unless name
@@ -232,7 +250,9 @@ module Docscribe
232
250
  returns_spec = Docscribe::Infer.returns_spec_from_node(
233
251
  node,
234
252
  fallback_type: config.fallback_type,
235
- 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
236
256
  )
237
257
 
238
258
  normal_type = external_sig&.return_type || returns_spec[:normal]
@@ -295,18 +315,30 @@ module Docscribe
295
315
  }
296
316
  end
297
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)
298
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
299
328
  { lines: lines, reasons: reasons }
300
329
  rescue StandardError => e
301
330
  debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_missing_merge_result')
302
331
  { lines: [], reasons: [] }
303
332
  end
304
333
 
305
- # 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.
306
338
  #
307
339
  # @note module_function: when included, also defines #parse_existing_doc_tags (instance visibility: private)
308
- # @param [Object] lines Param documentation.
309
- # @return [Hash]
340
+ # @param [Array<String>] lines existing doc comment lines
341
+ # @return [Hash] parsed tag info
310
342
  def parse_existing_doc_tags(lines)
311
343
  param_names = {}
312
344
  has_return = false
@@ -314,8 +346,12 @@ module Docscribe
314
346
  has_protected = false
315
347
  has_module_function_note = false
316
348
  raise_types = {}
349
+ plugin_tags = {}
317
350
 
318
351
  Array(lines).each do |line|
352
+ if (m = line.match(/^\s*#\s*@(\w+)\b/))
353
+ plugin_tags[m[1]] = true
354
+ end
319
355
  if (pname = extract_param_name_from_param_line(line))
320
356
  param_names[pname] = true
321
357
  end
@@ -334,17 +370,18 @@ module Docscribe
334
370
  raise_types: raise_types,
335
371
  has_private: has_private,
336
372
  has_protected: has_protected,
337
- has_module_function_note: has_module_function_note
373
+ has_module_function_note: has_module_function_note,
374
+ plugin_tags: plugin_tags
338
375
  }
339
376
  end
340
377
 
341
- # Method documentation.
378
+ # Extract exception names from a `@raise` doc line.
342
379
  #
343
380
  # @note module_function: when included, also defines #extract_raise_types_from_line (instance visibility: private)
344
- # @param [Object] line Param documentation.
381
+ # @param [String] line a `@raise` doc line
345
382
  # @raise [StandardError]
346
- # @return [Object]
347
- # @return [Array] if StandardError
383
+ # @return [String, nil] the exception name or nil
384
+ # @return [Array] if StandardError or line not matched
348
385
  def extract_raise_types_from_line(line)
349
386
  return [] unless line.match?(/^\s*#\s*@raise\b/)
350
387
 
@@ -359,15 +396,78 @@ module Docscribe
359
396
  []
360
397
  end
361
398
 
362
- # Method documentation.
399
+ # Parse exception names from a `@raise [ExceptionA, ExceptionB]` line.
363
400
  #
364
401
  # @note module_function: when included, also defines #parse_raise_bracket_list (instance visibility: private)
365
- # @param [Object] s Param documentation.
366
- # @return [Object]
402
+ # @param [String] s the `@raise` line text
403
+ # @return [Array<String>, nil] the exception names or nil
367
404
  def parse_raise_bracket_list(s)
368
405
  s.to_s.split(',').map(&:strip).reject(&:empty?)
369
406
  end
370
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
+
371
471
  # Build generated `@param` / `@option` lines for a method node.
372
472
  #
373
473
  # External signatures take precedence over inferred parameter types.
@@ -501,15 +601,15 @@ module Docscribe
501
601
  params.empty? ? nil : params
502
602
  end
503
603
 
504
- # Method documentation.
604
+ # Format a `@param` tag line using the configured param tag style.
505
605
  #
506
606
  # @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]
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]
513
613
  def format_param_tag(indent, name, type, documentation, style:)
514
614
  doc = documentation.to_s.strip
515
615
  type = type.to_s
@@ -524,22 +624,22 @@ module Docscribe
524
624
  doc.empty? ? line : "#{line} #{doc}"
525
625
  end
526
626
 
527
- # Method documentation.
627
+ # Extract keyword argument option pairs from a hash default value.
528
628
  #
529
629
  # @note module_function: when included, also defines #hash_option_pairs (instance visibility: private)
530
- # @param [Object] node Param documentation.
531
- # @return [Object]
630
+ # @param [Parser::AST::Node, nil] node a `:hash` node
631
+ # @return [Array<Parser::AST::Node>] the `:pair` children
532
632
  def hash_option_pairs(node)
533
633
  return [] unless node&.type == :hash
534
634
 
535
635
  node.children.select { |child| child.is_a?(Parser::AST::Node) && child.type == :pair }
536
636
  end
537
637
 
538
- # Method documentation.
638
+ # Get the symbol name from an option key node.
539
639
  #
540
640
  # @note module_function: when included, also defines #option_key_name (instance visibility: private)
541
- # @param [Object] key_node Param documentation.
542
- # @return [Object]
641
+ # @param [Parser::AST::Node] key_node a `:sym` node
642
+ # @return [String] the option key name
543
643
  def option_key_name(key_node)
544
644
  case key_node&.type
545
645
  when :sym, :str
@@ -549,20 +649,22 @@ module Docscribe
549
649
  end
550
650
  end
551
651
 
552
- # Method documentation.
652
+ # Get the raw source literal for a default value node.
553
653
  #
554
654
  # @note module_function: when included, also defines #node_default_literal (instance visibility: private)
555
- # @param [Object] node Param documentation.
556
- # @return [Object]
655
+ # @param [Parser::AST::Node, nil] node a default value node
656
+ # @return [String, nil] the source literal or nil
557
657
  def node_default_literal(node)
558
658
  node&.loc&.expression&.source
559
659
  end
560
660
 
561
- # Method documentation.
661
+ # Extract the parameter name from a `@param` doc line.
662
+ #
663
+ # Handles both `"@param [Type] name"` and `"@param name [Type]"` styles.
562
664
  #
563
665
  # @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]
666
+ # @param [String] line a `@param` doc line
667
+ # @return [String, nil] the parameter name or nil
566
668
  def extract_param_name_from_param_line(line)
567
669
  return Regexp.last_match(1) if line =~ /@param\b\s+\[[^\]]+\]\s+(\S+)/
568
670
  return Regexp.last_match(1) if line =~ /@param\b\s+(\S+)\s+\[[^\]]+\]/
@@ -570,14 +672,57 @@ module Docscribe
570
672
  nil
571
673
  end
572
674
 
573
- # 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.
574
719
  #
575
720
  # @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.
580
- # @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]
581
726
  def debug_warn(e, insertion:, name:, phase:)
582
727
  return unless debug?
583
728
 
@@ -595,10 +740,10 @@ module Docscribe
595
740
  warn "Docscribe DEBUG: #{phase} failed at #{where}: #{e.class}: #{e.message}"
596
741
  end
597
742
 
598
- # Method documentation.
743
+ # Check whether debug mode is enabled.
599
744
  #
600
745
  # @note module_function: when included, also defines #debug? (instance visibility: private)
601
- # @return [Object]
746
+ # @return [Boolean]
602
747
  def debug?
603
748
  ENV['DOCSCRIBE_DEBUG'] == '1'
604
749
  end