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.
@@ -66,11 +66,14 @@ module Docscribe
66
66
  # @param [Symbol, nil] strategy :safe or :aggressive
67
67
  # @param [Boolean, nil] rewrite compatibility alias for aggressive strategy
68
68
  # @param [Boolean, nil] merge compatibility alias for safe strategy
69
- # @param [Docscribe::Config, nil] config config object (defaults to loaded config)
69
+ # @param [Docscribe::Config] config config object (defaults to loaded config)
70
70
  # @param [String] file source name used for parser locations/debugging
71
+ # @param [nil] core_rbs_provider Param documentation.
71
72
  # @raise [Docscribe::ParseError]
73
+ # @raise [StandardError]
72
74
  # @return [Hash]
73
- def rewrite_with_report(code, strategy: nil, rewrite: nil, merge: nil, config: nil, file: '(inline)')
75
+ def rewrite_with_report(code, strategy: nil, rewrite: nil, merge: nil, config: Docscribe::Config.new({}),
76
+ core_rbs_provider: nil, file: '(inline)')
74
77
  strategy = normalize_strategy(strategy: strategy, rewrite: rewrite, merge: merge)
75
78
  validate_strategy!(strategy)
76
79
 
@@ -80,20 +83,30 @@ module Docscribe
80
83
 
81
84
  config ||= Docscribe::Config.load
82
85
  signature_provider = build_signature_provider(config, code, file.to_s)
86
+ begin
87
+ core_rbs_provider ||= config.core_rbs_provider if config.respond_to?(:core_rbs_provider)
88
+ rescue StandardError => e
89
+ warn "Docscribe: failed to load core RBS provider: #{e.message}" if ENV['DOCSCRIBE_DEBUG']
90
+ core_rbs_provider = nil
91
+ end
83
92
 
84
93
  collector = Docscribe::InlineRewriter::Collector.new(buffer)
85
94
  collector.process(ast)
86
95
 
96
+ # Collect additional insertions from CollectorPlugins
97
+ plugin_insertions = Docscribe::Plugin.run_collector_plugins(ast, buffer)
98
+
87
99
  method_insertions = collector.insertions
88
100
  attr_insertions = collector.respond_to?(:attr_insertions) ? collector.attr_insertions : []
89
101
 
90
- all = method_insertions.map { |i| [:method, i] } + attr_insertions.map { |i| [:attr, i] }
91
-
102
+ all = method_insertions.map { |i| [:method, i] } +
103
+ attr_insertions.map { |i| [:attr, i] } +
104
+ plugin_insertions.map { |i| [:plugin, i] }
92
105
  rewriter = Parser::Source::TreeRewriter.new(buffer)
93
106
  merge_inserts = Hash.new { |h, k| h[k] = [] }
94
107
  changes = []
95
108
 
96
- all.sort_by { |(_kind, ins)| ins.node.loc.expression.begin_pos }
109
+ all.sort_by { |(kind, ins)| plugin_insertion_pos(kind, ins) }
97
110
  .reverse_each do |kind, ins|
98
111
  case kind
99
112
  when :method
@@ -103,6 +116,7 @@ module Docscribe
103
116
  insertion: ins,
104
117
  config: config,
105
118
  signature_provider: signature_provider,
119
+ core_rbs_provider: core_rbs_provider,
106
120
  strategy: strategy,
107
121
  changes: changes,
108
122
  file: file.to_s
@@ -117,6 +131,13 @@ module Docscribe
117
131
  strategy: strategy,
118
132
  merge_inserts: merge_inserts
119
133
  )
134
+ when :plugin
135
+ apply_plugin_insertion!(
136
+ rewriter: rewriter,
137
+ buffer: buffer,
138
+ insertion: ins,
139
+ strategy: strategy
140
+ )
120
141
  end
121
142
  end
122
143
 
@@ -127,6 +148,114 @@ module Docscribe
127
148
 
128
149
  private
129
150
 
