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.
- checksums.yaml +4 -4
- data/README.md +296 -2
- data/lib/docscribe/cli/config_builder.rb +17 -5
- data/lib/docscribe/cli/generate.rb +309 -0
- data/lib/docscribe/cli/options.rb +8 -1
- data/lib/docscribe/cli/run.rb +52 -51
- data/lib/docscribe/cli.rb +8 -2
- data/lib/docscribe/config/defaults.rb +8 -2
- data/lib/docscribe/config/filtering.rb +2 -2
- data/lib/docscribe/config/plugin.rb +29 -0
- data/lib/docscribe/config/rbs.rb +38 -1
- data/lib/docscribe/config/template.rb +54 -130
- data/lib/docscribe/config.rb +1 -0
- data/lib/docscribe/infer/returns.rb +151 -12
- data/lib/docscribe/infer.rb +7 -2
- data/lib/docscribe/inline_rewriter/collector.rb +144 -97
- data/lib/docscribe/inline_rewriter/doc_block.rb +10 -1
- data/lib/docscribe/inline_rewriter/doc_builder.rb +256 -54
- data/lib/docscribe/inline_rewriter.rb +216 -56
- data/lib/docscribe/plugin/base/collector_plugin.rb +53 -0
- data/lib/docscribe/plugin/base/tag_plugin.rb +38 -0
- data/lib/docscribe/plugin/context.rb +38 -0
- data/lib/docscribe/plugin/registry.rb +69 -0
- data/lib/docscribe/plugin/tag.rb +23 -0
- data/lib/docscribe/plugin.rb +58 -0
- data/lib/docscribe/types/rbs/collection_loader.rb +50 -0
- data/lib/docscribe/types/rbs/provider.rb +3 -0
- data/lib/docscribe/version.rb +1 -1
- metadata +13 -5
|
@@ -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
|
|
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:
|
|
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] } +
|
|
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 { |(
|
|
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:,
|
|
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
|
-
|
|
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
|
-
#
|
|
702
|
+
# Format an attribute `@param` tag line using the configured param tag style.
|
|
554
703
|
#
|
|
555
704
|
# @private
|
|
556
|
-
# @param [
|
|
557
|
-
# @param [
|
|
558
|
-
# @param [
|
|
559
|
-
# @param [
|
|
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
|
-
#
|
|
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 [
|
|
597
|
-
# @param [
|
|
598
|
-
# @param [
|
|
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
|
-
#
|
|
764
|
+
# Delegate to DocBuilder.build for generating a complete doc block.
|
|
615
765
|
#
|
|
616
766
|
# @private
|
|
617
|
-
# @param [
|
|
618
|
-
# @param [
|
|
619
|
-
# @param [Object] signature_provider
|
|
620
|
-
# @
|
|
621
|
-
|
|
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
|
-
#
|
|
783
|
+
# Delegate to DocBuilder.build_missing_merge_result for generating missing doc lines only.
|
|
630
784
|
#
|
|
631
785
|
# @private
|
|
632
|
-
# @param [
|
|
633
|
-
# @param [
|
|
634
|
-
# @param [
|
|
635
|
-
# @param [Object] signature_provider
|
|
636
|
-
# @
|
|
637
|
-
|
|
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
|
-
#
|
|
807
|
+
# Get doc comment block info (preceding comments) for a method insertion.
|
|
647
808
|
#
|
|
648
809
|
# @private
|
|
649
|
-
# @param [
|
|
650
|
-
# @param [
|
|
651
|
-
# @return [
|
|
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
|
-
#
|
|
820
|
+
# Find the range of an existing doc comment block to remove (aggressive mode).
|
|
660
821
|
#
|
|
661
822
|
# @private
|
|
662
|
-
# @param [
|
|
663
|
-
# @param [
|
|
664
|
-
# @return [
|
|
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
|
-
#
|
|
833
|
+
# Get the beginning-of-line ranges for the anchor and method nodes.
|
|
673
834
|
#
|
|
674
835
|
# @private
|
|
675
|
-
# @param [
|
|
676
|
-
# @param [
|
|
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
|
-
#
|
|
847
|
+
# Get the source line number for the method's anchor node.
|
|
687
848
|
#
|
|
688
849
|
# @private
|
|
689
|
-
# @param [
|
|
850
|
+
# @param [Collector::Insertion] insertion the collected method insertion
|
|
690
851
|
# @raise [StandardError]
|
|
691
|
-
# @return [
|
|
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
|
-
#
|
|
859
|
+
# Get the anchor node for an insertion (Sorbet `sig` or the method node itself).
|
|
700
860
|
#
|
|
701
861
|
# @private
|
|
702
|
-
# @param [
|
|
703
|
-
# @return [
|
|
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
|