docscribe 1.4.1 → 1.5.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +588 -104
  3. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  4. data/lib/docscribe/cli/config_builder.rb +180 -36
  5. data/lib/docscribe/cli/formatters/json.rb +294 -0
  6. data/lib/docscribe/cli/formatters/sarif.rb +235 -0
  7. data/lib/docscribe/cli/formatters/text.rb +208 -0
  8. data/lib/docscribe/cli/formatters.rb +26 -0
  9. data/lib/docscribe/cli/generate.rb +296 -125
  10. data/lib/docscribe/cli/init.rb +58 -14
  11. data/lib/docscribe/cli/options.rb +410 -133
  12. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  13. data/lib/docscribe/cli/run.rb +503 -189
  14. data/lib/docscribe/cli/sigs.rb +366 -0
  15. data/lib/docscribe/cli/update_types.rb +103 -0
  16. data/lib/docscribe/cli.rb +35 -9
  17. data/lib/docscribe/config/defaults.rb +16 -12
  18. data/lib/docscribe/config/emit.rb +18 -0
  19. data/lib/docscribe/config/filtering.rb +37 -31
  20. data/lib/docscribe/config/loader.rb +20 -13
  21. data/lib/docscribe/config/plugin.rb +2 -1
  22. data/lib/docscribe/config/rbs.rb +68 -27
  23. data/lib/docscribe/config/sorbet.rb +40 -17
  24. data/lib/docscribe/config/sorting.rb +2 -1
  25. data/lib/docscribe/config/template.rb +10 -1
  26. data/lib/docscribe/config/utils.rb +12 -9
  27. data/lib/docscribe/config.rb +3 -4
  28. data/lib/docscribe/infer/ast_walk.rb +1 -1
  29. data/lib/docscribe/infer/constants.rb +15 -0
  30. data/lib/docscribe/infer/literals.rb +39 -26
  31. data/lib/docscribe/infer/names.rb +24 -16
  32. data/lib/docscribe/infer/params.rb +57 -13
  33. data/lib/docscribe/infer/raises.rb +23 -15
  34. data/lib/docscribe/infer/returns.rb +784 -199
  35. data/lib/docscribe/infer.rb +28 -28
  36. data/lib/docscribe/inline_rewriter/collector.rb +816 -430
  37. data/lib/docscribe/inline_rewriter/doc_block.rb +323 -150
  38. data/lib/docscribe/inline_rewriter/doc_builder.rb +1837 -648
  39. data/lib/docscribe/inline_rewriter/source_helpers.rb +119 -71
  40. data/lib/docscribe/inline_rewriter/tag_sorter.rb +165 -107
  41. data/lib/docscribe/inline_rewriter.rb +1144 -727
  42. data/lib/docscribe/parsing.rb +29 -10
  43. data/lib/docscribe/plugin/base/collector_plugin.rb +3 -3
  44. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -2
  45. data/lib/docscribe/plugin/context.rb +28 -18
  46. data/lib/docscribe/plugin/registry.rb +49 -23
  47. data/lib/docscribe/plugin/tag.rb +9 -14
  48. data/lib/docscribe/plugin.rb +54 -22
  49. data/lib/docscribe/types/provider_chain.rb +4 -2
  50. data/lib/docscribe/types/rbs/collection_loader.rb +2 -3
  51. data/lib/docscribe/types/rbs/provider.rb +127 -62
  52. data/lib/docscribe/types/rbs/type_formatter.rb +286 -77
  53. data/lib/docscribe/types/signature.rb +22 -42
  54. data/lib/docscribe/types/sorbet/base_provider.rb +51 -27
  55. data/lib/docscribe/types/sorbet/rbi_provider.rb +3 -3
  56. data/lib/docscribe/types/sorbet/source_provider.rb +3 -2
  57. data/lib/docscribe/types/yard/formatter.rb +100 -0
  58. data/lib/docscribe/types/yard/parser.rb +240 -0
  59. data/lib/docscribe/types/yard/types.rb +52 -0
  60. data/lib/docscribe/version.rb +1 -1
  61. metadata +34 -2