151
+ # Resolve the source begin_pos for sorting, handling both Struct-based
152
+ # insertions (method/attr) and Hash-based insertions (plugin).
153
+ #
154
+ # @private
155
+ # @param [Symbol] kind :method, :attr, or :plugin
156
+ # @param [Object] ins insertion object or hash
157
+ # @return [Integer]
158
+ def plugin_insertion_pos(kind, ins)
159
+ case kind
160
+ when :plugin
161
+ ins[:anchor_node].loc.expression.begin_pos
162
+ else
163
+ ins.node.loc.expression.begin_pos
164
+ end
165
+ end
166
+
167
+ # Apply one CollectorPlugin insertion according to the selected strategy.
168
+ #
169
+ # :safe — skip if a doc-like block already exists above anchor_node
170
+ # :aggressive — remove existing doc block, insert fresh
171
+ #
172
+ # @private
173
+ # @param [Parser::Source::TreeRewriter] rewriter
174
+ # @param [Parser::Source::Buffer] buffer
175
+ # @param [Hash] insertion { anchor_node:, doc: }
176
+ # @param [Symbol] strategy
177
+ # @return [void]
178
+ def apply_plugin_insertion!(rewriter:, buffer:, insertion:, strategy:)
179
+ anchor_node = insertion[:anchor_node]
180
+ doc = insertion[:doc]
181
+ return unless anchor_node && doc && !doc.empty?
182
+
183
+ indent = SourceHelpers.line_indent(anchor_node)
184
+ doc = normalize_plugin_doc_indent(doc, indent)
185
+ bol_range = SourceHelpers.line_start_range(buffer, anchor_node)
186
+
187
+ case strategy
188
+ when :aggressive
189
+ # Will remove ANY comments above the method. Plugin will decide what will be changed.
190
+ if (range = any_comment_block_removal_range(buffer, bol_range.begin_pos))
191
+ rewriter.remove(range)
192
+ end
193
+ rewriter.insert_before(bol_range, doc)
194
+
195
+ when :safe
196
+ return if SourceHelpers.already_has_doc_immediately_above?(buffer, bol_range.begin_pos)
197
+
198
+ rewriter.insert_before(bol_range, doc)
199
+ end
200
+ end
201
+
202
+ # Remove any contiguous comment block immediately above anchor_node,
203
+ # regardless of whether it looks like documentation.
204
+ #
205
+ # Used by CollectorPlugin in aggressive mode where the plugin itself
206
+ # is responsible for deciding what to replace.
207
+ #
208
+ # @private
209
+ # @param [Parser::Source::Buffer] buffer
210
+ # @param [Integer] bol_pos beginning-of-line position of anchor_node
211
+ # @return [Parser::Source::Range, nil]
212
+ def any_comment_block_removal_range(buffer, bol_pos)
213
+ src = buffer.source
214
+ lines = src.lines
215
+ def_line_idx = src[0...bol_pos].count("\n")
216
+ i = def_line_idx - 1
217
+
218
+ # Skip blank lines directly above node
219
+ i -= 1 while i >= 0 && lines[i].strip.empty?
220
+
221
+ # Nearest non-blank line must be a comment
222
+ return nil unless i >= 0 && lines[i] =~ /^\s*#/
223
+
224
+ # Walk upward through the entire contiguous comment block
225
+ start_idx = i
226
+ start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
227
+ start_idx += 1
228
+
229
+ # Preserve leading directive-style lines (rubocop, magic comments, etc.)
230
+ removable_start_idx = start_idx
231
+ while removable_start_idx <= i &&
232
+ SourceHelpers.preserved_comment_line?(lines[removable_start_idx])
233
+ removable_start_idx += 1
234
+ end
235
+
236
+ return nil if removable_start_idx > i
237
+
238
+ start_pos = removable_start_idx.positive? ? lines[0...removable_start_idx].join.length : 0
239
+ Parser::Source::Range.new(buffer, start_pos, bol_pos)
240
+ end
241
+
242
+ # Normalise indentation of a plugin-generated doc block.
243
+ #
244
+ # Plugins produce doc strings without knowledge of the surrounding
245
+ # indentation. We strip leading whitespace from each non-empty line
246
+ # and re-prefix it with the indent derived from anchor_node.
247
+ #
248
+ # @private
249
+ # @param [String] doc raw doc string from plugin
250
+ # @param [String] indent indentation prefix to apply
251
+ # @return [String]
252
+ def normalize_plugin_doc_indent(doc, indent)
253
+ doc.lines.map do |line|
254
+ stripped = line.lstrip
255
+ stripped.match?(/\A\r?\n?\z/) ? line : "#{indent}#{stripped}"
256
+ end.join
257
+ end
258
+
130
259
  # Normalize strategy inputs, including compatibility booleans.
