docscribe 1.4.1 → 1.4.2

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 +149 -0
  3. data/lib/docscribe/cli/config_builder.rb +125 -35
  4. data/lib/docscribe/cli/generate.rb +288 -117
  5. data/lib/docscribe/cli/init.rb +49 -13
  6. data/lib/docscribe/cli/options.rb +302 -127
  7. data/lib/docscribe/cli/run.rb +391 -135
  8. data/lib/docscribe/cli.rb +23 -5
  9. data/lib/docscribe/config/defaults.rb +11 -11
  10. data/lib/docscribe/config/emit.rb +1 -0
  11. data/lib/docscribe/config/filtering.rb +24 -11
  12. data/lib/docscribe/config/loader.rb +7 -4
  13. data/lib/docscribe/config/plugin.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +31 -22
  15. data/lib/docscribe/config/sorbet.rb +41 -15
  16. data/lib/docscribe/config/sorting.rb +1 -0
  17. data/lib/docscribe/config/template.rb +1 -0
  18. data/lib/docscribe/config/utils.rb +1 -0
  19. data/lib/docscribe/config.rb +1 -0
  20. data/lib/docscribe/infer/constants.rb +15 -0
  21. data/lib/docscribe/infer/literals.rb +43 -25
  22. data/lib/docscribe/infer/names.rb +24 -15
  23. data/lib/docscribe/infer/params.rb +52 -6
  24. data/lib/docscribe/infer/raises.rb +24 -14
  25. data/lib/docscribe/infer/returns.rb +365 -182
  26. data/lib/docscribe/infer.rb +10 -9
  27. data/lib/docscribe/inline_rewriter/collector.rb +766 -375
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +217 -74
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +1488 -602
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +100 -52
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +109 -48
  32. data/lib/docscribe/inline_rewriter.rb +1009 -595
  33. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -3
  34. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -1
  35. data/lib/docscribe/plugin/registry.rb +34 -7
  36. data/lib/docscribe/plugin.rb +48 -17
  37. data/lib/docscribe/types/rbs/collection_loader.rb +0 -1
  38. data/lib/docscribe/types/rbs/provider.rb +75 -26
  39. data/lib/docscribe/types/rbs/type_formatter.rb +127 -59
  40. data/lib/docscribe/types/sorbet/base_provider.rb +31 -12
  41. data/lib/docscribe/version.rb +1 -1
  42. metadata +2 -2
@@ -65,23 +65,14 @@ module Docscribe
65
65
  # @param [Array<String>] missing_lines generated tag lines to add
66
66
  # @param [Boolean] sort_tags whether sortable tags should be reordered
67
67
  # @param [Array<String>] tag_order configured sortable tag order
68
- # @param [Hash] filter_existing Param documentation.
68
+ # @param [Hash] filter_existing tags to filter from existing block
69
69
  # @return [Array<String>]
70
70
  def merge(existing_lines, missing_lines:, sort_tags:, tag_order:, filter_existing: {})
71
71
  existing_entries = parse(existing_lines, tag_order: tag_order)
72
72
  missing_entries = parse_generated(missing_lines, tag_order: tag_order)
73
-
74
- filter_param_names = filter_existing[:param_names] || []
75
- filter_return = !!filter_existing[:return]
76
-
77
- existing_entries = existing_entries.reject do |e|
78
- (e.kind == :tag && e.tag == 'param' && filter_param_names.include?(e.subject)) ||
79
- (e.kind == :tag && e.tag == 'return' && filter_return)
80
- end
81
-
73
+ existing_entries = filter_existing_entries(existing_entries, filter_existing)
82
74
  entries = existing_entries + missing_entries
83
75
  entries = sort(entries, tag_order: tag_order) if sort_tags
84
-
85
76
  render(entries)
86
77
  end
87
78
 
@@ -98,6 +89,40 @@ module Docscribe
98
89
  end
99
90
  end
100
91
 
