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.
@@ -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,39 @@ module Docscribe
80
83
 
81
84
  config ||= Docscribe::Config.load
82
85
  signature_provider = build_signature_provider(config, code, file.to_s)
86
+ unless core_rbs_provider
87
+ if config.respond_to?(:core_rbs_provider)
88
+ begin
89
+ core_rbs_provider = config.core_rbs_provider
90
+ rescue StandardError
91
+ core_rbs_provider = nil
92
+ end
93
+ elsif config.respond_to?(:rbs_provider)
94
+ begin
95
+ core_rbs_provider = config.rbs_provider
96
+ rescue StandardError
97
+ core_rbs_provider = nil
98
+ end
99
+ end
100
+ end
83
101
 
84
102
  collector = Docscribe::InlineRewriter::Collector.new(buffer)
85
103
  collector.process(ast)
86
104
 
105
+ # Collect additional insertions from CollectorPlugins
106
+ plugin_insertions = Docscribe::Plugin.run_collector_plugins(ast, buffer)
107
+
87
108
  method_insertions = collector.insertions
88
109
  attr_insertions = collector.respond_to?(:attr_insertions) ? collector.attr_insertions : []
89
110
 
90
- all = method_insertions.map { |i| [:method, i] } + attr_insertions.map { |i| [:attr, i] }
91
-
111
+ all = method_insertions.map { |i| [:method, i] } +
112
+ attr_insertions.map { |i| [:attr, i] } +
113
+ plugin_insertions.map { |i| [:plugin, i] }
92
114
  rewriter = Parser::Source::TreeRewriter.new(buffer)
93
115
  merge_inserts = Hash.new { |h, k| h[k] = [] }
94
116
  changes = []
95
117
 
96
- all.sort_by { |(_kind, ins)| ins.node.loc.expression.begin_pos }
118
+ all.sort_by { |(kind, ins)| plugin_insertion_pos(kind, ins) }
97
119
  .reverse_each do |kind, ins|
98
120
  case kind
99
121
  when :method
@@ -103,6 +125,7 @@ module Docscribe
103
125
  insertion: ins,
104
126
  config: config,
105
127
  signature_provider: signature_provider,
128
+ core_rbs_provider: core_rbs_provider,
106
129
  strategy: strategy,
107
130
  changes: changes,
108
131
  file: file.to_s
@@ -117,6 +140,13 @@ module Docscribe
117
140
  strategy: strategy,
118
141
  merge_inserts: merge_inserts
119
142
  )
143
+ when :plugin
144
+ apply_plugin_insertion!(
145
+ rewriter: rewriter,
146
+ buffer: buffer,
147
+ insertion: ins,
148
+ strategy: strategy
149
+ )
120
150
  end
121
151
  end
122
152
 
@@ -127,6 +157,114 @@ module Docscribe
127
157
 
128
158
  private
129
159
 