131
260
  #
132
261
  # Precedence:
@@ -179,9 +308,10 @@ module Docscribe
179
308
  # @param [Symbol] strategy
180
309
  # @param [Array<Hash>] changes structured change records
181
310
  # @param [String] file
311
+ # @param [Object] core_rbs_provider Param documentation.
182
312
  # @return [void]
183
- def apply_method_insertion!(rewriter:, buffer:, insertion:, config:, signature_provider:, strategy:, changes:,
184
- file:)
313
+ def apply_method_insertion!(rewriter:, buffer:, insertion:, config:, signature_provider:, core_rbs_provider:,
314
+ strategy:, changes:, file:)
185
315
  name = SourceHelpers.node_name(insertion.node)
186
316
 
187
317
  return unless config.process_method?(
@@ -193,13 +323,27 @@ module Docscribe
193
323
 
194
324
  anchor_bol_range, = method_bol_ranges(buffer, insertion)
195
325
 
326
+ # Create external_sig for param_types lookup
327
+ external_sig = signature_provider&.signature_for(
328
+ container: insertion.container,
329
+ scope: insertion.scope,
330
+ name: SourceHelpers.node_name(insertion.node)
331
+ )
332
+
196
333
  case strategy
197
334
  when :aggressive
198
335
  if (range = method_comment_block_removal_range(buffer, insertion))
199
336
  rewriter.remove(range)
200
337
  end
201
338
 
202
- doc = build_method_doc(insertion, config: config, signature_provider: signature_provider)
339
+ effective_param_types = external_sig&.param_types || DocBuilder.build_param_types_from_node(
340
+ insertion.node,
341
+ external_sig: external_sig,
342
+ config: config
343
+ )
344
+
345
+ doc = build_method_doc(insertion, config: config, signature_provider: signature_provider,
346
+ core_rbs_provider: core_rbs_provider, param_types: effective_param_types)
203
347
  return if doc.nil? || doc.empty?
204
348
 
205
349
  rewriter.insert_before(anchor_bol_range, doc)
@@ -220,7 +364,10 @@ module Docscribe
220
364
  insertion,
221
365
  existing_lines: info[:doc_lines],
222
366
  config: config,
223
- signature_provider: signature_provider
367
+ signature_provider: signature_provider,
368
+ core_rbs_provider: core_rbs_provider,
369
+ param_types: external_sig&.param_types,
370
+ strategy: strategy
224
371
  )
225
372
 
226
373
  missing_lines = merge_result[:lines]
@@ -273,7 +420,9 @@ module Docscribe
273
420
  return
274
421
  end
275
422
 
276
- doc = build_method_doc(insertion, config: config, signature_provider: signature_provider)
423
+ doc = build_method_doc(insertion, config: config, signature_provider: signature_provider,
424
+ core_rbs_provider: core_rbs_provider,
425
+ param_types: external_sig&.param_types)
277
426
  return if doc.nil? || doc.empty?
278
427
 
279
428
  rewriter.insert_before(anchor_bol_range, doc)
@@ -550,14 +699,14 @@ module Docscribe
550
699
  nil
551
700
  end