92
+ # Remove existing entries matching the filter criteria (param names or return tag).
93
+ #
94
+ # @note module_function: when included, also defines #filter_existing_entries (instance visibility: private)
95
+ # @param [Array<Entry>] entries parsed existing entries
96
+ # @param [Hash] filter_existing filter specification with :param_names and :return keys
97
+ # @return [Array<Entry>] filtered entries
98
+ def filter_existing_entries(entries, filter_existing)
99
+ filter_param_names = filter_existing[:param_names] || []
100
+ filter_return = !!filter_existing[:return]
101
+ entries.reject do |entry|
102
+ filter_param_entry?(entry, filter_param_names) || filter_return_entry?(entry, filter_return)
103
+ end
104
+ end
105
+
106
+ # Check whether an entry is a @param tag whose name is in the filter list.
107
+ #
108
+ # @note module_function: when included, also defines #filter_param_entry? (instance visibility: private)
109
+ # @param [Entry] entry the entry to check
110
+ # @param [Array<String>] param_names parameter names to filter
111
+ # @return [Boolean]
112
+ def filter_param_entry?(entry, param_names)
113
+ entry.kind == :tag && entry.tag == 'param' && param_names.include?(entry.subject)
114
+ end
115
+
116
+ # Check whether an entry is a @return tag that should be filtered.
117
+ #
118
+ # @note module_function: when included, also defines #filter_return_entry? (instance visibility: private)
119
+ # @param [Entry] entry the entry to check
120
+ # @param [Boolean] filter_return whether return tags should be filtered
121
+ # @return [Boolean]
122
+ def filter_return_entry?(entry, filter_return)
123
+ entry.kind == :tag && entry.tag == 'return' && filter_return
124
+ end
125
+
101
126
  # Parse a doc block into structured entries.
102
127
  #
103
128
  # Only tags listed in `tag_order` are treated as sortable tag entries.
@@ -109,32 +134,56 @@ module Docscribe
109
134
  # @return [Array<Entry>]
110
135
  def parse(lines, tag_order:)
111
136
  sortable_tags = normalized_tag_order(tag_order)
112
- entries = []
113
- i = 0
114
- index = 0
115
-
116
- while i < lines.length
117
- line = lines[i]
118
-
119
- if sortable_top_level_tag_line?(line, sortable_tags)
120
- entry, i = consume_tag_entry(lines, i, index: index, sortable_tags: sortable_tags)
121
- entries << entry
122
- else
123
- entries << Entry.new(
124
- kind: :other,
125
- lines: [line],
126
- generated: false,
127
- index: index
128
- )
129
- i += 1
130
- end
137
+ parse_lines(lines, sortable_tags, entries: [], index: 0)
138
+ end
131
139
 
140
+ # Iterate through all lines and parse each one into a structured entry.
141
+ #
142
+ # @note module_function: when included, also defines #parse_lines (instance visibility: private)
143
+ # @param [Array<String>] lines comment block lines
144
+ # @param [Array<String>] sortable_tags tag names treated as sortable
145
+ # @param [Array<Entry>] entries accumulated parsed entries
146
+ # @param [Integer] index stable ordering index for entries
147
+ # @return [Array<Entry>]
148
+ def parse_lines(lines, sortable_tags, entries:, index:)
149
+ idx = 0
150
+ while idx < lines.length
151
+ idx = parse_one_line(lines, idx, sortable_tags, entries, index)
132
152
  index += 1
133
153
  end
134
-
135
154
  entries
136
155
  end
137
156
 
157
+ # Parse a single line as a sortable tag entry or non-tag content.
158
+ #
159
+ # @note module_function: when included, also defines #parse_one_line (instance visibility: private)
160
+ # @param [Array<String>] lines comment block lines
161
+ # @param [Integer] idx current line index
162
+ # @param [Array<String>] sortable_tags tag names treated as sortable
163
+ # @param [Array<Entry>] entries accumulated parsed entries
164
+ # @param [Integer] index stable ordering index for entries
165
+ # @return [Integer] next line index after parsing
166
+ def parse_one_line(lines, idx, sortable_tags, entries, index)
167
+ if sortable_top_level_tag_line?(lines[idx], sortable_tags)
168
+ entry, idx = consume_tag_entry(lines, idx, index: index, sortable_tags: sortable_tags)
169
+ entries << entry
170
+ else
171
+ entries << build_other_entry(lines[idx], index)
172
+ idx += 1
173
+ end
174
+ idx
175
+ end
176
+
177
+ # Create an :other entry for a non-tag line (prose, blank separators, etc.).
178
+ #
179
+ # @note module_function: when included, also defines #build_other_entry (instance visibility: private)
180
+ # @param [String] line the comment line
181
+ # @param [Integer] index stable ordering index
182
+ # @return [Entry]
183
+ def build_other_entry(line, index)
184
+ Entry.new(kind: :other, lines: [line], generated: false, index: index)
185
+ end
186
+
138
187
  # Sort parsed entries by configured tag order, preserving boundaries between tag runs.
139
188
  #
140
189
  # @note module_function: when included, also defines #sort (instance visibility: private)