160
+ # Resolve the source begin_pos for sorting, handling both Struct-based
161
+ # insertions (method/attr) and Hash-based insertions (plugin).
162
+ #
163
+ # @private
164
+ # @param [Symbol] kind :method, :attr, or :plugin
165
+ # @param [Object] ins insertion object or hash
166
+ # @return [Integer]
167
+ def plugin_insertion_pos(kind, ins)
168
+ case kind
169
+ when :plugin
170
+ ins[:anchor_node].loc.expression.begin_pos
171
+ else
172
+ ins.node.loc.expression.begin_pos
173
+ end
174
+ end
175
+
176
+ # Apply one CollectorPlugin insertion according to the selected strategy.
177
+ #
178
+ # :safe — skip if a doc-like block already exists above anchor_node
179
+ # :aggressive — remove existing doc block, insert fresh
180
+ #
181
+ # @private
182
+ # @param [Parser::Source::TreeRewriter] rewriter
183
+ # @param [Parser::Source::Buffer] buffer
184
+ # @param [Hash] insertion { anchor_node:, doc: }
185
+ # @param [Symbol] strategy
186
+ # @return [void]
187
+ def apply_plugin_insertion!(rewriter:, buffer:, insertion:, strategy:)
188
+ anchor_node = insertion[:anchor_node]
189
+ doc = insertion[:doc]
190
+ return unless anchor_node && doc && !doc.empty?
191
+
192
+ indent = SourceHelpers.line_indent(anchor_node)
193
+ doc = normalize_plugin_doc_indent(doc, indent)
194
+ bol_range = SourceHelpers.line_start_range(buffer, anchor_node)
195
+
196
+ case strategy
197
+ when :aggressive
198
+ # Will remove ANY comments above the method. Plugin will decide what will be changed.
199
+ if (range = any_comment_block_removal_range(buffer, bol_range.begin_pos))
200
+ rewriter.remove(range)
201
+ end
202
+ rewriter.insert_before(bol_range, doc)
203
+
204
+ when :safe
205
+ return if SourceHelpers.already_has_doc_immediately_above?(buffer, bol_range.begin_pos)
206
+
207
+ rewriter.insert_before(bol_range, doc)
208
+ end
209
+ end
210
+
211
+ # Remove any contiguous comment block immediately above anchor_node,
212
+ # regardless of whether it looks like documentation.
213
+ #
214
+ # Used by CollectorPlugin in aggressive mode where the plugin itself
215
+ # is responsible for deciding what to replace.
216
+ #
217
+ # @private
218
+ # @param [Parser::Source::Buffer] buffer
219
+ # @param [Integer] bol_pos beginning-of-line position of anchor_node
220
+ # @return [Parser::Source::Range, nil]
221
+ def any_comment_block_removal_range(buffer, bol_pos)
222
+ src = buffer.source
223
+ lines = src.lines
224
+ def_line_idx = src[0...bol_pos].count("\n")
225
+ i = def_line_idx - 1
226
+
227
+ # Skip blank lines directly above node
228
+ i -= 1 while i >= 0 && lines[i].strip.empty?
229
+
230
+ # Nearest non-blank line must be a comment
231
+ return nil unless i >= 0 && lines[i] =~ /^\s*#/
232
+
233
+ # Walk upward through the entire contiguous comment block
234
+ start_idx = i
235
+ start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
236
+ start_idx += 1
237
+
238
+ # Preserve leading directive-style lines (rubocop, magic comments, etc.)
239
+ removable_start_idx = start_idx
240
+ while removable_start_idx <= i &&
241
+ SourceHelpers.preserved_comment_line?(lines[removable_start_idx])
242
+ removable_start_idx += 1
243
+ end
244
+
245
+ return nil if removable_start_idx > i
246
+
247
+ start_pos = removable_start_idx.positive? ? lines[0...removable_start_idx].join.length : 0
248
+ Parser::Source::Range.new(buffer, start_pos, bol_pos)
249
+ end
250
+
251
+ # Normalise indentation of a plugin-generated doc block.
252
+ #
253
+ # Plugins produce doc strings without knowledge of the surrounding
254
+ # indentation. We strip leading whitespace from each non-empty line
255
+ # and re-prefix it with the indent derived from anchor_node.
256
+ #
257
+ # @private
258
+ # @param [String] doc raw doc string from plugin
259
+ # @param [String] indent indentation prefix to apply
260
+ # @return [String]
261
+ def normalize_plugin_doc_indent(doc, indent)
262
+ doc.lines.map do |line|
263
+ stripped = line.lstrip
264
+ stripped.match?(/\A\r?\n?\z/) ? line : "#{indent}#{stripped}"
265
+ end.join
266
+ end
267
+
130
268
  # Normalize strategy inputs, including compatibility booleans.
131
269
  #
132
270
  # Precedence:
@@ -179,9 +317,10 @@ module Docscribe
179
317
  # @param [Symbol] strategy
180
318
  # @param [Array<Hash>] changes structured change records
181
319
  # @param [String] file
320
+ # @param [Object] core_rbs_provider Param documentation.
182
321
  # @return [void]
183
- def apply_method_insertion!(rewriter:, buffer:, insertion:, config:, signature_provider:, strategy:, changes:,
184
- file:)
322
+ def apply_method_insertion!(rewriter:, buffer:, insertion:, config:, signature_provider:, core_rbs_provider:,
323
+ strategy:, changes:, file:)
185
324
  name = SourceHelpers.node_name(insertion.node)
186
325
 