552
701
 
553
- # Method documentation.
702
+ # Format an attribute `@param` tag line using the configured param tag style.
554
703
  #
555
704
  # @private
556
- # @param [Object] indent Param documentation.
557
- # @param [Object] name Param documentation.
558
- # @param [Object] type Param documentation.
559
- # @param [Object] style Param documentation.
560
- # @return [String]
705
+ # @param [String] indent leading whitespace
706
+ # @param [Symbol] name attribute name
707
+ # @param [String] type attribute type
708
+ # @param [String, Symbol] style param tag style (`"name_type"` or `"type_name"`)
709
+ # @return [String] formatted doc line
561
710
  def format_attribute_param_tag(indent, name, type, style:)
562
711
  type = type.to_s
563
712
 
@@ -590,15 +739,16 @@ module Docscribe
590
739
  config.fallback_type
591
740
  end
592
741
 
593
- # Method documentation.
742
+ # Build the appropriate external signature provider for the given source.
743
+ #
744
+ # Checks config methods in order: `signature_provider_for`, `signature_provider`, `rbs_provider`.
594
745
  #
595
746
  # @private
596
- # @param [Object] config Param documentation.
597
- # @param [Object] code Param documentation.
598
- # @param [Object] file Param documentation.
747
+ # @param [Docscribe::Config] config the active configuration
748
+ # @param [String] code the source code being processed
749
+ # @param [String] file the file name
599
750
  # @raise [StandardError]
600
- # @return [Object]
601
- # @return [Object?] if StandardError
751
+ # @return [Object, nil] a signature provider or nil
602
752
  def build_signature_provider(config, code, file)
603
753
  if config.respond_to?(:signature_provider_for)
604
754
  config.signature_provider_for(source: code, file: file)
@@ -611,44 +761,55 @@ module Docscribe
611
761
  config.respond_to?(:rbs_provider) ? config.rbs_provider : nil
612
762
  end
613
763
 
614
- # Method documentation.
764
+ # Delegate to DocBuilder.build for generating a complete doc block.
615
765
  #
616
766
  # @private
617
- # @param [Object] insertion Param documentation.
618
- # @param [Object] config Param documentation.
619
- # @param [Object] signature_provider Param documentation.
620
- # @return [Object]
621
- def build_method_doc(insertion, config:, signature_provider:)
767
+ # @param [Collector::Insertion] insertion the collected method insertion
768
+ # @param [Docscribe::Config] config the active configuration
769
+ # @param [Object, nil] signature_provider external signature provider
770
+ # @param [Object, nil] core_rbs_provider RBS core type provider
771
+ # @param [Hash, nil] param_types parameter name -> type map
772
+ # @return [String, nil] generated doc block or nil
773
+ def build_method_doc(insertion, config:, signature_provider:, core_rbs_provider:, param_types:)
622
774
  DocBuilder.build(
623
775
  insertion,
624
776
  config: config,
625
- signature_provider: signature_provider
777
+ signature_provider: signature_provider,
778
+ core_rbs_provider: core_rbs_provider,
779
+ param_types: param_types
626
780
  )
627
781
  end
628
782
 
629
- # Method documentation.
783
+ # Delegate to DocBuilder.build_missing_merge_result for generating missing doc lines only.
630
784
  #
631
785
  # @private