@@ -142,25 +191,46 @@ module Docscribe
142
191
  # @param [Array<String>] tag_order configured sortable tag order
143
192
  # @return [Array<Entry>]
144
193
  def sort(entries, tag_order:)
145
- out = []
194
+ out = [] #: Array[untyped]
146
195
  priority = build_priority(tag_order)
147
- i = 0
148
-
149
- while i < entries.length
150
- if entries[i].kind == :tag
151
- run = []
152
- while i < entries.length && entries[i].kind == :tag
153
- run << entries[i]
154
- i += 1
155
- end
196
+ sort_loop(entries, out, priority)
197
+ out
198
+ end
199
+
200
+ # Iterate entries, sorting contiguous tag runs while preserving non-tag boundaries.
201
+ #
202
+ # @note module_function: when included, also defines #sort_loop (instance visibility: private)
203
+ # @param [Array<Entry>] entries parsed entries to sort
204
+ # @param [Array<Entry>] out output array for sorted entries
205
+ # @param [Hash{String=>Integer}] priority tag priority map
206
+ # @return [void]
207
+ def sort_loop(entries, out, priority)
208
+ idx = 0
209
+
210
+ while idx < entries.length
211
+ if entries[idx].kind == :tag
212
+ run, idx = consume_tag_run(entries, idx)
156
213
  out.concat(sort_run(run, priority: priority))
157
214
  else
158
- out << entries[i]
159
- i += 1
215
+ out << entries[idx]
216
+ idx += 1
160
217
  end
161
218
  end
219
+ end
162
220
 
163
- out
221
+ # Collect a contiguous run of :tag entries starting at idx.
222
+ #
223
+ # @note module_function: when included, also defines #consume_tag_run (instance visibility: private)
224
+ # @param [Array<Entry>] entries parsed entries
225
+ # @param [Integer] idx start index
226
+ # @return [Array<(Array<Entry>, Integer)>] the tag run and next index
227
+ def consume_tag_run(entries, idx)
228
+ run = [] #: Array[untyped]
229
+ while idx < entries.length && entries[idx].kind == :tag
230
+ run << entries[idx]
231
+ idx += 1
232
+ end
233
+ [run, idx]
164
234
  end
165
235
 
166
236
  # Render parsed entries back into comment lines.
@@ -194,32 +264,61 @@ module Docscribe
194
264
  # @param [Array<Entry>] entries
195
265
  # @return [Array<Array<Entry>>]
196
266
  def build_groups(entries)
197
- groups = []
198
- i = 0
199
-
200
- while i < entries.length
201
- entry = entries[i]
267
+ groups = [] #: Array[untyped]
268
+ group_entries_loop(entries, groups)
269
+ groups
270
+ end
202
271
 
203
- if entry.tag == 'param'
204
- group = [entry]
205
- i += 1
272
+ # Iterate entries to build sorted groups, attaching @option entries to their @param.
273
+ #
274
+ # @note module_function: when included, also defines #group_entries_loop (instance visibility: private)
275
+ # @param [Array<Entry>] entries contiguous tag run entries
276
+ # @param [Array<Array<Entry>>] groups accumulated groups
277
+ # @return [void]
278
+ def group_entries_loop(entries, groups)
279
+ idx = 0
280
+ idx = group_one_entry(entries, idx, groups) while idx < entries.length
281
+ end
206
282
 
207
- while i < entries.length &&
208
- entries[i].tag == 'option' &&
209
- entries[i].option_owner &&
210
- entries[i].option_owner == entry.subject
211
- group << entries[i]
212
- i += 1
213
- end
283
+ # Group a single entry, creating a param group with @option children if applicable.
284
+ #
285
+ # @note module_function: when included, also defines #group_one_entry (instance visibility: private)
286
+ # @param [Array<Entry>] entries contiguous tag run entries
287
+ # @param [Integer] idx current entry index
288
+ # @param [Array<Array<Entry>>] groups accumulated groups
289
+ # @return [Integer] next index after processing the group
290
+ def group_one_entry(entries, idx, groups)
291
+ entry = entries[idx]
292
+ if entry.tag == 'param'
293
+ group = build_param_group(entries, idx, entry)
294
+ groups << group
295
+ idx + group.size
296
+ else
297
+ groups << [entry]
298
+ idx + 1
299
+ end
300
+ end
214
301
 