@@ -12,38 +12,33 @@ module Docscribe
12
12
  module DocBlock
13
13
  module_function
14
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
15
  # @!attribute [rw] kind
21
- # @return [Object]
22
- # @param [Object] value
16
+ # @return [Symbol]
17
+ # @param [Symbol] value
23
18
  #
24
19
  # @!attribute [rw] tag
25
- # @return [Object]
26
- # @param [Object] value
20
+ # @return [String]
21
+ # @param [String] value
27
22
  #
28
23
  # @!attribute [rw] lines
29
- # @return [Object]
30
- # @param [Object] value
24
+ # @return [Array<String>]
25
+ # @param [Array<String>] value
31
26
  #
32
27
  # @!attribute [rw] subject
33
- # @return [Object]
34
- # @param [Object] value
28
+ # @return [String?]
29
+ # @param [String?] value
35
30
  #
36
31
  # @!attribute [rw] option_owner
37
- # @return [Object]
38
- # @param [Object] value
32
+ # @return [String?]
33
+ # @param [String?] value
39
34
  #
40
35
  # @!attribute [rw] generated
41
- # @return [Object]
42
- # @param [Object] value
36
+ # @return [Boolean]
37
+ # @param [Boolean] value
43
38
  #
44
39
  # @!attribute [rw] index
