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,8 +12,8 @@ module Docscribe
12
12
 
13
13
  # Extract the method name from a `:def` or `:defs` node.
14
14
  #
15
- # @note module_function: when included, also defines #node_name (instance visibility: private)
16
- # @param [Parser::AST::Node] node
15
+ # @note module_function: defines #node_name (visibility: private)
16
+ # @param [Parser::AST::Node] node def or defs AST node
17
17
  # @return [Symbol, nil]
18
18
  def node_name(node)
19
19
  case node.type
@@ -26,9 +26,9 @@ module Docscribe
26
26
  #
27
27
  # Used as the insertion point for generated documentation.
28
28
  #
29
- # @note module_function: when included, also defines #line_start_range (instance visibility: private)
30
- # @param [Parser::Source::Buffer] buffer
31
- # @param [Parser::AST::Node] node
29
+ # @note module_function: defines #line_start_range (visibility: private)
30
+ # @param [Parser::Source::Buffer] buffer source buffer for range
31
+ # @param [Parser::AST::Node] node target AST node
32
32
  # @return [Parser::Source::Range]
33
33
  def line_start_range(buffer, node)
34
34
  start_pos = node.loc.expression.begin_pos
@@ -47,49 +47,22 @@ module Docscribe
47
47
  #
48
48
  # Returns nil if no doc-like block is present.
49
49
  #
50
- # @note module_function: when included, also defines #doc_comment_block_info (instance visibility: private)
51
- # @param [Parser::Source::Buffer] buffer
50
+ # @note module_function: defines #doc_comment_block_info (visibility: private)
51
+ # @param [Parser::Source::Buffer] buffer source buffer to scan
52
52
  # @param [Integer] def_bol_pos beginning-of-line position of the target def
53
- # @return [Hash, nil]
53
+ # @return [Hash<Symbol, Array<String>, Integer, nil>, nil]
54
54
  def doc_comment_block_info(buffer, def_bol_pos)
55
- src = buffer.source
56
- lines = src.lines
57
- def_line_idx = src[0...def_bol_pos].count("\n")
58
- i = def_line_idx - 1
59
-
60
- # Skip blank lines directly above def
61
- i -= 1 while i >= 0 && lines[i].strip.empty?
62
-
63
- # Nearest non-blank line must be a comment
64
- return nil unless i >= 0 && lines[i] =~ /^\s*#/
65
-
66
- # Walk upward to include the entire contiguous comment block
67
- end_idx = i
68
- start_idx = i
69
- start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
70
- start_idx += 1
55
+ lines = buffer.source.lines
56
+ def_line_idx = (buffer.source[0...def_bol_pos] || '').count("\n")
57
+ block_range = find_comment_block_range(lines, def_line_idx)
58
+ return nil unless block_range
71
59
 
72
- # Preserve leading directive-style comments
73
- removable_start_idx = start_idx
74
- removable_start_idx += 1 while removable_start_idx <= end_idx && preserved_comment_line?(lines[removable_start_idx])
60
+ start_idx = block_range[:start_idx]
61
+ end_idx = block_range[:end_idx]
62
+ preserved_start_idx = find_preserved_start_idx(lines, start_idx, end_idx)
63
+ return nil unless doc_marker?(lines, preserved_start_idx..end_idx)
75
64
 
76
- return nil if removable_start_idx > end_idx
77
-
78
- remaining = lines[removable_start_idx..end_idx]
79
- return nil unless remaining.any? { |line| doc_marker_line?(line) }
80
-
81
- start_pos = start_idx.positive? ? lines[0...start_idx].join.length : 0
82
- doc_start_pos = removable_start_idx.positive? ? lines[0...removable_start_idx].join.length : 0
83
- end_pos = lines[0..end_idx].join.length
84
-
85
- {
86
- lines: lines[start_idx..end_idx],
87
- preserved_lines: lines[start_idx...removable_start_idx],
88
- doc_lines: lines[removable_start_idx..end_idx],
89
- start_pos: start_pos,
90
- doc_start_pos: doc_start_pos,
91
- end_pos: end_pos
92
- }
65
+ build_block_info(lines, start_idx, preserved_start_idx, end_idx)
93
66
  end
94
67
 
95
68
  # Compute the removable range for an existing doc-like block above a method.