187
326
  return unless config.process_method?(
@@ -193,13 +332,21 @@ module Docscribe
193
332
 
194
333
  anchor_bol_range, = method_bol_ranges(buffer, insertion)
195
334
 
335
+ # Create external_sig for param_types lookup
336
+ external_sig = signature_provider&.signature_for(
337
+ container: insertion.container,
338
+ scope: insertion.scope,
339
+ name: SourceHelpers.node_name(insertion.node)
340
+ )
341
+
196
342
  case strategy
197
343
  when :aggressive
198
344
  if (range = method_comment_block_removal_range(buffer, insertion))
199
345
  rewriter.remove(range)
200
346
  end
201
347
 
202
- doc = build_method_doc(insertion, config: config, signature_provider: signature_provider)
348
+ doc = build_method_doc(insertion, config: config, signature_provider: signature_provider,
349
+ core_rbs_provider: core_rbs_provider, param_types: external_sig&.param_types)
203
350
  return if doc.nil? || doc.empty?
204
351
 
205
352
  rewriter.insert_before(anchor_bol_range, doc)
@@ -220,7 +367,9 @@ module Docscribe
220
367
  insertion,
221
368
  existing_lines: info[:doc_lines],
222
369
  config: config,
223
- signature_provider: signature_provider
370
+ signature_provider: signature_provider,
371
+ core_rbs_provider: core_rbs_provider,
372
+ param_types: external_sig&.param_types
224
373
  )
225
374
 
226
375
  missing_lines = merge_result[:lines]
@@ -273,7 +422,8 @@ module Docscribe
273
422
  return
274
423
  end
275
424
 
276
- doc = build_method_doc(insertion, config: config, signature_provider: signature_provider)
425
+ doc = build_method_doc(insertion, config: config, signature_provider: signature_provider,
426
+ core_rbs_provider: core_rbs_provider, param_types: external_sig&.param_types)
277
427
  return if doc.nil? || doc.empty?
278
428
 
279
429
  rewriter.insert_before(anchor_bol_range, doc)
@@ -550,14 +700,14 @@ module Docscribe
550
700
  nil
551
701
  end
552
702
 
553
- # Method documentation.
703
+ # Format an attribute `@param` tag line using the configured param tag style.
554
704
  #
555
705
  # @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]
706
+ # @param [String] indent leading whitespace
707
+ # @param [Symbol] name attribute name
708
+ # @param [String] type attribute type
709
+ # @param [String, Symbol] style param tag style (`"name_type"` or `"type_name"`)
710
+ # @return [String] formatted doc line
561
711
  def format_attribute_param_tag(indent, name, type, style:)
562
712
  type = type.to_s
563
713
 
@@ -590,15 +740,16 @@ module Docscribe
590
740
  config.fallback_type
591
741
  end
592
742
 
593
- # Method documentation.
743
+ # Build the appropriate external signature provider for the given source.
744
+ #
745
+ # Checks config methods in order: `signature_provider_for`, `signature_provider`, `rbs_provider`.
594
746
  #
595
747
  # @private
596
- # @param [Object] config Param documentation.
597
- # @param [Object] code Param documentation.
598
- # @param [Object] file Param documentation.
748
+ # @param [Docscribe::Config] config the active configuration
749
+ # @param [String] code the source code being processed
750
+ # @param [String] file the file name
599
751
  # @raise [StandardError]
600
- # @return [Object]
601
- # @return [Object?] if StandardError
752
+ # @return [Object, nil] a signature provider or nil
602
753
  def build_signature_provider(config, code, file)
603
754
  if config.respond_to?(:signature_provider_for)
604
755
  config.signature_provider_for(source: code, file: file)
@@ -611,44 +762,53 @@ module Docscribe
611
762
  config.respond_to?(:rbs_provider) ? config.rbs_provider : nil
612
763
  end
613
764
 
614
- # Method documentation.
765
+ # Delegate to DocBuilder.build for generating a complete doc block.
615
766
  #
616
767
  # @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:)
768
+ # @param [Collector::Insertion] insertion the collected method insertion
769
+ # @param [Docscribe::Config] config the active configuration
770
+ # @param [Object, nil] signature_provider external signature provider
771
+ # @param [Object, nil] core_rbs_provider RBS core type provider
772
+ # @param [Hash, nil] param_types parameter name -> type map
773
+ # @return [String, nil] generated doc block or nil
774
+ def build_method_doc(insertion, config:, signature_provider:, core_rbs_provider:, param_types:)
622
775
  DocBuilder.build(
623
776
  insertion,
624
777
  config: config,
625
- signature_provider: signature_provider
778
+ signature_provider: signature_provider,
779
+ core_rbs_provider: core_rbs_provider,
780
+ param_types: param_types
626
781
  )
627
782
  end
628
783
 
629
- # Method documentation.
784
+ # Delegate to DocBuilder.build_missing_merge_result for generating missing doc lines only.
630
785
  #
631
786
  # @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:)