45
- # @return [Object]
46
- # @param [Object] value
40
+ # @return [Integer]
41
+ # @param [Integer] value
47
42
  Entry = Struct.new(
48
43
  :kind,
49
44
  :tag,
@@ -60,37 +55,28 @@ module Docscribe
60
55
  # Existing text is preserved exactly. If sorting is enabled, only sortable tag runs
61
56
  # are normalized according to the configured tag order.
62
57
  #
63
- # @note module_function: when included, also defines #merge (instance visibility: private)
58
+ # @note module_function: defines #merge (visibility: private)
64
59
  # @param [Array<String>] existing_lines existing doc block lines
65
60
  # @param [Array<String>] missing_lines generated tag lines to add
66
61
  # @param [Boolean] sort_tags whether sortable tags should be reordered
67
62
  # @param [Array<String>] tag_order configured sortable tag order
68
- # @param [Hash] filter_existing Param documentation.
63
+ # @param [Hash<Symbol, Object>] filter_existing tags to filter from existing block
69
64
  # @return [Array<String>]
70
65
  def merge(existing_lines, missing_lines:, sort_tags:, tag_order:, filter_existing: {})
71
66
  existing_entries = parse(existing_lines, tag_order: tag_order)
72
67
  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
-
68
+ existing_entries = filter_existing_entries(existing_entries, filter_existing)
82
69
  entries = existing_entries + missing_entries
83
70
  entries = sort(entries, tag_order: tag_order) if sort_tags
84
-
85
71
  render(entries)
86
72
  end
87
73
 
88
74
  # Parse generated missing tag lines and mark them as generated entries.
89
75
  #
90
- # @note module_function: when included, also defines #parse_generated (instance visibility: private)
76
+ # @note module_function: defines #parse_generated (visibility: private)
91
77
  # @param [Array<String>] lines generated lines
92
78
  # @param [Array<String>] tag_order configured sortable tag order
93
- # @return [Array<Entry>]
79
+ # @return [Array<Docscribe::InlineRewriter::DocBlock::Entry>]
94
80
  def parse_generated(lines, tag_order:)
95
81
  parse(lines, tag_order: tag_order).map do |entry|
96
82
  entry.generated = true if entry.kind == :tag
@@ -98,75 +84,154 @@ module Docscribe
98
84
  end
99
85
  end
100
86
 
87
+ # Remove existing entries matching the filter criteria (param names or return tag).
88
+ #
89
+ # @note module_function: defines #filter_existing_entries (visibility: private)
90
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries parsed existing entries
91
+ # @param [Hash<Symbol, Object>] filter_existing filter specification with :param_names and :return keys
92
+ # @return [Array<Docscribe::InlineRewriter::DocBlock::Entry>] filtered entries
93
+ def filter_existing_entries(entries, filter_existing)
94
+ filter_param_names = filter_existing[:param_names] || []
95
+ filter_return = !!filter_existing[:return]
96
+ entries.reject do |entry|
97
+ filter_param_entry?(entry, filter_param_names) || filter_return_entry?(entry, filter_return)
98
+ end
99
+ end
100
+
101
+ # Check whether an entry is a @param tag whose name is in the filter list.
102
+ #
103
+ # @note module_function: defines #filter_param_entry? (visibility: private)
104
+ # @param [Docscribe::InlineRewriter::DocBlock::Entry] entry the entry to check
105
+ # @param [Array<String>] param_names parameter names to filter
106
+ # @return [Boolean]
107
+ def filter_param_entry?(entry, param_names)
108
+ entry.kind == :tag && entry.tag == 'param' && param_names.include?(entry.subject)
109
+ end
110
+
111
+ # Check whether an entry is a @return tag that should be filtered.
112
+ #
113
+ # @note module_function: defines #filter_return_entry? (visibility: private)
114
+ # @param [Docscribe::InlineRewriter::DocBlock::Entry] entry the entry to check
115
+ # @param [Boolean] filter_return whether return tags should be filtered
116
+ # @return [Boolean]
117
+ def filter_return_entry?(entry, filter_return)
118
+ entry.kind == :tag && entry.tag == 'return' && filter_return
119
+ end
120
+
101
121
  # Parse a doc block into structured entries.
102
122
  #
103
123
  # Only tags listed in `tag_order` are treated as sortable tag entries.
104
124
  # Other lines become `:other` entries and act as sort boundaries.
105
125
  #
106
- # @note module_function: when included, also defines #parse (instance visibility: private)
126
+ # @note module_function: defines #parse (visibility: private)
107
127
  # @param [Array<String>] lines comment block lines
108
128
  # @param [Array<String>] tag_order configured sortable tag order
109
- # @return [Array<Entry>]
129
+ # @return [Array<Docscribe::InlineRewriter::DocBlock::Entry>]
110
130
  def parse(lines, tag_order:)
111
131
  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
132
+ parse_lines(lines, sortable_tags, entries: [], index: 0)
133
+ end
131
134
 
135
+ # Iterate through all lines and parse each one into a structured entry.
136
+ #
137
+ # @note module_function: defines #parse_lines (visibility: private)
138
+ # @param [Array<String>] lines comment block lines
139
+ # @param [Array<String>] sortable_tags tag names treated as sortable
140
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries accumulated parsed entries
141
+ # @param [Integer] index stable ordering index for entries
142
+ # @return [Array<Docscribe::InlineRewriter::DocBlock::Entry>]
143
+ def parse_lines(lines, sortable_tags, entries:, index:)
144
+ idx = 0
145
+ while idx < lines.length
146
+ idx = parse_one_line(lines, idx, sortable_tags, entries, index)
132
147
  index += 1
133
148
  end
134
-
135
149
  entries
136
150
  end
137
151
 
152
+ # Parse a single line as a sortable tag entry or non-tag content.
153
+ #
154
+ # @note module_function: defines #parse_one_line (visibility: private)
155
+ # @param [Array<String>] lines comment block lines
156
+ # @param [Integer] idx current line index
157
+ # @param [Array<String>] sortable_tags tag names treated as sortable
158
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries accumulated parsed entries
159
+ # @param [Integer] index stable ordering index for entries
160
+ # @return [Integer] next line index after parsing
161
+ def parse_one_line(lines, idx, sortable_tags, entries, index)
162
+ if sortable_top_level_tag_line?(lines[idx], sortable_tags)
163
+ entry, idx = consume_tag_entry(lines, idx, index: index, sortable_tags: sortable_tags)
164
+ entries << entry
165
+ else
166
+ entries << build_other_entry(lines[idx], index)
167
+ idx += 1
168
+ end
169
+ idx
170
+ end
171
+
172
+ # Create an :other entry for a non-tag line (prose, blank separators, etc.).
173
+ #
174
+ # @note module_function: defines #build_other_entry (visibility: private)
175
+ # @param [String] line the comment line
176
+ # @param [Integer] index stable ordering index
177
+ # @return [Docscribe::InlineRewriter::DocBlock::Entry]
178
+ def build_other_entry(line, index)
179
+ Entry.new(kind: :other, lines: [line], generated: false, index: index)
180
+ end
181
+
138
182
  # Sort parsed entries by configured tag order, preserving boundaries between tag runs.
139
183
  #
140
- # @note module_function: when included, also defines #sort (instance visibility: private)
141
- # @param [Array<Entry>] entries parsed entries
184
+ # @note module_function: defines #sort (visibility: private)
185
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries parsed entries
142
186
  # @param [Array<String>] tag_order configured sortable tag order
143
- # @return [Array<Entry>]
187
+ # @return [Array<Docscribe::InlineRewriter::DocBlock::Entry>]
144
188
  def sort(entries, tag_order:)
145
- out = []
189
+ out = [] #: Array[untyped]
146
190
  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
191
+ sort_loop(entries, out, priority)
192
+ out
193
+ end
194
+
195
+ # Iterate entries, sorting contiguous tag runs while preserving non-tag boundaries.
196
+ #
197
+ # @note module_function: defines #sort_loop (visibility: private)
198
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries parsed entries to sort
199
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] out output array for sorted entries
200
+ # @param [Hash<String, Integer>] priority tag priority map
201
+ # @return [void]
202
+ def sort_loop(entries, out, priority)
203
+ idx = 0
204
+
205
+ while idx < entries.length
206
+ if entries[idx].kind == :tag
207
+ run, idx = consume_tag_run(entries, idx)
156
208
  out.concat(sort_run(run, priority: priority))
