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
@@ -52,44 +52,17 @@ module Docscribe
52
52
  # @param [Integer] def_bol_pos beginning-of-line position of the target def
53
53
  # @return [Hash, 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.
@@ -104,35 +77,109 @@ module Docscribe
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: when included, also defines #find_comment_block_range (instance visibility: private)
96
+ # @param [Array<String>] lines
97
+ # @param [Integer] def_line_idx
98
+ # @return [Hash{start_idx: Integer, end_idx: 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: when included, also defines #find_preserved_start_idx (instance visibility: private)
117
+ # @param [Array<String>] lines
118
+ # @param [Integer] start_idx
119
+ # @param [Integer] end_idx
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: when included, also defines #doc_marker? (instance visibility: private)
130
+ # @param [Array<String>] lines
131
+ # @param [Range] 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: when included, also defines #build_block_info (instance visibility: private)
140
+ # @param [Array<String>] lines
141
+ # @param [Integer] start_idx
142
+ # @param [Integer] preserved_start_idx
143
+ # @param [Integer] end_idx
144
+ # @return [Hash]
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: when included, also defines #compute_removal_range (instance visibility: private)
158
+ # @param [Parser::Source::Buffer] buffer
159
+ # @param [Array<String>] lines
160
+ # @param [Integer] preserved_start_idx
161
+ # @param [Integer] def_bol_pos
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: when included, also defines #compute_positions (instance visibility: private)
171
+ # @param [Array<String>] lines
172
+ # @param [Integer] start_idx
173
+ # @param [Integer] doc_start_idx
174
+ # @param [Integer] end_pos_idx
175
+ # @return [Hash{start_pos: Integer, doc_start_pos: Integer, end_pos: 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:
@@ -191,14 +238,15 @@ 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)
241
+ # @note module_function: when included, also defines #already_has_doc_immediately_above?
242
+ # (instance visibility: private)
195
243
  # @param [Parser::Source::Buffer] buffer
196
244
  # @param [Integer] insert_pos
197
245
  # @return [Boolean]
198
246
  def already_has_doc_immediately_above?(buffer, insert_pos)
199
247
  src = buffer.source
200
248
  lines = src.lines
201
- current_line_index = src[0...insert_pos].count("\n")
249
+ current_line_index = (src[0...insert_pos] || '').count("\n")
202
250
  i = current_line_index - 1
203
251
  i -= 1 while i >= 0 && lines[i].strip.empty?
204
252
  return false if i.negative?
@@ -64,26 +64,45 @@ module Docscribe
64
64
  # @param [Array<String>] lines
65
65
  # @return [Array<Hash>]
66
66
  def parse_segments(lines)
67
- segments = []
67
+ segments = [] #: Array[untyped]
68
68
  i = 0
69
69
 
70
- while i < lines.length
71
- line = lines[i]
70
+ i = advance_parse(lines, i, segments) while i < lines.length
71
+
72
+ segments
73
+ end
72
74
 
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
75
+ # Parse the next line as either a tag run or non-tag content, appending to segments.
76
+ #
77
+ # @note module_function: when included, also defines #advance_parse (instance visibility: private)
78
+ # @param [Array<String>] lines comment block lines
79
+ # @param [Integer] idx current parse index
80
+ # @param [Array<Hash>] segments accumulated parsed segments
81
+ # @return [Integer] new index after processing
82
+ def advance_parse(lines, idx, segments)
83
+ if top_level_tag_line?(lines[idx])
84
+ consume_tag_run(lines, idx, segments)
85
+ else
86
+ segments << { type: :other, lines: [lines[idx]] }
87
+ idx + 1
84
88
  end
89
+ end
85
90
 
86
- segments
91
+ # Consume a contiguous tag run and append to segments.
92
+ #
93
+ # @note module_function: when included, also defines #consume_tag_run (instance visibility: private)
94
+ # @param [Array<String>] lines
95
+ # @param [Integer] idx current index
96
+ # @param [Array<Hash>] segments accumulated segments
97
+ # @return [Integer] new index after consuming the run
98
+ def consume_tag_run(lines, idx, segments)
99
+ entries = [] #: Array[untyped]
100
+ while idx < lines.length && top_level_tag_line?(lines[idx])
101
+ entry, idx = consume_entry(lines, idx)
102
+ entries << entry
103
+ end
104
+ segments << { type: :tag_run, entries: entries }
105
+ idx
87
106
  end
88
107
 
89
108
  # Sort one parsed segment if it is a tag run.