215
- groups << group
216
- else
217
- groups << [entry]
218
- i += 1
219
- end
302
+ # Build a group starting with a @param entry and including its following @option entries.
303
+ #
304
+ # @note module_function: when included, also defines #build_param_group (instance visibility: private)
305
+ # @param [Array<Entry>] entries contiguous tag run entries
306
+ # @param [Integer] idx index of the @param entry
307
+ # @param [Entry] entry the @param entry
308
+ # @return [Array<Entry>] the param group including @option children
309
+ def build_param_group(entries, idx, entry)
310
+ group = [entry]
311
+ idx += 1
312
+
313
+ while idx < entries.length &&
314
+ entries[idx].tag == 'option' &&
315
+ entries[idx].option_owner &&
316
+ entries[idx].option_owner == entry.subject
317
+ group << entries[idx]
318
+ idx += 1
220
319
  end
221
320
 
222
- groups
321
+ group
223
322
  end
224
323
 
225
324
  # Compute the priority of a grouped sortable unit.
@@ -264,21 +363,67 @@ module Docscribe
264
363
  def consume_tag_entry(lines, start_idx, index:, sortable_tags:)
265
364
  first = lines[start_idx]
266
365
  tag = extract_tag(first)
366
+ entry_lines = collect_continuation_lines(lines, start_idx + 1, first, sortable_tags)
367
+ i = entry_lines.length + start_idx
368
+ entry = build_tag_entry(first, tag, entry_lines, index)
369
+ [entry, i]
370
+ end
267
371
 
268
- entry_lines = [first]
269
- i = start_idx + 1
372
+ # Collect the first tag line and all continuation lines belonging to the same entry.
373
+ #
374
+ # @note module_function: when included, also defines #collect_continuation_lines (instance visibility: private)
375
+ # @param [Array<String>] lines comment block lines
376
+ # @param [Integer] start_idx index after the tag line
377
+ # @param [String] first the tag line itself
378
+ # @param [Array<String>] sortable_tags tag names treated as sortable
379
+ # @return [Array<String>] all lines belonging to this entry
380
+ def collect_continuation_lines(lines, start_idx, first, sortable_tags)
381
+ result = [first]
382
+ add_continuation_lines(lines, start_idx, result, sortable_tags)
383
+ result
384
+ end
270
385
 
386
+ # Append continuation lines to the result array until a non-continuation line is found.
387
+ #
388
+ # @note module_function: when included, also defines #add_continuation_lines (instance visibility: private)
389
+ # @param [Array<String>] lines comment block lines
390
+ # @param [Integer] start_idx index to start scanning from
391
+ # @param [Array<String>] result accumulated entry lines
392
+ # @param [Array<String>] sortable_tags tag names treated as sortable
393
+ # @return [void]
394
+ def add_continuation_lines(lines, start_idx, result, sortable_tags)
395
+ i = start_idx
271
396
  while i < lines.length
272
397
  line = lines[i]
273
- break if sortable_top_level_tag_line?(line, sortable_tags)
274
- break if blank_comment_line?(line)
275
- break unless continuation_comment_line?(line)
398
+ break unless continuation_candidate?(line, sortable_tags)
276
399
 
277
- entry_lines << line
400
+ result << line
278
401
  i += 1
279
402
  end
403
+ end
280
404
 
281
- entry = Entry.new(
405
+ # Check whether a line can serve as a continuation of the current tag entry.
406
+ #
407
+ # @note module_function: when included, also defines #continuation_candidate? (instance visibility: private)
408
+ # @param [String] line the line to check
409
+ # @param [Array<String>] sortable_tags tag names treated as sortable
410
+ # @return [Boolean]
411
+ def continuation_candidate?(line, sortable_tags)
412
+ !sortable_top_level_tag_line?(line, sortable_tags) &&
413
+ !blank_comment_line?(line) &&
414
+ continuation_comment_line?(line)
415
+ end
416
+
417
+ # Build a tag Entry struct with metadata from the parsed tag line and continuation lines.
418
+ #
419
+ # @note module_function: when included, also defines #build_tag_entry (instance visibility: private)
420
+ # @param [String] first the first (tag) line
421
+ # @param [String, nil] tag the extracted tag name
422
+ # @param [Array<String>] entry_lines all lines belonging to this entry
423
+ # @param [Integer] index stable ordering index
424
+ # @return [Entry]
425
+ def build_tag_entry(first, tag, entry_lines, index)
426
+ Entry.new(
282
427
  kind: :tag,
283
428
  tag: tag,
284
429
  lines: entry_lines,
@@ -287,8 +432,6 @@ module Docscribe
287
432
  generated: false,
288
433
  index: index
289
434
  )
290
-
291
- [entry, i]
292
435
  end
293
436
 
294
437
  # Extract the grouping subject for a sortable tag.