@@ -97,42 +70,116 @@ module Docscribe
97
70
  # Preserved directive lines (such as RuboCop directives or magic comments) are excluded
98
71
  # from the returned range.
99
72
  #
100
- # @note module_function: when included, also defines #comment_block_removal_range (instance visibility: private)
101
- # @param [Parser::Source::Buffer] buffer
73
+ # @note module_function: defines #comment_block_removal_range (visibility: private)
74
+ # @param [Parser::Source::Buffer] buffer source buffer to scan
102
75
  # @param [Integer] def_bol_pos beginning-of-line position of the target def
103
76
  # @return [Parser::Source::Range, nil]
104
77
  def comment_block_removal_range(buffer, def_bol_pos)
105
78
  src = buffer.source
106
79
  lines = src.lines
107
- def_line_idx = src[0...def_bol_pos].count("\n")
80
+ def_line_idx = (src[0...def_bol_pos] || '').count("\n")
81
+ block_range = find_comment_block_range(lines, def_line_idx)
82
+ return nil unless block_range
83
+
84
+ preserved_start_idx = find_preserved_start_idx(lines, block_range[:start_idx], block_range[:end_idx])
85
+ return nil unless doc_marker?(lines, preserved_start_idx..block_range[:end_idx])
86
+
87
+ compute_removal_range(buffer, lines, preserved_start_idx, def_bol_pos)
88
+ end
89
+
90
+ # Find the range of a contiguous comment block directly above a method definition.
91
+ #
92
+ # Walks upward from def_line_idx, skipping blank lines, then includes all
93
+ # contiguous comment lines.
94
+ #
95
+ # @note module_function: defines #find_comment_block_range (visibility: private)
96
+ # @param [Array<String>] lines source code lines
97
+ # @param [Integer] def_line_idx def line index
98
+ # @return [Hash<Symbol, Integer>, nil]
99
+ def find_comment_block_range(lines, def_line_idx)
108
100
  i = def_line_idx - 1
109
101
 
110
- # Skip blank lines directly above def
111
102
  i -= 1 while i >= 0 && lines[i].strip.empty?
112
-
113
- # Nearest non-blank line must be a comment to remove anything
114
103
  return nil unless i >= 0 && lines[i] =~ /^\s*#/
115
104
 
116
- # Walk upward to include the entire contiguous comment block
117
105
  start_idx = i
118
106
  start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
119
107
  start_idx += 1
120
108
 
121
- # Preserve leading directive-style comments (currently: rubocop directives)
122
- removable_start_idx = start_idx
123
- removable_start_idx += 1 while removable_start_idx <= i && preserved_comment_line?(lines[removable_start_idx])
109
+ { start_idx: start_idx, end_idx: i }
110
+ end
124
111
 
125
- # If the whole block is preserved directives, there is nothing to remove
126
- return nil if removable_start_idx > i
112
+ # Find the first index in a comment block after preserved directive-style lines.
113
+ #
114
+ # Preserved lines include RuboCop directives and Ruby magic comments.
115
+ #
116
+ # @note module_function: defines #find_preserved_start_idx (visibility: private)
117
+ # @param [Array<String>] lines source code lines
118
+ # @param [Integer] start_idx block start index
119
+ # @param [Integer] end_idx block end index
120
+ # @return [Integer]
121
+ def find_preserved_start_idx(lines, start_idx, end_idx)
122
+ idx = start_idx
123
+ idx += 1 while idx <= end_idx && preserved_comment_line?(lines[idx])
124
+ idx
125
+ end
127
126
 
128
- # SAFETY: only remove if the remaining block looks like documentation
129
- remaining = lines[removable_start_idx..i]
130
- return nil unless remaining.any? { |line| doc_marker_line?(line) }
127
+ # Whether a comment block range contains documentation markers.
128
+ #
129
+ # @note module_function: defines #doc_marker? (visibility: private)
130
+ # @param [Array<String>] lines source code lines
131
+ # @param [Range<Integer>] range line index range
132
+ # @return [Boolean]
133
+ def doc_marker?(lines, range)
134
+ (lines[range] || []).any? { |line| doc_marker_line?(line) }
135
+ end
131
136
 