157
209
  else
158
- out << entries[i]
159
- i += 1
210
+ out << entries[idx]
211
+ idx += 1
160
212
  end
161
213
  end
214
+ end
162
215
 
163
- out
216
+ # Collect a contiguous run of :tag entries starting at idx.
217
+ #
218
+ # @note module_function: defines #consume_tag_run (visibility: private)
219
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries parsed entries
220
+ # @param [Integer] idx start index
221
+ # @return [(Array<Docscribe::InlineRewriter::DocBlock::Entry>, Integer)]
222
+ def consume_tag_run(entries, idx)
223
+ run = [] #: Array[untyped]
224
+ while idx < entries.length && entries[idx].kind == :tag
225
+ run << entries[idx]
226
+ idx += 1
227
+ end
228
+ [run, idx]
164
229
  end
165
230
 
166
231
  # Render parsed entries back into comment lines.
167
232
  #
168
- # @note module_function: when included, also defines #render (instance visibility: private)
169
- # @param [Array<Entry>] entries
233
+ # @note module_function: defines #render (visibility: private)
234
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries contiguous tag run entries
170
235
  # @return [Array<String>]
171
236
  def render(entries)
172
237
  entries.flat_map(&:lines)
@@ -174,10 +239,10 @@ module Docscribe
174
239
 
175
240
  # Sort one contiguous run of sortable tag entries.
176
241
  #
177
- # @note module_function: when included, also defines #sort_run (instance visibility: private)
178
- # @param [Array<Entry>] entries contiguous tag run
179
- # @param [Hash{String=>Integer}] priority tag priority map
180
- # @return [Array<Entry>]
242
+ # @note module_function: defines #sort_run (visibility: private)
243
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries contiguous tag run
244
+ # @param [Hash<String, Integer>] priority tag priority map
245
+ # @return [Array<Docscribe::InlineRewriter::DocBlock::Entry>]
181
246
  def sort_run(entries, priority:)
182
247
  groups = build_groups(entries)
183
248
 
@@ -190,43 +255,72 @@ module Docscribe
190
255
 
