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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +662 -187
  3. data/exe/docscribe +2 -126
  4. data/lib/docscribe/cli/config_builder.rb +62 -0
  5. data/lib/docscribe/cli/init.rb +58 -0
  6. data/lib/docscribe/cli/options.rb +204 -0
  7. data/lib/docscribe/cli/run.rb +415 -0
  8. data/lib/docscribe/cli.rb +31 -0
  9. data/lib/docscribe/config/defaults.rb +71 -0
  10. data/lib/docscribe/config/emit.rb +126 -0
  11. data/lib/docscribe/config/filtering.rb +160 -0
  12. data/lib/docscribe/config/loader.rb +59 -0
  13. data/lib/docscribe/config/rbs.rb +51 -0
  14. data/lib/docscribe/config/sorbet.rb +87 -0
  15. data/lib/docscribe/config/sorting.rb +23 -0
  16. data/lib/docscribe/config/template.rb +176 -0
  17. data/lib/docscribe/config/utils.rb +102 -0
  18. data/lib/docscribe/config.rb +20 -230
  19. data/lib/docscribe/infer/ast_walk.rb +28 -0
  20. data/lib/docscribe/infer/constants.rb +11 -0
  21. data/lib/docscribe/infer/literals.rb +55 -0
  22. data/lib/docscribe/infer/names.rb +43 -0
  23. data/lib/docscribe/infer/params.rb +62 -0
  24. data/lib/docscribe/infer/raises.rb +68 -0
  25. data/lib/docscribe/infer/returns.rb +171 -0
  26. data/lib/docscribe/infer.rb +104 -258
  27. data/lib/docscribe/inline_rewriter/collector.rb +845 -0
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +383 -0
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +605 -0
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +228 -0
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +244 -0
  32. data/lib/docscribe/inline_rewriter.rb +599 -428
  33. data/lib/docscribe/parsing.rb +55 -44
  34. data/lib/docscribe/types/provider_chain.rb +37 -0
  35. data/lib/docscribe/types/rbs/provider.rb +213 -0
  36. data/lib/docscribe/types/rbs/type_formatter.rb +132 -0
  37. data/lib/docscribe/types/signature.rb +65 -0
  38. data/lib/docscribe/types/sorbet/base_provider.rb +217 -0
  39. data/lib/docscribe/types/sorbet/rbi_provider.rb +35 -0
  40. data/lib/docscribe/types/sorbet/source_provider.rb +25 -0
  41. data/lib/docscribe/version.rb +1 -1
  42. 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