132
- start_pos = removable_start_idx.positive? ? lines[0...removable_start_idx].join.length : 0
137
+ # Build block info hash from computed line ranges.
138
+ #
139
+ # @note module_function: defines #build_block_info (visibility: private)
140
+ # @param [Array<String>] lines source code lines
141
+ # @param [Integer] start_idx block start index
142
+ # @param [Integer] preserved_start_idx preserved start index
143
+ # @param [Integer] end_idx block end index
144
+ # @return [Hash<Symbol, Array<String>, Integer, nil>]
145
+ def build_block_info(lines, start_idx, preserved_start_idx, end_idx)
146
+ positions = compute_positions(lines, start_idx, preserved_start_idx, end_idx)
147
+ {
148
+ lines: lines[start_idx..end_idx],
149
+ preserved_lines: lines[start_idx...preserved_start_idx],
150
+ doc_lines: lines[preserved_start_idx..end_idx],
151
+ **positions
152
+ }
153
+ end
154
+
155
+ # Compute the removal range for preserved start position.
156
+ #
157
+ # @note module_function: defines #compute_removal_range (visibility: private)
158
+ # @param [Parser::Source::Buffer] buffer source buffer for position
159
+ # @param [Array<String>] lines source code lines
160
+ # @param [Integer] preserved_start_idx preserved start index
161
+ # @param [Integer] def_bol_pos beginning-of-line position of the target def
162
+ # @return [Parser::Source::Range]
163
+ def compute_removal_range(buffer, lines, preserved_start_idx, def_bol_pos)
164
+ start_pos = preserved_start_idx.positive? ? (lines[0...preserved_start_idx] || []).join.length : 0
133
165
  Parser::Source::Range.new(buffer, start_pos, def_bol_pos)
134
166
  end
135
167
 
168
+ # Compute source positions for a comment block.
169
+ #
170
+ # @note module_function: defines #compute_positions (visibility: private)
171
+ # @param [Array<String>] lines source code lines
172
+ # @param [Integer] start_idx block start index
173
+ # @param [Integer] doc_start_idx doc content start index
174
+ # @param [Integer] end_pos_idx block end position index
175
+ # @return [Hash<Symbol, Integer>]
176
+ def compute_positions(lines, start_idx, doc_start_idx, end_pos_idx)
177
+ start_pos = start_idx.positive? ? (lines[0...start_idx] || []).join.length : 0
178
+ doc_start_pos = doc_start_idx.positive? ? (lines[0...doc_start_idx] || []).join.length : 0
179
+ end_pos = (lines[0..end_pos_idx] || []).join.length
180
+ { start_pos: start_pos, doc_start_pos: doc_start_pos, end_pos: end_pos }
181
+ end
182
+
136
183
  # Whether a comment line should be preserved during aggressive replacement.
137
184
  #
138
185
  # Preserved lines include:
@@ -140,8 +187,8 @@ module Docscribe
140
187
  # - Ruby magic comments
141
188
  # - tool directives such as `:nocov:` / `:stopdoc:`
142
189
  #
143
- # @note module_function: when included, also defines #preserved_comment_line? (instance visibility: private)
144
- # @param [String] line
190
+ # @note module_function: defines #preserved_comment_line? (visibility: private)
191
+ # @param [String] line comment line to check
145
192
  # @return [Boolean]
146
193
  def preserved_comment_line?(line)
147
194
  # RuboCop directives
@@ -166,8 +213,8 @@ module Docscribe
166
213
  # - Docscribe header lines
167
214
  # - YARD tags/directives beginning with `@`
168
215
  #
169
- # @note module_function: when included, also defines #doc_marker_line? (instance visibility: private)
170
- # @param [String] line
216
+ # @note module_function: defines #doc_marker_line? (visibility: private)
217
+ # @param [String] line comment line to check
171
218
  # @return [Boolean]
172
219
  def doc_marker_line?(line)
173
220
  # Docscribe header line:
@@ -191,14 +238,14 @@ module Docscribe
191
238
  #
192
239
  # This helper is retained for compatibility/legacy behavior checks.
193
240
  #