632
- # @param [Object] insertion Param documentation.
633
- # @param [Object] existing_lines Param documentation.
634
- # @param [Object] config Param documentation.
635
- # @param [Object] signature_provider Param documentation.
636
- # @return [Object]
637
- def build_missing_method_merge_result(insertion, existing_lines:, config:, signature_provider:)
786
+ # @param [Collector::Insertion] insertion the collected method insertion
787
+ # @param [Array<String>] existing_lines existing doc-like lines
788
+ # @param [Docscribe::Config] config the active configuration
789
+ # @param [Object, nil] signature_provider external signature provider
790
+ # @param [Object, nil] core_rbs_provider RBS core type provider
791
+ # @param [Hash, nil] param_types parameter name -> type map
792
+ # @param [Object] strategy Param documentation.
793
+ # @return [Hash] result with `:lines` and `:reasons` keys
794
+ def build_missing_method_merge_result(insertion, existing_lines:, config:, signature_provider:,
795
+ core_rbs_provider:, param_types:, strategy:)
638
796
  DocBuilder.build_missing_merge_result(
639
797
  insertion,
640
798
  existing_lines: existing_lines,
641
799
  config: config,
642
- signature_provider: signature_provider
800
+ signature_provider: signature_provider,
801
+ core_rbs_provider: core_rbs_provider,
802
+ param_types: param_types,
803
+ strategy: strategy
643
804
  )
644
805
  end
645
806
 
646
- # Method documentation.
807
+ # Get doc comment block info (preceding comments) for a method insertion.
647
808
  #
648
809
  # @private
649
- # @param [Object] buffer Param documentation.
650
- # @param [Object] insertion Param documentation.
651
- # @return [Object]
810
+ # @param [Parser::Source::Buffer] buffer the source buffer
811
+ # @param [Collector::Insertion] insertion the collected method insertion
812
+ # @return [Hash, nil] doc comment block info or nil
652
813
  def method_doc_comment_info(buffer, insertion)
653
814
  anchor_bol_range, def_bol_range = method_bol_ranges(buffer, insertion)
654
815
 
@@ -656,12 +817,12 @@ module Docscribe
656
817
  SourceHelpers.doc_comment_block_info(buffer, def_bol_range.begin_pos)
657
818
  end
658
819
 
659
- # Method documentation.
820
+ # Find the range of an existing doc comment block to remove (aggressive mode).
660
821
  #
661
822
  # @private
662
- # @param [Object] buffer Param documentation.
663
- # @param [Object] insertion Param documentation.
664
- # @return [Object]
823
+ # @param [Parser::Source::Buffer] buffer the source buffer
824
+ # @param [Collector::Insertion] insertion the collected method insertion
825
+ # @return [Parser::Source::Range, nil]
665
826
  def method_comment_block_removal_range(buffer, insertion)
666
827
  anchor_bol_range, def_bol_range = method_bol_ranges(buffer, insertion)
667
828
 
@@ -669,12 +830,12 @@ module Docscribe
669
830
  SourceHelpers.comment_block_removal_range(buffer, def_bol_range.begin_pos)
670
831
  end
671
832
 
672
- # Method documentation.
833
+ # Get the beginning-of-line ranges for the anchor and method nodes.
673
834
  #
674
835
  # @private
675
- # @param [Object] buffer Param documentation.
676
- # @param [Object] insertion Param documentation.
677
- # @return [Array]
836
+ # @param [Parser::Source::Buffer] buffer the source buffer
837
+ # @param [Collector::Insertion] insertion the collected method insertion
838
+ # @return [Array<Parser::Source::Range>]
678
839
  def method_bol_ranges(buffer, insertion)
679
840
  anchor_node = anchor_node_for(insertion)
680
841
  [
@@ -683,24 +844,23 @@ module Docscribe
683
844
  ]
684
845
  end
685
846
 
686
- # Method documentation.
847
+ # Get the source line number for the method's anchor node.
687
848
  #
688
849
  # @private
689
- # @param [Object] insertion Param documentation.
850
+ # @param [Collector::Insertion] insertion the collected method insertion
690
851
  # @raise [StandardError]
691
- # @return [Object]
692
- # @return [Object] if StandardError
852
+ # @return [Integer] the 1-based line number
693
853
  def method_line_for(insertion)
694
854
  anchor_node_for(insertion).loc.expression.line
695
855
  rescue StandardError
696
856
  insertion.node.loc.expression.line
697
857
  end
698
858
 
699
- # Method documentation.
859
+ # Get the anchor node for an insertion (Sorbet `sig` or the method node itself).
700
860
  #
