docscribe 1.1.0 → 1.2.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.
- checksums.yaml +4 -4
- data/README.md +662 -187
- data/exe/docscribe +2 -126
- data/lib/docscribe/cli/config_builder.rb +62 -0
- data/lib/docscribe/cli/init.rb +58 -0
- data/lib/docscribe/cli/options.rb +204 -0
- data/lib/docscribe/cli/run.rb +415 -0
- data/lib/docscribe/cli.rb +31 -0
- data/lib/docscribe/config/defaults.rb +71 -0
- data/lib/docscribe/config/emit.rb +126 -0
- data/lib/docscribe/config/filtering.rb +160 -0
- data/lib/docscribe/config/loader.rb +59 -0
- data/lib/docscribe/config/rbs.rb +51 -0
- data/lib/docscribe/config/sorbet.rb +87 -0
- data/lib/docscribe/config/sorting.rb +23 -0
- data/lib/docscribe/config/template.rb +176 -0
- data/lib/docscribe/config/utils.rb +102 -0
- data/lib/docscribe/config.rb +20 -230
- data/lib/docscribe/infer/ast_walk.rb +28 -0
- data/lib/docscribe/infer/constants.rb +11 -0
- data/lib/docscribe/infer/literals.rb +55 -0
- data/lib/docscribe/infer/names.rb +43 -0
- data/lib/docscribe/infer/params.rb +62 -0
- data/lib/docscribe/infer/raises.rb +68 -0
- data/lib/docscribe/infer/returns.rb +171 -0
- data/lib/docscribe/infer.rb +104 -258
- data/lib/docscribe/inline_rewriter/collector.rb +845 -0
- data/lib/docscribe/inline_rewriter/doc_block.rb +383 -0
- data/lib/docscribe/inline_rewriter/doc_builder.rb +605 -0
- data/lib/docscribe/inline_rewriter/source_helpers.rb +228 -0
- data/lib/docscribe/inline_rewriter/tag_sorter.rb +244 -0
- data/lib/docscribe/inline_rewriter.rb +599 -428
- data/lib/docscribe/parsing.rb +55 -44
- data/lib/docscribe/types/provider_chain.rb +37 -0
- data/lib/docscribe/types/rbs/provider.rb +213 -0
- data/lib/docscribe/types/rbs/type_formatter.rb +132 -0
- data/lib/docscribe/types/signature.rb +65 -0
- data/lib/docscribe/types/sorbet/base_provider.rb +217 -0
- data/lib/docscribe/types/sorbet/rbi_provider.rb +35 -0
- data/lib/docscribe/types/sorbet/source_provider.rb +25 -0
- data/lib/docscribe/version.rb +1 -1
- metadata +37 -3
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docscribe
|
|
4
|
+
module InlineRewriter
|
|
5
|
+
# Text-preserving doc-block parsing and tag sorting helpers.
|
|
6
|
+
#
|
|
7
|
+
# This module operates on existing comment blocks and is used to:
|
|
8
|
+
# - preserve user-authored tag text exactly
|
|
9
|
+
# - append generated missing tag entries
|
|
10
|
+
# - sort only configured sortable tags
|
|
11
|
+
# - preserve boundaries such as prose comments and blank comment lines
|
|
12
|
+
module DocBlock
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# One parsed entry inside a doc block.
|
|
16
|
+
#
|
|
17
|
+
# `kind` is:
|
|
18
|
+
# - `:tag` for a sortable top-level tag entry
|
|
19
|
+
# - `:other` for prose/separators/non-sortable content
|
|
20
|
+
# @!attribute [rw] kind
|
|
21
|
+
# @return [Object]
|
|
22
|
+
# @param [Object] value
|
|
23
|
+
#
|
|
24
|
+
# @!attribute [rw] tag
|
|
25
|
+
# @return [Object]
|
|
26
|
+
# @param [Object] value
|
|
27
|
+
#
|
|
28
|
+
# @!attribute [rw] lines
|
|
29
|
+
# @return [Object]
|
|
30
|
+
# @param [Object] value
|
|
31
|
+
#
|
|
32
|
+
# @!attribute [rw] subject
|
|
33
|
+
# @return [Object]
|
|
34
|
+
# @param [Object] value
|
|
35
|
+
#
|
|
36
|
+
# @!attribute [rw] option_owner
|
|
37
|
+
# @return [Object]
|
|
38
|
+
# @param [Object] value
|
|
39
|
+
#
|
|
40
|
+
# @!attribute [rw] generated
|
|
41
|
+
# @return [Object]
|
|
42
|
+
# @param [Object] value
|
|
43
|
+
#
|
|
44
|
+
# @!attribute [rw] index
|
|
45
|
+
# @return [Object]
|
|
46
|
+
# @param [Object] value
|
|
47
|
+
Entry = Struct.new(
|
|
48
|
+
:kind,
|
|
49
|
+
:tag,
|
|
50
|
+
:lines,
|
|
51
|
+
:subject,
|
|
52
|
+
:option_owner,
|
|
53
|
+
:generated,
|
|
54
|
+
:index,
|
|
55
|
+
keyword_init: true
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Merge existing doc lines with newly generated missing tag lines.
|
|
59
|
+
#
|
|
60
|
+
# Existing text is preserved exactly. If sorting is enabled, only sortable tag runs
|
|
61
|
+
# are normalized according to the configured tag order.
|
|
62
|
+
#
|
|
63
|
+
# @note module_function: when included, also defines #merge (instance visibility: private)
|
|
64
|
+
# @param [Array<String>] existing_lines existing doc block lines
|
|
65
|
+
# @param [Array<String>] missing_lines generated tag lines to add
|
|
66
|
+
# @param [Boolean] sort_tags whether sortable tags should be reordered
|
|
67
|
+
# @param [Array<String>] tag_order configured sortable tag order
|
|
68
|
+
# @return [Array<String>]
|
|
69
|
+
def merge(existing_lines, missing_lines:, sort_tags:, tag_order:)
|
|
70
|
+
existing_entries = parse(existing_lines, tag_order: tag_order)
|
|
71
|
+
missing_entries = parse_generated(missing_lines, tag_order: tag_order)
|
|
72
|
+
|
|
73
|
+
entries = existing_entries + missing_entries
|
|
74
|
+
entries = sort(entries, tag_order: tag_order) if sort_tags
|
|
75
|
+
|
|
76
|
+
render(entries)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Parse generated missing tag lines and mark them as generated entries.
|
|
80
|
+
#
|
|
81
|
+
# @note module_function: when included, also defines #parse_generated (instance visibility: private)
|
|
82
|
+
# @param [Array<String>] lines generated lines
|
|
83
|
+
# @param [Array<String>] tag_order configured sortable tag order
|
|
84
|
+
# @return [Array<Entry>]
|
|
85
|
+
def parse_generated(lines, tag_order:)
|
|
86
|
+
parse(lines, tag_order: tag_order).map do |entry|
|
|
87
|
+
entry.generated = true if entry.kind == :tag
|
|
88
|
+
entry
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Parse a doc block into structured entries.
|
|
93
|
+
#
|
|
94
|
+
# Only tags listed in `tag_order` are treated as sortable tag entries.
|
|
95
|
+
# Other lines become `:other` entries and act as sort boundaries.
|
|
96
|
+
#
|
|
97
|
+
# @note module_function: when included, also defines #parse (instance visibility: private)
|
|
98
|
+
# @param [Array<String>] lines comment block lines
|
|
99
|
+
# @param [Array<String>] tag_order configured sortable tag order
|
|
100
|
+
# @return [Array<Entry>]
|
|
101
|
+
def parse(lines, tag_order:)
|
|
102
|
+
sortable_tags = normalized_tag_order(tag_order)
|
|
103
|
+
entries = []
|
|
104
|
+
i = 0
|
|
105
|
+
index = 0
|
|
106
|
+
|
|
107
|
+
while i < lines.length
|
|
108
|
+
line = lines[i]
|
|
109
|
+
|
|
110
|
+
if sortable_top_level_tag_line?(line, sortable_tags)
|
|
111
|
+
entry, i = consume_tag_entry(lines, i, index: index, sortable_tags: sortable_tags)
|
|
112
|
+
entries << entry
|
|
113
|
+
else
|
|
114
|
+
entries << Entry.new(
|
|
115
|
+
kind: :other,
|
|
116
|
+
lines: [line],
|
|
117
|
+
generated: false,
|
|
118
|
+
index: index
|
|
119
|
+
)
|
|
120
|
+
i += 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
index += 1
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
entries
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Sort parsed entries by configured tag order, preserving boundaries between tag runs.
|
|
130
|
+
#
|
|
131
|
+
# @note module_function: when included, also defines #sort (instance visibility: private)
|
|
132
|
+
# @param [Array<Entry>] entries parsed entries
|
|
133
|
+
# @param [Array<String>] tag_order configured sortable tag order
|
|
134
|
+
# @return [Array<Entry>]
|
|
135
|
+
def sort(entries, tag_order:)
|
|
136
|
+
out = []
|
|
137
|
+
priority = build_priority(tag_order)
|
|
138
|
+
i = 0
|
|
139
|
+
|
|
140
|
+
while i < entries.length
|
|
141
|
+
if entries[i].kind == :tag
|
|
142
|
+
run = []
|
|
143
|
+
while i < entries.length && entries[i].kind == :tag
|
|
144
|
+
run << entries[i]
|
|
145
|
+
i += 1
|
|
146
|
+
end
|
|
147
|
+
out.concat(sort_run(run, priority: priority))
|
|
148
|
+
else
|
|
149
|
+
out << entries[i]
|
|
150
|
+
i += 1
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
out
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Render parsed entries back into comment lines.
|
|
158
|
+
#
|
|
159
|
+
# @note module_function: when included, also defines #render (instance visibility: private)
|
|
160
|
+
# @param [Array<Entry>] entries
|
|
161
|
+
# @return [Array<String>]
|
|
162
|
+
def render(entries)
|
|
163
|
+
entries.flat_map(&:lines)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Sort one contiguous run of sortable tag entries.
|
|
167
|
+
#
|
|
168
|
+
# @note module_function: when included, also defines #sort_run (instance visibility: private)
|
|
169
|
+
# @param [Array<Entry>] entries contiguous tag run
|
|
170
|
+
# @param [Hash{String=>Integer}] priority tag priority map
|
|
171
|
+
# @return [Array<Entry>]
|
|
172
|
+
def sort_run(entries, priority:)
|
|
173
|
+
groups = build_groups(entries)
|
|
174
|
+
|
|
175
|
+
groups
|
|
176
|
+
.each_with_index
|
|
177
|
+
.sort_by { |(group, idx)| [group_priority(group, priority), idx] }
|
|
178
|
+
.map(&:first)
|
|
179
|
+
.flatten
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Group entries so related `@option` tags stay attached to their owning `@param`.
|
|
183
|
+
#
|
|
184
|
+
# @note module_function: when included, also defines #build_groups (instance visibility: private)
|
|
185
|
+
# @param [Array<Entry>] entries
|
|
186
|
+
# @return [Array<Array<Entry>>]
|
|
187
|
+
def build_groups(entries)
|
|
188
|
+
groups = []
|
|
189
|
+
i = 0
|
|
190
|
+
|
|
191
|
+
while i < entries.length
|
|
192
|
+
entry = entries[i]
|
|
193
|
+
|
|
194
|
+
if entry.tag == 'param'
|
|
195
|
+
group = [entry]
|
|
196
|
+
i += 1
|
|
197
|
+
|
|
198
|
+
while i < entries.length &&
|
|
199
|
+
entries[i].tag == 'option' &&
|
|
200
|
+
entries[i].option_owner &&
|
|
201
|
+
entries[i].option_owner == entry.subject
|
|
202
|
+
group << entries[i]
|
|
203
|
+
i += 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
groups << group
|
|
207
|
+
else
|
|
208
|
+
groups << [entry]
|
|
209
|
+
i += 1
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
groups
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Compute the priority of a grouped sortable unit.
|
|
217
|
+
#
|
|
218
|
+
# @note module_function: when included, also defines #group_priority (instance visibility: private)
|
|
219
|
+
# @param [Array<Entry>] group
|
|
220
|
+
# @param [Hash{String=>Integer}] priority
|
|
221
|
+
# @return [Integer]
|
|
222
|
+
def group_priority(group, priority)
|
|
223
|
+
priority.fetch(group.first.tag, priority.length)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Build a tag priority map from configured order.
|
|
227
|
+
#
|
|
228
|
+
# @note module_function: when included, also defines #build_priority (instance visibility: private)
|
|
229
|
+
# @param [Array<String>] tag_order
|
|
230
|
+
# @return [Hash{String=>Integer}]
|
|
231
|
+
def build_priority(tag_order)
|
|
232
|
+
normalized_tag_order(tag_order).each_with_index.to_h
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Normalize configured tag names by removing leading `@`.
|
|
236
|
+
#
|
|
237
|
+
# @note module_function: when included, also defines #normalized_tag_order (instance visibility: private)
|
|
238
|
+
# @param [Array<String>] tag_order
|
|
239
|
+
# @return [Array<String>]
|
|
240
|
+
def normalized_tag_order(tag_order)
|
|
241
|
+
Array(tag_order).map { |t| t.to_s.sub(/\A@/, '') }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Consume one sortable top-level tag entry and its continuation lines.
|
|
245
|
+
#
|
|
246
|
+
# Continuation lines are comment lines that belong to the same logical tag entry
|
|
247
|
+
# until a new sortable tag line or a blank comment separator is encountered.
|
|
248
|
+
#
|
|
249
|
+
# @note module_function: when included, also defines #consume_tag_entry (instance visibility: private)
|
|
250
|
+
# @param [Array<String>] lines
|
|
251
|
+
# @param [Integer] start_idx
|
|
252
|
+
# @param [Integer] index stable original index
|
|
253
|
+
# @param [Array<String>] sortable_tags
|
|
254
|
+
# @return [Array<(Entry, Integer)>] parsed entry and next index
|
|
255
|
+
def consume_tag_entry(lines, start_idx, index:, sortable_tags:)
|
|
256
|
+
first = lines[start_idx]
|
|
257
|
+
tag = extract_tag(first)
|
|
258
|
+
|
|
259
|
+
entry_lines = [first]
|
|
260
|
+
i = start_idx + 1
|
|
261
|
+
|
|
262
|
+
while i < lines.length
|
|
263
|
+
line = lines[i]
|
|
264
|
+
break if sortable_top_level_tag_line?(line, sortable_tags)
|
|
265
|
+
break if blank_comment_line?(line)
|
|
266
|
+
break unless continuation_comment_line?(line)
|
|
267
|
+
|
|
268
|
+
entry_lines << line
|
|
269
|
+
i += 1
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
entry = Entry.new(
|
|
273
|
+
kind: :tag,
|
|
274
|
+
tag: tag,
|
|
275
|
+
lines: entry_lines,
|
|
276
|
+
subject: extract_subject(first, tag),
|
|
277
|
+
option_owner: extract_option_owner(first),
|
|
278
|
+
generated: false,
|
|
279
|
+
index: index
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
[entry, i]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Extract the grouping subject for a sortable tag.
|
|
286
|
+
#
|
|
287
|
+
# Currently only `@param` entries carry a subject, used to keep `@option` tags attached.
|
|
288
|
+
#
|
|
289
|
+
# @note module_function: when included, also defines #extract_subject (instance visibility: private)
|
|
290
|
+
# @param [String] line
|
|
291
|
+
# @param [String] tag
|
|
292
|
+
# @return [String, nil]
|
|
293
|
+
def extract_subject(line, tag)
|
|
294
|
+
case tag
|
|
295
|
+
when 'param'
|
|
296
|
+
extract_param_name(line)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Extract a parameter name from a `@param` line.
|
|
301
|
+
#
|
|
302
|
+
# Supports both:
|
|
303
|
+
# - `@param [Type] name`
|
|
304
|
+
# - `@param name [Type]`
|
|
305
|
+
#
|
|
306
|
+
# @note module_function: when included, also defines #extract_param_name (instance visibility: private)
|
|
307
|
+
# @param [String] line
|
|
308
|
+
# @return [String, nil]
|
|
309
|
+
def extract_param_name(line)
|
|
310
|
+
return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+\[[^\]]+\]\s+(\S+)/
|
|
311
|
+
return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+(\S+)\s+\[[^\]]+\]/
|
|
312
|
+
|
|
313
|
+
nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Extract the owning options-hash param name from an `@option` line.
|
|
317
|
+
#
|
|
318
|
+
# @note module_function: when included, also defines #extract_option_owner (instance visibility: private)
|
|
319
|
+
# @param [String] line
|
|
320
|
+
# @return [String, nil]
|
|
321
|
+
def extract_option_owner(line)
|
|
322
|
+
line[/^\s*#\s*@option\b\s+(\S+)/, 1]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Whether a line is a sortable top-level tag line.
|
|
326
|
+
#
|
|
327
|
+
# @note module_function: when included, also defines #sortable_top_level_tag_line? (instance visibility: private)
|
|
328
|
+
# @param [String] line
|
|
329
|
+
# @param [Array<String>] sortable_tags
|
|
330
|
+
# @return [Boolean]
|
|
331
|
+
def sortable_top_level_tag_line?(line, sortable_tags)
|
|
332
|
+
return false unless top_level_tag_line?(line)
|
|
333
|
+
|
|
334
|
+
sortable_tags.include?(extract_tag(line))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Extract a top-level tag name without the leading `@`.
|
|
338
|
+
#
|
|
339
|
+
# @note module_function: when included, also defines #extract_tag (instance visibility: private)
|
|
340
|
+
# @param [String] line
|
|
341
|
+
# @return [String, nil]
|
|
342
|
+
def extract_tag(line)
|
|
343
|
+
line[/^\s*#\s*@(\w+)/, 1]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Whether a line begins a top-level YARD-style tag.
|
|
347
|
+
#
|
|
348
|
+
# @note module_function: when included, also defines #top_level_tag_line? (instance visibility: private)
|
|
349
|
+
# @param [String] line
|
|
350
|
+
# @return [Boolean]
|
|
351
|
+
def top_level_tag_line?(line)
|
|
352
|
+
!!(line =~ /^\s*#\s*@\w+/)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Whether a line is any comment line.
|
|
356
|
+
#
|
|
357
|
+
# @note module_function: when included, also defines #comment_line? (instance visibility: private)
|
|
358
|
+
# @param [String] line
|
|
359
|
+
# @return [Boolean]
|
|
360
|
+
def comment_line?(line)
|
|
361
|
+
!!(line =~ /^\s*#/)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Whether a line is a blank comment separator such as `#`.
|
|
365
|
+
#
|
|
366
|
+
# @note module_function: when included, also defines #blank_comment_line? (instance visibility: private)
|
|
367
|
+
# @param [String] line
|
|
368
|
+
# @return [Boolean]
|
|
369
|
+
def blank_comment_line?(line)
|
|
370
|
+
!!(line =~ /^\s*#\s*$/)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Whether a comment line should be treated as a continuation of the previous tag entry.
|
|
374
|
+
#
|
|
375
|
+
# @note module_function: when included, also defines #continuation_comment_line? (instance visibility: private)
|
|
376
|
+
# @param [String] line
|
|
377
|
+
# @return [Boolean]
|
|
378
|
+
def continuation_comment_line?(line)
|
|
379
|
+
!!(line =~ /^\s*#[ \t]{2,}\S/)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|