194
- # @note module_function: when included, also defines #already_has_doc_immediately_above? (instance visibility: private)
195
- # @param [Parser::Source::Buffer] buffer
196
- # @param [Integer] insert_pos
241
+ # @note module_function: defines #already_has_doc_immediately_above? (visibility: private)
242
+ # @param [Parser::Source::Buffer] buffer source buffer to check
243
+ # @param [Integer] insert_pos insertion position
197
244
  # @return [Boolean]
198
245
  def already_has_doc_immediately_above?(buffer, insert_pos)
199
246
  src = buffer.source
200
247
  lines = src.lines
201
- current_line_index = src[0...insert_pos].count("\n")
248
+ current_line_index = (src[0...insert_pos] || '').count("\n")
202
249
  i = current_line_index - 1
203
250
  i -= 1 while i >= 0 && lines[i].strip.empty?
204
251
  return false if i.negative?
@@ -210,10 +257,11 @@ module Docscribe
210
257
  #
211
258
  # Tabs and spaces are preserved exactly.
212
259
  #
213
- # @note module_function: when included, also defines #line_indent (instance visibility: private)
214
- # @param [Parser::AST::Node] node
260
+ # @note module_function: defines #line_indent (visibility: private)
261
+ # @param [Parser::AST::Node] node target AST node
215
262
  # @raise [StandardError]
216
- # @return [String]
263
+ # @return [String] if StandardError
264
+ # @return [String] if StandardError
217
265
  def line_indent(node)
218
266
  line = node.loc.expression.source_line
219
267
  return '' unless line
@@ -11,33 +11,30 @@ module Docscribe
11
11
  module TagSorter
12
12
  module_function
13
13
 
14
- # One sortable top-level tag entry plus its continuation lines.
15
14
  # @!attribute [rw] tag
16
- # @return [Object]
17
- # @param [Object] value
15
+ # @return [String]
16
+ # @param [String] value
18
17
  #
19
18
  # @!attribute [rw] lines
20
- # @return [Object]
21
- # @param [Object] value
19
+ # @return [Array<String>]
20
+ # @param [Array<String>] value
22
21
  #
23
22
  # @!attribute [rw] param_name
24
- # @return [Object]
25
- # @param [Object] value
23
+ # @return [String?]
24
+ # @param [String?] value
26
25
  #
27
26
  # @!attribute [rw] option_owner
28
- # @return [Object]
29
- # @param [Object] value
27
+ # @return [String?]
28
+ # @param [String?] value
30
29
  #
31
30
  # @!attribute [rw] index
32
- # @return [Object]
33
- # @param [Object] value
31
+ # @return [Integer]
32
+ # @param [Integer] value
34
33
  Entry = Struct.new(:tag, :lines, :param_name, :option_owner, :index, keyword_init: true)
35
34
 
36
- # Sort contiguous top-level tag runs according to configured tag order.
35
+ # Sort
37
36
  #
38
- # Non-tag content is preserved as-is and acts as a sort boundary.
39
- #
40
- # @note module_function: when included, also defines #sort (instance visibility: private)
37
+ # @note module_function: defines #sort (visibility: private)
41
38
  # @param [Array<String>] lines comment block lines
42
39
  # @param [Array<String>] tag_order configured tag order
43
40
  # @return [Array<String>]
@@ -47,50 +44,69 @@ module Docscribe
47
44
  segments.flat_map { |seg| sort_segment(seg, priority: priority) }
48
45
  end
49
46
 
50
- # Build a tag priority map from configured tag order.
47
+ # Build priority
51
48
  #
52
- # @note module_function: when included, also defines #build_priority (instance visibility: private)
53
- # @param [Array<String>] tag_order
54
- # @return [Hash{String=>Integer}]
49
+ # @note module_function: defines #build_priority (visibility: private)
50
+ # @param [Array<String>] tag_order configured tag order
51
+ # @return [Hash<String, Integer>]
55
52
  def build_priority(tag_order)
56
53
  Array(tag_order).map { |t| t.to_s.sub(/\A@/, '') }
57
54
  .each_with_index
58
55
  .to_h
59
56
  end
60
57
 
61
- # Parse lines into sortable tag-run segments and non-sortable segments.
58
+ # Parse segments
62
59
  #
63
- # @note module_function: when included, also defines #parse_segments (instance visibility: private)
64
- # @param [Array<String>] lines
65
- # @return [Array<Hash>]
60
+ # @note module_function: defines #parse_segments (visibility: private)
61
+ # @param [Array<String>] lines comment block lines
62
+ # @return [Array<Hash<Symbol, Object>>]
66
63
  def parse_segments(lines)