191
256
  # Group entries so related `@option` tags stay attached to their owning `@param`.
192
257
  #
193
- # @note module_function: when included, also defines #build_groups (instance visibility: private)
194
- # @param [Array<Entry>] entries
195
- # @return [Array<Array<Entry>>]
258
+ # @note module_function: defines #build_groups (visibility: private)
259
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries contiguous tag run entries
260
+ # @return [Array<Array<Docscribe::InlineRewriter::DocBlock::Entry>>]
196
261
  def build_groups(entries)
197
- groups = []
198
- i = 0
199
-
200
- while i < entries.length
201
- entry = entries[i]
262
+ groups = [] #: Array[untyped]
263
+ group_entries_loop(entries, groups)
264
+ groups
265
+ end
202
266
 
203
- if entry.tag == 'param'
204
- group = [entry]
205
- i += 1
267
+ # Iterate entries to build sorted groups, attaching @option entries to their @param.
268
+ #
269
+ # @note module_function: defines #group_entries_loop (visibility: private)
270
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries contiguous tag run entries
271
+ # @param [Array<Array<Docscribe::InlineRewriter::DocBlock::Entry>>] groups accumulated groups
272
+ # @return [void]
273
+ def group_entries_loop(entries, groups)
274
+ idx = 0
275
+ idx = group_one_entry(entries, idx, groups) while idx < entries.length
276
+ end
206
277
 
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
278
+ # Group a single entry, creating a param group with @option children if applicable.
279
+ #
280
+ # @note module_function: defines #group_one_entry (visibility: private)
281
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries contiguous tag run entries
282
+ # @param [Integer] idx current entry index
283
+ # @param [Array<Array<Docscribe::InlineRewriter::DocBlock::Entry>>] groups accumulated groups
284
+ # @return [Integer] next index after processing the group
285
+ def group_one_entry(entries, idx, groups)
286
+ entry = entries[idx]
287
+ if entry.tag == 'param'
288
+ group = build_param_group(entries, idx, entry)
289
+ groups << group
290
+ idx + group.size
291
+ else
292
+ groups << [entry]
293
+ idx + 1
294
+ end
295
+ end
214
296
 
215
- groups << group
216
- else
217
- groups << [entry]
218
- i += 1
219
- end
297
+ # Build a group starting with a @param entry and including its following @option entries.
298
+ #
299
+ # @note module_function: defines #build_param_group (visibility: private)
300
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] entries contiguous tag run entries
301
+ # @param [Integer] idx index of the @param entry
302
+ # @param [Docscribe::InlineRewriter::DocBlock::Entry] entry the @param entry
303
+ # @return [Array<Docscribe::InlineRewriter::DocBlock::Entry>] the param group including @option children
304
+ def build_param_group(entries, idx, entry)
305
+ group = [entry]
306
+ idx += 1
307
+
308
+ while idx < entries.length &&
309
+ entries[idx].tag == 'option' &&
310
+ entries[idx].option_owner &&
311
+ entries[idx].option_owner == entry.subject
312
+ group << entries[idx]
313
+ idx += 1
220
314
  end
221
315
 
222
- groups
316
+ group
223
317
  end
224
318
 
225
319
  # Compute the priority of a grouped sortable unit.
226
320
  #
227
- # @note module_function: when included, also defines #group_priority (instance visibility: private)
228
- # @param [Array<Entry>] group
229
- # @param [Hash{String=>Integer}] priority
321
+ # @note module_function: defines #group_priority (visibility: private)
322
+ # @param [Array<Docscribe::InlineRewriter::DocBlock::Entry>] group tag group array
323
+ # @param [Hash<String, Integer>] priority tag priority map
230
324
  # @return [Integer]
231
325
  def group_priority(group, priority)
232
326
  priority.fetch(group.first.tag, priority.length)
@@ -234,17 +328,17 @@ module Docscribe
234
328
 
235
329
  # Build a tag priority map from configured order.
236
330
  #