787
+ # @param [Collector::Insertion] insertion the collected method insertion
788
+ # @param [Array<String>] existing_lines existing doc-like lines
789
+ # @param [Docscribe::Config] config the active configuration
790
+ # @param [Object, nil] signature_provider external signature provider
791
+ # @param [Object, nil] core_rbs_provider RBS core type provider
792
+ # @param [Hash, nil] param_types parameter name -> type map
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:)
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
643
803
  )
644
804
  end
645
805
 
646
- # Method documentation.
806
+ # Get doc comment block info (preceding comments) for a method insertion.
647
807
  #
648
808
  # @private
649
- # @param [Object] buffer Param documentation.
650
- # @param [Object] insertion Param documentation.
651
- # @return [Object]
809
+ # @param [Parser::Source::Buffer] buffer the source buffer
810
+ # @param [Collector::Insertion] insertion the collected method insertion
811
+ # @return [Hash, nil] doc comment block info or nil
652
812
  def method_doc_comment_info(buffer, insertion)
653
813
  anchor_bol_range, def_bol_range = method_bol_ranges(buffer, insertion)
654
814
 
@@ -656,12 +816,12 @@ module Docscribe
656
816
  SourceHelpers.doc_comment_block_info(buffer, def_bol_range.begin_pos)
657
817
  end
658
818
 
659
- # Method documentation.
819
+ # Find the range of an existing doc comment block to remove (aggressive mode).
660
820
  #
661
821
  # @private
662
- # @param [Object] buffer Param documentation.
663
- # @param [Object] insertion Param documentation.
664
- # @return [Object]
822
+ # @param [Parser::Source::Buffer] buffer the source buffer
823
+ # @param [Collector::Insertion] insertion the collected method insertion
824
+ # @return [Parser::Source::Range, nil]
665
825
  def method_comment_block_removal_range(buffer, insertion)
666
826
  anchor_bol_range, def_bol_range = method_bol_ranges(buffer, insertion)
667
827
 
@@ -669,12 +829,12 @@ module Docscribe
669
829
  SourceHelpers.comment_block_removal_range(buffer, def_bol_range.begin_pos)
670
830
  end
671
831
 
672
- # Method documentation.
832
+ # Get the beginning-of-line ranges for the anchor and method nodes.
673
833
  #
674
834
  # @private
675
- # @param [Object] buffer Param documentation.
676
- # @param [Object] insertion Param documentation.
677
- # @return [Array]
835
+ # @param [Parser::Source::Buffer] buffer the source buffer
836
+ # @param [Collector::Insertion] insertion the collected method insertion
837
+ # @return [Array<Parser::Source::Range>]
678
838
  def method_bol_ranges(buffer, insertion)
679
839
  anchor_node = anchor_node_for(insertion)
680
840
  [
@@ -683,24 +843,23 @@ module Docscribe
683
843
  ]
684
844
  end
685
845
 
686
- # Method documentation.
846
+ # Get the source line number for the method's anchor node.
687
847
  #
688
848
  # @private
689
- # @param [Object] insertion Param documentation.
849
+ # @param [Collector::Insertion] insertion the collected method insertion
690
850
  # @raise [StandardError]
691
- # @return [Object]
692
- # @return [Object] if StandardError
851
+ # @return [Integer] the 1-based line number
693
852
  def method_line_for(insertion)
694
853
  anchor_node_for(insertion).loc.expression.line
695
854
  rescue StandardError
696
855
  insertion.node.loc.expression.line
697
856
  end
698
857
 
699
- # Method documentation.
858
+ # Get the anchor node for an insertion (Sorbet `sig` or the method node itself).
700
859
  #
701
860
  # @private
702
- # @param [Object] insertion Param documentation.
703
- # @return [Object]
861
+ # @param [Collector::Insertion] insertion the collected method insertion
862
+ # @return [Parser::AST::Node]
704
863
  def anchor_node_for(insertion)
705
864
  if insertion.respond_to?(:anchor_node) && insertion.anchor_node
706
865
  insertion.anchor_node
@@ -0,0 +1,55 @@
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 [Parser::AST::Node] ast root AST node of the file
45
+ # @param [Parser::Source::Buffer] buffer source buffer
46
+ # @param [Object] _ast Param documentation.
47
+ # @param [Object] _buffer Param documentation.
48
+ # @return [Array<Hash>]
49
+ def collect(_ast, _buffer)
50
+ []
51
+ end
52
+ end
53
+ end
54
+ end
55
+ 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