67
- segments = []
64
+ segments = [] #: Array[untyped]
68
65
  i = 0
69
66
 
70
- while i < lines.length
71
- line = lines[i]
67
+ i = advance_parse(lines, i, segments) while i < lines.length
68
+
69
+ segments
70
+ end
72
71
 
73
- if top_level_tag_line?(line)
74
- entries = []
75
- while i < lines.length && top_level_tag_line?(lines[i])
76
- entry, i = consume_entry(lines, i)
77
- entries << entry
78
- end
79
- segments << { type: :tag_run, entries: entries }
80
- else
81
- segments << { type: :other, lines: [line] }
82
- i += 1
83
- end
72
+ # Advance parse
73
+ #
74
+ # @note module_function: defines #advance_parse (visibility: private)
75
+ # @param [Array<String>] lines comment block lines
76
+ # @param [Integer] idx current parse index
77
+ # @param [Array<Hash<Symbol, Object>>] segments accumulated parsed segments
78
+ # @return [Integer] new index after processing
79
+ def advance_parse(lines, idx, segments)
80
+ if top_level_tag_line?(lines[idx])
81
+ consume_tag_run(lines, idx, segments)
82
+ else
83
+ segments << { type: :other, lines: [lines[idx]] }
84
+ idx + 1
84
85
  end
86
+ end
85
87
 
86
- segments
88
+ # Consume tag run
89
+ #
90
+ # @note module_function: defines #consume_tag_run (visibility: private)
91
+ # @param [Array<String>] lines comment block lines
92
+ # @param [Integer] idx current index
93
+ # @param [Array<Hash<Symbol, Object>>] segments accumulated segments
94
+ # @return [Integer] new index after consuming the run
95
+ def consume_tag_run(lines, idx, segments)
96
+ entries = [] #: Array[untyped]
97
+ while idx < lines.length && top_level_tag_line?(lines[idx])
98
+ entry, idx = consume_entry(lines, idx)
99
+ entries << entry
100
+ end
101
+ segments << { type: :tag_run, entries: entries }
102
+ idx
87
103
  end
88
104
 
89
- # Sort one parsed segment if it is a tag run.
105
+ # Sort segment
90
106
  #
91
- # @note module_function: when included, also defines #sort_segment (instance visibility: private)
92
- # @param [Hash] segment
93
- # @param [Hash{String=>Integer}] priority
107
+ # @note module_function: defines #sort_segment (visibility: private)
108
+ # @param [Hash<Symbol, Object>] segment parsed comment segment
109
+ # @param [Hash<String, Integer>] priority tag priority map
94
110
  # @return [Array<String>]
95
111
  def sort_segment(segment, priority:)
96
112
  return segment[:lines] unless segment[:type] == :tag_run
@@ -104,125 +120,167 @@ module Docscribe
104
120
  .flat_map(&:lines)
105
121
  end
106
122
 
107
- # Compute sort priority for a grouped tag entry.
123
+ # Group priority
108
124
  #
109
- # @note module_function: when included, also defines #group_priority (instance visibility: private)
110
- # @param [Array<Entry>] group
111
- # @param [Hash{String=>Integer}] priority
125
+ # @note module_function: defines #group_priority (visibility: private)
126
+ # @param [Array<Docscribe::InlineRewriter::TagSorter::Entry>] group entry group array
127
+ # @param [Hash<String, Integer>] priority tag priority map
112
128
  # @return [Integer]
113
129
  def group_priority(group, priority)
114
130
  first = group.first
115
131
  priority.fetch(first.tag, priority.length)
116
132
  end
117
133
 
118
- # Consume one top-level tag entry and its continuation lines.
134
+ # Consume entry
119
135
  #
120
- # @note module_function: when included, also defines #consume_entry (instance visibility: private)
121
- # @param [Array<String>] lines
122
- # @param [Integer] start_idx
123
- # @return [Array<(Entry, Integer)>]
136
+ # @note module_function: defines #consume_entry (visibility: private)
137
+ # @param [Array<String>] lines comment block lines
138
+ # @param [Integer] start_idx original index of the first line
139
+ # @return [(Docscribe::InlineRewriter::TagSorter::Entry, Integer)]
124
140
  def consume_entry(lines, start_idx)