237
- # @note module_function: when included, also defines #build_priority (instance visibility: private)
238
- # @param [Array<String>] tag_order
239
- # @return [Hash{String=>Integer}]
331
+ # @note module_function: defines #build_priority (visibility: private)
332
+ # @param [Array<String>] tag_order configured sortable tag order
333
+ # @return [Hash<String, Integer>]
240
334
  def build_priority(tag_order)
241
335
  normalized_tag_order(tag_order).each_with_index.to_h
242
336
  end
243
337
 
244
338
  # Normalize configured tag names by removing leading `@`.
245
339
  #
246
- # @note module_function: when included, also defines #normalized_tag_order (instance visibility: private)
247
- # @param [Array<String>] tag_order
340
+ # @note module_function: defines #normalized_tag_order (visibility: private)
341
+ # @param [Array<String>] tag_order configured sortable tag order
248
342
  # @return [Array<String>]
249
343
  def normalized_tag_order(tag_order)
250
344
  Array(tag_order).map { |t| t.to_s.sub(/\A@/, '') }
@@ -255,30 +349,76 @@ module Docscribe
255
349
  # Continuation lines are comment lines that belong to the same logical tag entry
256
350
  # until a new sortable tag line or a blank comment separator is encountered.
257
351
  #
258
- # @note module_function: when included, also defines #consume_tag_entry (instance visibility: private)
259
- # @param [Array<String>] lines
260
- # @param [Integer] start_idx
352
+ # @note module_function: defines #consume_tag_entry (visibility: private)
353
+ # @param [Array<String>] lines comment block lines
354
+ # @param [Integer] start_idx index to start scanning from
261
355
  # @param [Integer] index stable original index
262
- # @param [Array<String>] sortable_tags
263
- # @return [Array<(Entry, Integer)>] parsed entry and next index
356
+ # @param [Array<String>] sortable_tags tag names treated as sortable
357
+ # @return [(Docscribe::InlineRewriter::DocBlock::Entry, Integer)]
264
358
  def consume_tag_entry(lines, start_idx, index:, sortable_tags:)
265
359
  first = lines[start_idx]
266
- tag = extract_tag(first)
360
+ tag = extract_tag(first) || ''
361
+ entry_lines = collect_continuation_lines(lines, start_idx + 1, first, sortable_tags)
362
+ i = entry_lines.length + start_idx
363
+ entry = build_tag_entry(first, tag, entry_lines, index)
364
+ [entry, i]
365
+ end
267
366
 
268
- entry_lines = [first]
269
- i = start_idx + 1
367
+ # Collect the first tag line and all continuation lines belonging to the same entry.
368
+ #
369
+ # @note module_function: defines #collect_continuation_lines (visibility: private)
370
+ # @param [Array<String>] lines comment block lines
371
+ # @param [Integer] start_idx index after the tag line
372
+ # @param [String] first the tag line itself
373
+ # @param [Array<String>] sortable_tags tag names treated as sortable
374
+ # @return [Array<String>] all lines belonging to this entry
375
+ def collect_continuation_lines(lines, start_idx, first, sortable_tags)
376
+ result = [first]
377
+ add_continuation_lines(lines, start_idx, result, sortable_tags)
378
+ result
379
+ end
270
380
 
381
+ # Append continuation lines to the result array until a non-continuation line is found.
382
+ #
383
+ # @note module_function: defines #add_continuation_lines (visibility: private)
384
+ # @param [Array<String>] lines comment block lines
385
+ # @param [Integer] start_idx index to start scanning from
386
+ # @param [Array<String>] result accumulated entry lines
387
+ # @param [Array<String>] sortable_tags tag names treated as sortable
388
+ # @return [void]
389
+ def add_continuation_lines(lines, start_idx, result, sortable_tags)
390
+ i = start_idx
271
391
  while i < lines.length
272
392
  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)
393
+ break unless continuation_candidate?(line, sortable_tags)
276
394
 
277
- entry_lines << line
395
+ result << line
278
396
  i += 1
279
397
  end
398
+ end
280
399
 