@@ -124,29 +143,51 @@ module Docscribe
124
143
  def consume_entry(lines, start_idx)
125
144
  first = lines[start_idx]
126
145
  tag = extract_tag_name(first)
127
- entry_lines = [first]
128
- i = start_idx + 1
146
+ entry_lines = collect_continuation_lines(lines, start_idx + 1)
147
+ i = entry_lines.length + start_idx
129
148
 
130
- while i < lines.length
131
- line = lines[i]
132
-
133
- break if top_level_tag_line?(line)
134
- break if blank_comment_line?(line)
135
- break unless comment_line?(line)
149
+ entry = build_entry(tag, entry_lines, first, start_idx)
136
150
 
137
- entry_lines << line
138
- i += 1
139
- end
151
+ [entry, i]
152
+ end
140
153
 
141
- entry = Entry.new(
154
+ # Build an Entry struct from parsed tag name, lines, and source line metadata.
155
+ #
156
+ # @note module_function: when included, also defines #build_entry (instance visibility: private)
157
+ # @param [String, nil] tag the extracted tag name
158
+ # @param [Array<String>] entry_lines all lines belonging to this entry
159
+ # @param [String] first the first (tag) line
160
+ # @param [Integer] start_idx original index of the first line
161
+ # @return [Entry]
162
+ def build_entry(tag, entry_lines, first, start_idx)
163
+ Entry.new(
142
164
  tag: tag,
143
165
  lines: entry_lines,
144
166
  param_name: extract_param_name(first),
145
167
  option_owner: extract_option_owner(first),
146
168
  index: start_idx
147
169
  )
170
+ end
148
171
 
149
- [entry, i]
172
+ # Collect continuation lines following a top-level tag entry.
173
+ #
174
+ # @note module_function: when included, also defines #collect_continuation_lines (instance visibility: private)
175
+ # @param [Array<String>] lines
176
+ # @param [Integer] start_idx
177
+ # @return [Array<String>]
178
+ def collect_continuation_lines(lines, start_idx)
179
+ result = [] #: Array[String]
180
+ i = start_idx
181
+
182
+ while i < lines.length
183
+ line = lines[i]
184
+ break if top_level_tag_line?(line) || blank_comment_line?(line) || !comment_line?(line)
185
+
186
+ result << line
187
+ i += 1
188
+ end
189
+
190
+ result
150
191
  end
151
192
 
152
193
  # Group entries so `@option` tags remain attached to their owning `@param`.
@@ -155,34 +196,54 @@ module Docscribe
155
196
  # @param [Array<Entry>] entries
156
197
  # @return [Array<Array<Entry>>]
157
198
  def group_entries(entries)
158
- groups = []
199
+ groups = [] #: Array[untyped]
159
200
  i = 0
160
201
 
161
202
  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
203
+ groups << group_entry(entries, i)
204
+ i += 1
181
205
  end
182
206
 
183
207
  groups
184
208
  end
185
209
 
210
+ # Group a single entry, attaching any subsequent @option entries if it is a @param.
211
+ #
212
+ # @note module_function: when included, also defines #group_entry (instance visibility: private)
213
+ # @param [Array<Entry>] entries parsed tag entries
214
+ # @param [Integer] idx index of the entry to group
215
+ # @return [Array<Entry>] the entry group
216
+ def group_entry(entries, idx)
217
+ entry = entries[idx]
218
+ if entry.tag == 'param'
219
+ [entry] + collect_option_entries(entries, idx + 1, entry.param_name)
220
+ else
221
+ [entry]
222
+ end
223
+ end
224
+
225
+ # Collect `@option` entries belonging to the given param name.
226
+ #
227
+ # @note module_function: when included, also defines #collect_option_entries (instance visibility: private)
228
+ # @param [Array<Entry>] entries
229
+ # @param [Integer] start_idx
230
+ # @param [String] param_name
231
+ # @return [Array<Entry>]
232
+ def collect_option_entries(entries, start_idx, param_name)
233
+ result = [] #: Array[untyped]
234
+ i = start_idx
235
+
236
+ while i < entries.length &&
237
+ entries[i].tag == 'option' &&
238
+ entries[i].option_owner &&
239
+ entries[i].option_owner == param_name
240
+ result << entries[i]
241
+ i += 1
242
+ end
243
+
244
+ result
245
+ end
246
+
186
247
  # Whether a line begins a top-level tag entry.
187
248
  #
188
249
  # @note module_function: when included, also defines #top_level_tag_line? (instance visibility: private)