125
141
  first = lines[start_idx]
126
- tag = extract_tag_name(first)
127
- entry_lines = [first]
128
- i = start_idx + 1
129
-
130
- while i < lines.length
131
- line = lines[i]
142
+ tag = extract_tag_name(first) || ''
143
+ entry_lines = collect_continuation_lines(lines, start_idx + 1)
144
+ i = entry_lines.length + start_idx
132
145
 
133
- break if top_level_tag_line?(line)
134
- break if blank_comment_line?(line)
135
- break unless comment_line?(line)
146
+ entry = build_entry(tag, entry_lines, first, start_idx)
136
147
 
137
- entry_lines << line
138
- i += 1
139
- end
148
+ [entry, i]
149
+ end
140
150
 
141
- entry = Entry.new(
151
+ # Build entry
152
+ #
153
+ # @note module_function: defines #build_entry (visibility: private)
154
+ # @param [String] tag the extracted tag name
155
+ # @param [Array<String>] entry_lines all lines belonging to this entry
156
+ # @param [String] first the first (tag) line
157
+ # @param [Integer] start_idx original index of the first line
158
+ # @return [Docscribe::InlineRewriter::TagSorter::Entry]
159
+ def build_entry(tag, entry_lines, first, start_idx)
160
+ Entry.new(
142
161
  tag: tag,
143
162
  lines: entry_lines,
144
163
  param_name: extract_param_name(first),
145
164
  option_owner: extract_option_owner(first),
146
165
  index: start_idx
147
166
  )
167
+ end
148
168
 
149
- [entry, i]
169
+ # Collect continuation lines
170
+ #
171
+ # @note module_function: defines #collect_continuation_lines (visibility: private)
172
+ # @param [Array<String>] lines comment block lines
173
+ # @param [Integer] start_idx original index of the first line
174
+ # @return [Array<String>]
175
+ def collect_continuation_lines(lines, start_idx)
176
+ result = [] #: Array[String]
177
+ i = start_idx
178
+
179
+ while i < lines.length
180
+ line = lines[i]
181
+ break if top_level_tag_line?(line) || blank_comment_line?(line) || !comment_line?(line)
182
+
183
+ result << line
184
+ i += 1
185
+ end
186
+
187
+ result
150
188
  end
151
189
 
152
- # Group entries so `@option` tags remain attached to their owning `@param`.
190
+ # Group entries
153
191
  #
154
- # @note module_function: when included, also defines #group_entries (instance visibility: private)
155
- # @param [Array<Entry>] entries
156
- # @return [Array<Array<Entry>>]
192
+ # @note module_function: defines #group_entries (visibility: private)
193
+ # @param [Array<Docscribe::InlineRewriter::TagSorter::Entry>] entries parsed tag entries
194
+ # @return [Array<Array<Docscribe::InlineRewriter::TagSorter::Entry>>]
157
195
  def group_entries(entries)
158
- groups = []
196
+ groups = [] #: Array[untyped]
159
197
  i = 0
160
198
 
161
199
  while i < entries.length
162
- entry = entries[i]
163
-
164
- if entry.tag == 'param'
165
- group = [entry]
166
- i += 1
167
-
168
- while i < entries.length &&
169
- entries[i].tag == 'option' &&
170
- entries[i].option_owner &&
171
- entries[i].option_owner == entry.param_name
172
- group << entries[i]
173
- i += 1
174
- end
175
-
176
- groups << group
177
- else
178
- groups << [entry]
179
- i += 1
180
- end
200
+ groups << group_entry(entries, i)
201
+ i += 1
181
202
  end
182
203
 
183
204
  groups
184
205
  end
185
206
 
186
- # Whether a line begins a top-level tag entry.
207
+ # Group entry
208
+ #
209
+ # @note module_function: defines #group_entry (visibility: private)
210
+ # @param [Array<Docscribe::InlineRewriter::TagSorter::Entry>] entries parsed tag entries
211
+ # @param [Integer] idx index of the entry to group
212
+ # @return [Array<Docscribe::InlineRewriter::TagSorter::Entry>] the entry group
213
+ def group_entry(entries, idx)
214
+ entry = entries[idx]
215
+ if entry.tag == 'param'
216
+ [entry] + collect_option_entries(entries, idx + 1, entry.param_name || '')
217
+ else
218
+ [entry]
219
+ end
220
+ end
221
+
222
+ # Collect option entries
223
+ #
224
+ # @note module_function: defines #collect_option_entries (visibility: private)
225
+ # @param [Array<Docscribe::InlineRewriter::TagSorter::Entry>] entries parsed tag entries
226
+ # @param [Integer] start_idx original index of the first line
227
+ # @param [String] param_name parent param name
228
+ # @return [Array<Docscribe::InlineRewriter::TagSorter::Entry>]
229
+ def collect_option_entries(entries, start_idx, param_name)
230
+ result = [] #: Array[untyped]
231
+ i = start_idx
232
+
233
+ while i < entries.length &&
234
+ entries[i].tag == 'option' &&
235
+ entries[i].option_owner &&
236
+ entries[i].option_owner == param_name
237
+ result << entries[i]
238
+ i += 1
239
+ end
240
+
241
+ result
242
+ end
243
+
244
+ # Top level tag line
187
245
  #
188
- # @note module_function: when included, also defines #top_level_tag_line? (instance visibility: private)
189
- # @param [String] line
246
+ # @note module_function: defines #top_level_tag_line? (visibility: private)
247
+ # @param [String] line comment line to check
190
248
  # @return [Boolean]
191
249
  def top_level_tag_line?(line)
192
250
  !!(line =~ /^\s*#\s*@\w+/)
193
251
  end
194
252
 
195
- # Whether a line is any comment line.
253
+ # Comment line
196
254
  #
197
- # @note module_function: when included, also defines #comment_line? (instance visibility: private)
198
- # @param [String] line
255
+ # @note module_function: defines #comment_line? (visibility: private)
256
+ # @param [String] line comment line to check
199
257
  # @return [Boolean]
200
258
  def comment_line?(line)
201
259
  !!(line =~ /^\s*#/)
202
260
  end
203
261
 
204
- # Whether a line is a blank comment separator.
262
+ # Blank comment line
205
263
  #
206
- # @note module_function: when included, also defines #blank_comment_line? (instance visibility: private)
207
- # @param [String] line
264
+ # @note module_function: defines #blank_comment_line? (visibility: private)
265
+ # @param [String] line comment line to check
208
266
  # @return [Boolean]
209
267
  def blank_comment_line?(line)
210
268
  !!(line =~ /^\s*#\s*$/)
211
269
  end
212
270
 
213
- # Extract tag name from a top-level tag line without the leading `@`.
271
+ # Extract tag name
214
272
  #
215
- # @note module_function: when included, also defines #extract_tag_name (instance visibility: private)
216
- # @param [String] line
273
+ # @note module_function: defines #extract_tag_name (visibility: private)
274
+ # @param [String] line comment line to parse
217
275
  # @return [String, nil]
218
276
  def extract_tag_name(line)
219
277
  line[/^\s*#\s*@(\w+)/, 1]
220
278
  end
221
279
 
222
- # Extract parameter name from a `@param` line.
280
+ # Extract param name
223
281
  #
224
- # @note module_function: when included, also defines #extract_param_name (instance visibility: private)
225
- # @param [String] line
282
+ # @note module_function: defines #extract_param_name (visibility: private)
283
+ # @param [String] line param tag line to parse
226
284
  # @return [String, nil]
227
285
  def extract_param_name(line)
228
286
  return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+\[[^\]]+\]\s+(\S+)/
@@ -231,10 +289,10 @@ module Docscribe
231
289
  nil
232
290
  end
233
291
 
234
- # Extract owning options-hash param name from an `@option` line.
292
+ # Extract option owner
235
293
  #
236
- # @note module_function: when included, also defines #extract_option_owner (instance visibility: private)
237
- # @param [String] line
294
+ # @note module_function: defines #extract_option_owner (visibility: private)
295
+ # @param [String] line option tag line to parse
238
296
  # @return [String, nil]
239
297
  def extract_option_owner(line)
240
298
  line[/^\s*#\s*@option\b\s+(\S+)/, 1]