281
- entry = Entry.new(
400
+ # Check whether a line can serve as a continuation of the current tag entry.
401
+ #
402
+ # @note module_function: defines #continuation_candidate? (visibility: private)
403
+ # @param [String] line the line to check
404
+ # @param [Array<String>] sortable_tags tag names treated as sortable
405
+ # @return [Boolean]
406
+ def continuation_candidate?(line, sortable_tags)
407
+ !sortable_top_level_tag_line?(line, sortable_tags) &&
408
+ !blank_comment_line?(line) &&
409
+ continuation_comment_line?(line)
410
+ end
411
+
412
+ # Build a tag Entry struct with metadata from the parsed tag line and continuation lines.
413
+ #
414
+ # @note module_function: defines #build_tag_entry (visibility: private)
415
+ # @param [String] first the first (tag) line
416
+ # @param [String] tag the extracted tag name
417
+ # @param [Array<String>] entry_lines all lines belonging to this entry
418
+ # @param [Integer] index stable ordering index
419
+ # @return [Docscribe::InlineRewriter::DocBlock::Entry]
420
+ def build_tag_entry(first, tag, entry_lines, index)
421
+ Entry.new(
282
422
  kind: :tag,
283
423
  tag: tag,
284
424
  lines: entry_lines,
@@ -287,18 +427,16 @@ module Docscribe
287
427
  generated: false,
288
428
  index: index
289
429
  )
290
-
291
- [entry, i]
292
430
  end
293
431
 
294
432
  # Extract the grouping subject for a sortable tag.
295
433
  #
296
434
  # Currently only `@param` entries carry a subject, used to keep `@option` tags attached.
297
435
  #
298
- # @note module_function: when included, also defines #extract_subject (instance visibility: private)
299
- # @param [String] line
300
- # @param [String] tag
301
- # @return [String, nil]
436
+ # @note module_function: defines #extract_subject (visibility: private)
437
+ # @param [String] line the line to check
438
+ # @param [String?] tag the extracted tag name
439
+ # @return [String?]
302
440
  def extract_subject(line, tag)
303
441
  case tag
304
442
  when 'param'
@@ -312,30 +450,65 @@ module Docscribe
312
450
  # - `@param [Type] name`
313
451
  # - `@param name [Type]`
314
452
  #
315
- # @note module_function: when included, also defines #extract_param_name (instance visibility: private)
316
- # @param [String] line
317
- # @return [String, nil]
453
+ # @note module_function: defines #extract_param_name (visibility: private)
454
+ # @param [String] line the line to check
455
+ # @return [String?]
318
456
  def extract_param_name(line)
319
- return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+\[[^\]]+\]\s+(\S+)/
320
- return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+(\S+)\s+\[[^\]]+\]/
457
+ content = line.sub(/^\s*#\s*/, '')
458
+ if (m = content.match(/@param\s+(\S+)\s+\[/))
459
+ return m[1]
460
+ elsif (m = content.match(/@param\s+\[/))
461
+ end0 = m.end(0) #: Integer
462
+ rest = content[(end0 - 1)..] #: String
463
+ type_end = matching_close_bracket(rest)
464
+ return name_after_bracket(rest, type_end) if type_end
465
+ end
466
+
467
+ nil
468
+ end
321
469
 
470
+ # Extract name after type bracket
471
+ #
472
+ # @note module_function: defines #name_after_bracket (visibility: private)
473
+ # @param [String] rest remaining tag content
474
+ # @param [Integer] type_end closing bracket position
475
+ # @return [String?]
476
+ def name_after_bracket(rest, type_end)
477
+ rest[(type_end + 1)..].to_s.strip.split(/\s+/).first
478
+ end
479
+
480
+ # Find the index of the matching close bracket for an outermost `[`.
481
+ #
482
+ # @note module_function: defines #matching_close_bracket (visibility: private)
483
+ # @param [String] str string to scan
484
+ # @return [Integer?]
485
+ def matching_close_bracket(str)
486
+ depth = 0
487
+ str.each_char.with_index do |c, i|
488
+ case c
489
+ when '[' then depth += 1
490
+ when ']'
491
+ depth -= 1
492
+ return i if depth.zero?
493
+ end
494
+ end
322
495
  nil
323
496
  end
324
497
 
325
498
  # Extract the owning options-hash param name from an `@option` line.
326
499
  #
327
- # @note module_function: when included, also defines #extract_option_owner (instance visibility: private)
328
- # @param [String] line
329
- # @return [String, nil]
500
+ # @note module_function: defines #extract_option_owner (visibility: private)
501
+ # @param [String] line the line to check
502
+ # @return [String?]
330
503
  def extract_option_owner(line)
331
504
  line[/^\s*#\s*@option\b\s+(\S+)/, 1]
332
505
  end
333
506
 
334
507
  # Whether a line is a sortable top-level tag line.
335
508
  #
336
- # @note module_function: when included, also defines #sortable_top_level_tag_line? (instance visibility: private)
337
- # @param [String] line
338
- # @param [Array<String>] sortable_tags
509
+ # @note module_function: defines #sortable_top_level_tag_line? (visibility: private)
510
+ # @param [String] line the line to check
511
+ # @param [Array<String>] sortable_tags tag names treated as sortable
339
512
  # @return [Boolean]
340
513
  def sortable_top_level_tag_line?(line, sortable_tags)
341
514
  return false unless top_level_tag_line?(line)
@@ -345,17 +518,17 @@ module Docscribe
345
518
 
346
519
  # Extract a top-level tag name without the leading `@`.
347
520
  #
348
- # @note module_function: when included, also defines #extract_tag (instance visibility: private)
349
- # @param [String] line
350
- # @return [String, nil]
521
+ # @note module_function: defines #extract_tag (visibility: private)
522
+ # @param [String] line the line to check
523
+ # @return [String?]
351
524
  def extract_tag(line)
352
525
  line[/^\s*#\s*@(\w+)/, 1]
353
526
  end
354
527
 
355
528
  # Whether a line begins a top-level YARD-style tag.
356
529
  #
357
- # @note module_function: when included, also defines #top_level_tag_line? (instance visibility: private)
358
- # @param [String] line
530
+ # @note module_function: defines #top_level_tag_line? (visibility: private)
531
+ # @param [String] line the line to check
359
532
  # @return [Boolean]
360
533
  def top_level_tag_line?(line)
361
534
  !!(line =~ /^\s*#\s*@\w+/)
@@ -363,8 +536,8 @@ module Docscribe
363
536
 
364
537
  # Whether a line is any comment line.
365
538
  #
366
- # @note module_function: when included, also defines #comment_line? (instance visibility: private)
367
- # @param [String] line
539
+ # @note module_function: defines #comment_line? (visibility: private)
540
+ # @param [String] line the line to check
368
541
  # @return [Boolean]
369
542
  def comment_line?(line)
370
543
  !!(line =~ /^\s*#/)
@@ -372,8 +545,8 @@ module Docscribe
372
545
 
373
546
  # Whether a line is a blank comment separator such as `#`.
374
547
  #
375
- # @note module_function: when included, also defines #blank_comment_line? (instance visibility: private)
376
- # @param [String] line
548
+ # @note module_function: defines #blank_comment_line? (visibility: private)
549
+ # @param [String] line the line to check
377
550
  # @return [Boolean]
378
551
  def blank_comment_line?(line)
379
552
  !!(line =~ /^\s*#\s*$/)
@@ -381,8 +554,8 @@ module Docscribe
381
554
 
382
555
  # Whether a comment line should be treated as a continuation of the previous tag entry.
383
556
  #
384
- # @note module_function: when included, also defines #continuation_comment_line? (instance visibility: private)
385
- # @param [String] line
557
+ # @note module_function: defines #continuation_comment_line? (visibility: private)
558
+ # @param [String] line the line to check
386
559
  # @return [Boolean]
387
560
  def continuation_comment_line?(line)
388
561
  !!(line =~ /^\s*#[ \t]{2,}\S/)