701
861
  # @private
702
- # @param [Object] insertion Param documentation.
703
- # @return [Object]
862
+ # @param [Collector::Insertion] insertion the collected method insertion
863
+ # @return [Parser::AST::Node]
704
864
  def anchor_node_for(insertion)
705
865
  if insertion.respond_to?(:anchor_node) && insertion.anchor_node
706
866
  insertion.anchor_node
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Plugin
5
+ module Base
6
+ # Base class for collector plugins.
7
+ #
8
+ # CollectorPlugins receive the raw AST and source buffer directly.
9
+ # They walk the tree themselves and return insertion targets that
10
+ # Docscribe will document according to the selected strategy.
11
+ #
12
+ # Idempotency is handled by Docscribe:
13
+ # - :safe => skip if a doc-like block already exists above anchor_node
14
+ # - :aggressive => replace existing doc block above anchor_node
15
+ #
16
+ # @example Minimal plugin
17
+ # class MyPlugin < Docscribe::Plugin::Base::CollectorPlugin
18
+ # def collect(ast, buffer)
19
+ # results = []
20
+ #
21
+ # ASTWalk.walk(ast) do |node|
22
+ # next unless node.type == :send
23
+ # recv, meth, *args = *node
24
+ # next unless recv.nil? && meth == :my_dsl_method
25
+ #
26
+ # results << {
27
+ # anchor_node: node,
28
+ # doc: "# My generated doc\n# @return [void]\n"
29
+ # }
30
+ # end
31
+ #
32
+ # results
33
+ # end
34
+ # end
35
+ #
36
+ # Docscribe::Plugin::Registry.register(MyPlugin.new)
37
+ class CollectorPlugin
38
+ # Walk the AST and return documentation insertion targets.
39
+ #
40
+ # Each result is a Hash with:
41
+ # - :anchor_node => Parser::AST::Node — node above which to insert doc
42
+ # - :doc => String — complete doc block including newlines
43
+ #
44
+ # @param [Object] _ast Param documentation.
45
+ # @param [Object] _buffer Param documentation.
46
+ # @return [Array<Hash>]
47
+ def collect(_ast, _buffer)
48
+ []
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Plugin
5
+ module Base
6
+ # Base class for tag plugins.
7
+ #
8
+ # TagPlugins hook into already-collected method insertions and append
9
+ # additional YARD tags to the generated doc block.
10
+ #
11
+ # @example
12
+ # class SincePlugin < Docscribe::Plugin::Base::TagPlugin
13
+ # def initialize(version:)
14
+ # @version = version
15
+ # end
16
+ #
17
+ # def call(context)
18
+ # [Docscribe::Plugin::Tag.new(name: 'since', text: @version)]
19
+ # end
20
+ # end
21
+ #
22
+ # Docscribe::Plugin::Registry.register(SincePlugin.new(version: '1.3.0'))
23
+ class TagPlugin
24
+ # Generate additional tags for a documented method.
25
+ #
26
+ # Called once per documented method. Return [] if this plugin has
27
+ # nothing to add for this particular method.
28
+ #
29
+ # @param [Docscribe::Plugin::Context] context method context snapshot
30
+ # @param [Object] _context Param documentation.
31
+ # @return [Array<Docscribe::Plugin::Tag>]
32
+ def call(_context)
33
+ []
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Plugin
5
+ # Snapshot of everything known about a method at doc-generation time.
6
+ #
7
+ # Passed to every registered TagPlugin. Read-only — plugins must not
8
+ # mutate the context.
9
+ #
10
+ # @!attribute node
11
+ # @return [Parser::AST::Node] the :def or :defs AST node
12
+ # @!attribute container
13
+ # @return [String] e.g. "MyModule::MyClass" or "Object" for top-level
14
+ # @!attribute scope
15
+ # @return [Symbol] :instance or :class
16
+ # @!attribute visibility
17
+ # @return [Symbol] :public, :protected, or :private
18
+ # @!attribute method_name
19
+ # @return [Symbol]
20
+ # @!attribute inferred_params
21
+ # @return [Hash{String => String}] name => inferred type
22
+ # @!attribute inferred_return
23
+ # @return [String] inferred return type
24
+ # @!attribute source
25
+ # @return [String] raw method source text
26
+ Context = Struct.new(
27
+ :node,
28
+ :container,
29
+ :scope,
30
+ :visibility,
31
+ :method_name,
32
+ :inferred_params,
33
+ :inferred_return,
34
+ :source,
35
+ keyword_init: true
36
+ )
37
+ end
38
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Plugin
5
+ # Global plugin registry.
6
+ #
7
+ # Plugins are registered once at boot time (e.g. in a file loaded via
8
+ # `plugins.require:` in docscribe.yml) and called for every file Docscribe
9
+ # processes.
10
+ #
11
+ # Thread safety: registration is expected to happen before any parallel
12
+ # rewriting begins.
13
+ module Registry
14
+ @tag_plugins = []
15
+ @collector_plugins = []
16
+
17
+ module_function
18
+
19
+ # Register a plugin.
20
+ #
21
+ # Routes to the appropriate list based on plugin type:
22
+ # - subclass of Base::TagPlugin => tag plugin
23
+ # - subclass of Base::CollectorPlugin => collector plugin
24
+ # - responds to #call => tag plugin (duck typing)
25
+ # - responds to #collect => collector plugin (duck typing)
26
+ #
27
+ # @note module_function: when included, also defines #register (instance visibility: private)
28
+ # @param [Object] plugin plugin instance
29
+ # @raise [ArgumentError] if plugin type cannot be determined
30
+ # @return [void]
31
+ def register(plugin)
32
+ if plugin.is_a?(Base::CollectorPlugin) || plugin.respond_to?(:collect)
33
+ @collector_plugins << plugin
34
+ elsif plugin.is_a?(Base::TagPlugin) || plugin.respond_to?(:call)
35
+ @tag_plugins << plugin
36
+ else
37
+ raise ArgumentError, 'Plugin must respond to #call (TagPlugin) or #collect (CollectorPlugin)'
38
+ end
39
+ end
40
+
41
+ # All registered tag plugins in registration order.
42
+ #
43
+ # @note module_function: when included, also defines #tag_plugins (instance visibility: private)
44
+ # @return [Array<#call>]
45
+ def tag_plugins
46
+ @tag_plugins.dup
47
+ end
48
+
49
+ # All registered collector plugins in registration order.
50
+ #
51
+ # @note module_function: when included, also defines #collector_plugins (instance visibility: private)
52
+ # @return [Array<#collect>]
53
+ def collector_plugins
54
+ @collector_plugins.dup
55
+ end
56
+
57
+ # Remove all registered plugins.
58
+ #
59
+ # Primarily used in tests to reset state between examples.
60
+ #
61
+ # @note module_function: when included, also defines #clear! (instance visibility: private)
62
+ # @return [void]
63
+ def clear!
64
+ @tag_plugins.clear
65
+ @collector_plugins.clear
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Plugin
5
+ # A single YARD-style tag returned by a TagPlugin.
6
+ #
7
+ # @example Simple tag
8
+ # Tag.new(name: 'since', text: '1.3.0')
9
+ # # => # @since 1.3.0
10
+ #
11
+ # @example Tag with types
12
+ # Tag.new(name: 'raise', types: ['ArgumentError'], text: 'if name is nil')
13
+ # # => # @raise [ArgumentError] if name is nil
14
+ #
15
+ # @!attribute name
16
+ # @return [String] tag name without leading @
17
+ # @!attribute text
18
+ # @return [String, nil] text after the type bracket
19
+ # @!attribute types
20
+ # @return [Array<String>, nil] optional type list rendered as [Foo, Bar]
21
+ Tag = Struct.new(:name, :text, :types, keyword_init: true)
22
+ end
23
+ end