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
@@ -18,413 +18,685 @@ module Docscribe
18
18
  module DocBuilder
19
19
  module_function
20
20
 
21
- # Build a complete doc block for one collected method insertion.
22
- #
23
- # External signatures, when available, override inferred param and return
24
- # types.
25
- #
26
- # @note module_function: when included, also defines #build (instance visibility: private)
27
- # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
28
- # @param [Docscribe::Config] config
29
- # @param [Object, nil] signature_provider provider responding to
30
- # `signature_for(container:, scope:, name:)`
31
- # @param [nil] core_rbs_provider Param documentation.
32
- # @param [nil] param_types Param documentation.
33
- # @param [nil] return_type_override Param documentation.
34
- # @param [nil] override_tags Param documentation.
21
+ PARAM_TYPE_COLLECTORS = {
22
+ arg: lambda { |arg_node, param_types, external_sig, config|
23
+ collect_param_type(
24
+ arg_node,
25
+ param_types,
26
+ external_sig,
27
+ config,
28
+ infer_name: nil
29
+ )
30
+ },
31
+
32
+ optarg: lambda { |arg_node, param_types, external_sig, config|
33
+ collect_optarg_param_type(
34
+ arg_node,
35
+ param_types,
36
+ external_sig,
37
+ config,
38
+ infer_name: nil
39
+ )
40
+ },
41
+
42
+ kwarg: lambda { |arg_node, param_types, external_sig, config|
43
+ collect_param_type(
44
+ arg_node,
45
+ param_types,
46
+ external_sig,
47
+ config,
48
+ infer_name: ->(param_name) { "#{param_name}:" }
49
+ )
50
+ },
51
+
52
+ kwoptarg: lambda { |arg_node, param_types, external_sig, config|
53
+ collect_optarg_param_type(
54
+ arg_node,
55
+ param_types,
56
+ external_sig,
57
+ config,
58
+ infer_name: ->(param_name) { "#{param_name}:" }
59
+ )
60
+ }
61
+ }.freeze
62
+
63
+ # Build
64
+ #
65
+ # @note module_function: defines #build (visibility: private)
66
+ # @param [Object] insertion the collected method insertion object
67
+ # @param [Docscribe::Config] config Docscribe configuration object
68
+ # @param [Object] opts additional keyword options forwarded to doc_setup
35
69
  # @raise [StandardError]
36
- # @return [String, nil]
37
- def build(insertion, config:, signature_provider: nil, core_rbs_provider: nil, param_types: nil, return_type_override: nil, override_tags: nil)
70
+ # @return [String, nil] if StandardError
71
+ # @return [nil] if StandardError
72
+ def build(insertion, config:, **opts)
73
+ setup = doc_setup(insertion, config: config, **opts)
74
+ return nil unless setup
75
+
76
+ build_unsafe(insertion, config: config, setup: setup, **opts)
77
+ rescue StandardError => e
78
+ debug_warn(e, insertion: insertion, name: '(unknown)', phase: 'DocBuilder.build')
79
+ nil
80
+ end
81
+
82
+ # Build merge additions
83
+ #
84
+ # @note module_function: defines #build_merge_additions (visibility: private)
85
+ # @param [Object] insertion the collected method insertion object
86
+ # @param [Array<String>] existing_lines existing doc comment lines being merged
87
+ # @param [Docscribe::Config] config Docscribe configuration object
88
+ # @param [Object] options additional keyword options forwarded to downstream methods
89
+ # @raise [StandardError]
90
+ # @return [String, nil] if StandardError
91
+ # @return [nil] if StandardError
92
+ def build_merge_additions(insertion, existing_lines:, config:, **options)
93
+ setup = doc_setup(insertion, config: config, **options)
94
+ return '' unless setup
95
+
96
+ info = parse_existing_doc_tags(existing_lines)
97
+ merge_dest_lines(existing_lines, setup: setup, insertion: insertion, config: config, info: info,
98
+ param_types: options[:param_types])
99
+ rescue StandardError => e
100
+ debug_warn(e, insertion: insertion, name: setup&.dig(:name) || '(unknown)',
101
+ phase: 'DocBuilder.build_merge_additions')
102
+ nil
103
+ end
104
+
105
+ # Build missing merge result
106
+ #
107
+ # @note module_function: defines #build_missing_merge_result (visibility: private)
108
+ # @param [Object] insertion the collected method insertion object
109
+ # @param [Array<String>] existing_lines existing doc comment lines being merged
110
+ # @param [Docscribe::Config] config Docscribe configuration object
111
+ # @param [Object] options additional keyword options forwarded to downstream methods
112
+ # @raise [StandardError]
113
+ # @return [Hash<Symbol, Object>] if StandardError
114
+ # @return [Hash] if StandardError
115
+ def build_missing_merge_result(insertion, existing_lines:, config:, **options)
116
+ setup = doc_setup(insertion, config: config, **options)
117
+ return { lines: [], reasons: [] } unless setup
118
+
119
+ info = parse_existing_doc_tags(existing_lines)
120
+ collect_all_missing(setup, info, insertion, config, options)
121
+ rescue StandardError => e
122
+ debug_warn(e, insertion: insertion, name: setup&.dig(:name) || '(unknown)',
123
+ phase: 'DocBuilder.build_missing_merge_result')
124
+ { lines: [], reasons: [] }
125
+ end
126
+
127
+ # Doc setup
128
+ #
129
+ # @note module_function: defines #doc_setup (visibility: private)
130
+ # @param [Object] insertion the collected method insertion object
131
+ # @param [Docscribe::Config] config Docscribe configuration object
132
+ # @param [Object] opts additional options
133
+ # @return [Hash<Symbol, Object>, nil]
134
+ def doc_setup(insertion, config:, **opts)
38
135
  node = insertion.node
39
136
  name = SourceHelpers.node_name(node)
40
137
  return nil unless name
41
138
 
42
- indent = SourceHelpers.line_indent(node)
43
- scope = insertion.scope
44
- visibility = insertion.visibility
45
- container = insertion.container
46
- method_symbol = scope == :instance ? '#' : '.'
139
+ setup = extract_base_setup(insertion, name)
140
+ resolve_doc_setup!(setup, node, name, config, opts)
141
+ end
142
+
143
+ # Build unsafe
144
+ #
145
+ # @note module_function: defines #build_unsafe (visibility: private)
146
+ # @param [Object] insertion the collected method insertion object
147
+ # @param [Docscribe::Config] config Docscribe configuration object
148
+ # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
149
+ # @param [Object] opts additional options including infer_default, fallback_type, treat_options_keyword_as_hash
150
+ # @return [String]
151
+ def build_unsafe(insertion, config:, setup:, **opts)
152
+ _, pl, rt = build_param_and_raise_info(setup, config, opts)
153
+ lines = build_doc_lines(setup, config: config, insertion: insertion, params_lines: pl, raise_types: rt,
154
+ override_tags: opts[:override_tags],
155
+ return_description: opts[:return_description],
156
+ description: opts[:description])
157
+ lines.map { |l| "#{l}\n" }.join
158
+ end
159
+
160
+ # Build param and raise info
161
+ #
162
+ # @note module_function: defines #build_param_and_raise_info (visibility: private)
163
+ # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
164
+ # @param [Docscribe::Config] config Docscribe configuration object
165
+ # @param [Hash<Symbol, Object>] opts additional options including
166
+ # @return [(Hash<String, String>, nil, Array<String>, nil, Array<String>)]
167
+ def build_param_and_raise_info(setup, config, opts)
168
+ pt = opts[:param_types] || build_param_types_from_node(setup[:node], external_sig: setup[:external_sig],
169
+ config: config)
170
+ pl = if config.emit_param_tags?
171
+ build_params_lines(setup[:node], setup[:indent], external_sig: setup[:external_sig], config: config,
172
+ param_types_override: pt,
173
+ param_descriptions: opts[:param_descriptions])
174
+ end
175
+ rt = config.emit_raise_tags? ? Docscribe::Infer.infer_raises_from_node(setup[:node]) : [] #: Array[String]
176
+ [pt, pl, rt]
177
+ end
47
178
 
48
- external_sig = signature_provider&.signature_for(
49
- container: container,
50
- scope: scope,
51
- name: name
179
+ # Resolve doc setup
180
+ #
181
+ # @note module_function: defines #resolve_doc_setup! (visibility: private)
182
+ # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
183
+ # @param [Parser::AST::Node] node AST node whose source text to extract
184
+ # @param [Symbol] name the method name string
185
+ # @param [Docscribe::Config] config Docscribe configuration object
186
+ # @param [Hash<Symbol, Object>] opts additional options including
187
+ # @return [Hash<Symbol, Object>]
188
+ def resolve_doc_setup!(setup, node, name, config, opts)
189
+ external_sig = resolve_external_sig(setup[:container], setup[:scope], name, opts[:signature_provider])
190
+ returns_spec = compute_returns_spec(node, config, opts[:param_types], opts[:core_rbs_provider])
191
+ normal_type = opts[:return_type_override] || external_sig&.return_type || returns_spec[:normal]
192
+
193
+ setup.merge(
194
+ external_sig: external_sig,
195
+ normal_type: normal_type,
196
+ rescue_specs: returns_spec[:rescues] || []
52
197
  )
198
+ end
53
199
 
54
- effective_param_types =
55
- param_types || build_param_types_from_node(node, external_sig: external_sig, config: config)
200
+ # Extract base setup
201
+ #
202
+ # @note module_function: defines #extract_base_setup (visibility: private)
203
+ # @param [Object] insertion the collected method insertion object
204
+ # @param [Symbol] name the method name string
205
+ # @return [Hash<Symbol, Object>]
206
+ def extract_base_setup(insertion, name)
207
+ n = insertion.node
208
+ { node: n, name: name, indent: SourceHelpers.line_indent(n), scope: insertion.scope,
209
+ visibility: insertion.visibility, container: insertion.container,
210
+ method_symbol: insertion.scope == :instance ? '#' : '.' }
211
+ end
56
212
 
57
- if config.emit_param_tags?
58
- params_lines = build_params_lines(node, indent, external_sig: external_sig, config: config, param_types_override: effective_param_types)
59
- end
60
- raise_types = config.emit_raise_tags? ? Docscribe::Infer.infer_raises_from_node(node) : []
213
+ # Resolve external sig
214
+ #
215
+ # @note module_function: defines #resolve_external_sig (visibility: private)
216
+ # @param [String] container method container name
217
+ # @param [Symbol] scope method scope symbol
218
+ # @param [Symbol] name the method name string
219
+ # @param [Docscribe::Types::ProviderChain, nil] signature_provider external sig provider
220
+ # @return [Docscribe::Types::MethodSignature, nil]
221
+ def resolve_external_sig(container, scope, name, signature_provider)
222
+ signature_provider&.signature_for(container: container, scope: scope, name: name)
223
+ end
61
224
 
62
- returns_spec = Docscribe::Infer.returns_spec_from_node(
63
- node,
64
- fallback_type: config.fallback_type,
65
- nil_as_optional: config.nil_as_optional?,
66
- param_types: effective_param_types,
67
- core_rbs_provider: core_rbs_provider
225
+ # Compute returns spec
226
+ #
227
+ # @note module_function: defines #compute_returns_spec (visibility: private)
228
+ # @param [Parser::AST::Node] node AST node whose source text to extract
229
+ # @param [Docscribe::Config] config Docscribe configuration object
230
+ # @param [Hash<String, String>, nil] param_types hash accumulating parameter name-to-type mappings
231
+ # @param [Object] core_rbs_provider RBS type provider
232
+ # @return [Hash<Symbol, Object>]
233
+ def compute_returns_spec(node, config, param_types, core_rbs_provider)
234
+ Docscribe::Infer.returns_spec_from_node(
235
+ node, fallback_type: config.fallback_type, nil_as_optional: config.nil_as_optional?,
236
+ param_types: param_types, core_rbs_provider: core_rbs_provider
68
237
  )
238
+ end
69
239
 
70
- normal_type = return_type_override || external_sig&.return_type || returns_spec[:normal]
71
- rescue_specs = returns_spec[:rescues] || []
240
+ # Parse existing doc tags
241
+ #
242
+ # @note module_function: defines #parse_existing_doc_tags (visibility: private)
243
+ # @param [Array<String>] lines existing doc comment lines
244
+ # @return [Hash<Symbol, Object>] parsed tag info
245
+ def parse_existing_doc_tags(lines)
246
+ init = init_parse_info
247
+ tags_started = false
248
+ joined_lines = join_multiline_tags(Array(lines))
249
+ joined_lines.each_with_object(init) do |line, info|
250
+ extract_all_comment_tags(line, info)
251
+ tags_started = parse_existing_tag_line(line, info, tags_started)
252
+ end
253
+ end
72
254
 
73
- lines = []
255
+ # Join @param/@return/@raise tag lines where the type bracket spans multiple lines.
256
+ #
257
+ # @note module_function: defines #join_multiline_tags (visibility: private)
258
+ # @param [Array<String>] lines doc comment lines
259
+ # @return [Array<String>]
260
+ def join_multiline_tags(lines)
261
+ result = [] #: Array[String]
262
+ i = 0
263
+ i = consume_tag_or_copy(lines, i, result) while i < lines.length
264
+ result
265
+ end
74
266
 
75
- if config.emit_header?
76
- lines << "#{indent}# +#{container}#{method_symbol}#{name}+ -> #{normal_type}"
77
- lines << "#{indent}#"
267
+ # Consume tag line or copy verbatim
268
+ #
269
+ # @note module_function: defines #consume_tag_or_copy (visibility: private)
270
+ # @param [Array<String>] lines doc comment lines
271
+ # @param [Integer] idx current line index
272
+ # @param [Array<String>] result result accumulator array
273
+ # @return [Integer]
274
+ def consume_tag_or_copy(lines, idx, result)
275
+ if (c = lines[idx].sub(/^\s*#\s*/, '')) =~ /^@(param|return|raise)\s+\[/ && unbalanced_bracket?(c)
276
+ buffer, consumed = join_tag_continuations(lines, idx)
277
+ result << "# #{buffer}"
278
+ idx + consumed
279
+ else
280
+ result << lines[idx]
281
+ idx + 1
78
282
  end
283
+ end
79
284
 
80
- if config.include_default_message?
81
- lines << "#{indent}# #{config.default_message(scope, visibility)}"
82
- lines << "#{indent}#"
285
+ # Join continuation lines for a multi-line tag type bracket.
286
+ #
287
+ # @note module_function: defines #join_tag_continuations (visibility: private)
288
+ # @param [Array<String>] lines all doc comment lines
289
+ # @param [Integer] start index of the @param/@return/@raise line
290
+ # @return [(String, Integer)] joined content and number of lines consumed
291
+ def join_tag_continuations(lines, start)
292
+ buffer = +lines[start].sub(/^\s*#\s*/, '').dup
293
+ i = start + 1
294
+ while i < lines.length
295
+ continuation = lines[i].sub(/^\s*#[ \t]/, '')
296
+ break unless continuation.start_with?(' ')
297
+
298
+ buffer << continuation.rstrip
299
+ i += 1
300
+ break unless unbalanced_bracket?(buffer)
83
301
  end
302
+ [buffer, i - start]
303
+ end
84
304
 
85
- if config.emit_visibility_tags?
86
- case visibility
87
- when :private
88
- lines << "#{indent}# @private"
89
- when :protected
90
- lines << "#{indent}# @protected"
91
- end
305
+ # Check if bracket depth is positive (an opening `[` is unclosed).
306
+ #
307
+ # @note module_function: defines #unbalanced_bracket? (visibility: private)
308
+ # @param [String] str string to check
309
+ # @return [Boolean]
310
+ def unbalanced_bracket?(str)
311
+ depth = 0
312
+ str.each_char do |c|
313
+ depth += 1 if c == '['
314
+ depth -= 1 if c == ']'
92
315
  end
316
+ depth.positive?
317
+ end
93
318
 
94
- if insertion.respond_to?(:module_function) && insertion.module_function
95
- included_vis =
96
- if insertion.respond_to?(:included_instance_visibility) && insertion.included_instance_visibility
97
- insertion.included_instance_visibility
98
- else
99
- :private
100
- end
101
-
102
- lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
103
- "(instance visibility: #{included_vis})"
319
+ # Parse a single doc comment line for tag info.
320
+ #
321
+ # @note module_function: defines #parse_existing_tag_line (visibility: private)
322
+ # @param [String] line the doc comment line
323
+ # @param [Hash<Symbol, Object>] info mutable parse info accumulator
324
+ # @param [Boolean] tags_started whether @tags have been seen
325
+ # @return [Boolean] updated tags_started
326
+ def parse_existing_tag_line(line, info, tags_started)
327
+ content = line.sub(/^\s*# ?/, '').rstrip
328
+ if content.start_with?('@')
329
+ tags_started = true.tap { track_last_tag(content, info) }
330
+ start_note_tag(line, info) if content.start_with?('@note ')
331
+ elsif tags_started && info[:last_tag]
332
+ append_note_continuation(line, info).tap { append_tag_continuation(content, info) }
333
+ else
334
+ info[:description] << content
104
335
  end
336
+ tags_started
337
+ end
105
338
 
106
- lines.concat(params_lines) if params_lines
107
- raise_types.each { |rt| lines << "#{indent}# @raise [#{rt}]" } if config.emit_raise_tags?
108
- lines << "#{indent}# @return [#{normal_type}]" if config.emit_return_tag?(scope, visibility)
339
+ # Start a note tag
340
+ #
341
+ # @note module_function: defines #start_note_tag (visibility: private)
342
+ # @param [String] line doc comment line
343
+ # @param [Hash<Symbol, Object>] info parse info hash
344
+ # @return [void]
345
+ def start_note_tag(line, info)
346
+ return if line.match?(/^\s*#\s*@note\s+module_function:/)
109
347
 
110
- if config.emit_rescue_conditional_returns?
111
- rescue_specs.each do |exceptions, rtype|
112
- lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
113
- end
114
- end
115
- plugin_tags = Docscribe::Plugin.run_tag_plugins(build_plugin_context(insertion, normal_type: normal_type))
116
- plugin_tags.concat(Array(override_tags)) if override_tags
348
+ empty = [] #: Array[String]
349
+ info[:note_lines] << empty
350
+ info[:note_lines].last << line.chomp
351
+ end
117
352
 
118
- lines.concat(render_plugin_tags(plugin_tags, indent))
119
- lines.map { |l| "#{l}\n" }.join
120
- rescue StandardError => e
121
- debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build')
122
- nil
353
+ # Append note continuation lines
354
+ #
355
+ # @note module_function: defines #append_note_continuation (visibility: private)
356
+ # @param [String] line doc comment line
357
+ # @param [Hash<Symbol, Object>] info parse info hash
358
+ # @return [Object]
359
+ def append_note_continuation(line, info)
360
+ return unless info[:last_tag] == :note && info[:note_lines].any?
361
+
362
+ info[:note_lines].last << line.chomp
123
363
  end
124
364
 
125
- # Build only the missing doc lines that should be merged into an existing
126
- # doc-like block.
365
+ # Init parse info
127
366
  #
128
- # This is used by safe mode for non-destructive updates.
367
+ # @note module_function: defines #init_parse_info (visibility: private)
368
+ # @return [Hash<Symbol, Object>]
369
+ def init_parse_info
370
+ {
371
+ param_names: {}, param_types: {}, param_descriptions: {},
372
+ raise_types: {}, plugin_tags: {},
373
+ has_return: false, return_type: nil, return_description: nil,
374
+ has_private: false, has_protected: false, has_module_function_note: false,
375
+ description: [],
376
+ last_tag: nil, last_param: nil,
377
+ note_lines: []
378
+ }
379
+ end
380
+
381
+ # Merge dest lines
129
382
  #
130
- # @note module_function: when included, also defines #build_merge_additions (instance visibility: private)
131
- # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
132
- # @param [Array<String>] existing_lines
133
- # @param [Docscribe::Config] config
134
- # @param [Object, nil] signature_provider
135
- # @param [nil] core_rbs_provider Param documentation.
136
- # @param [nil] param_types Param documentation.
137
- # @param [nil] return_type_override Param documentation.
138
- # @raise [StandardError]
383
+ # @note module_function: defines #merge_dest_lines (visibility: private)
384
+ # @param [Array<String>] existing_lines existing doc comment lines to merge into
385
+ # @param [Object] ctx merge context hash (setup, insertion, config, info, param_types)
139
386
  # @return [String, nil]
140
- def build_merge_additions(insertion, existing_lines:, config:, signature_provider: nil, core_rbs_provider: nil,
141
- param_types: nil, return_type_override: nil)
142
- node = insertion.node
143
- name = SourceHelpers.node_name(node)
144
- return '' unless name
387
+ def merge_dest_lines(existing_lines, **ctx)
388
+ merge_lines_with_context(existing_lines, **ctx)
389
+ end
145
390
 
146
- indent = SourceHelpers.line_indent(node)
147
- info = parse_existing_doc_tags(existing_lines)
148
- scope = insertion.scope
149
- visibility = insertion.visibility
391
+ # Merge lines with context
392
+ #
393
+ # @note module_function: defines #merge_lines_with_context (visibility: private)
394
+ # @param [Array<String>] existing_lines existing doc comment lines being merged
395
+ # @param [Object] ctx merge context (setup, insertion, config, info, param_types)
396
+ # @return [String]
397
+ def merge_lines_with_context(existing_lines, **ctx)
398
+ s = ctx[:setup]
399
+ i = s[:indent]
400
+ config = ctx[:config]
401
+ info = ctx[:info]
402
+ base_ary = build_initial_line_ary(existing_lines, i)
403
+ line_ary = merge_all_tag_lines(base_ary, s: s, i: i, config: config, info: info,
404
+ insertion: ctx[:insertion], param_types: ctx[:param_types])
405
+ useful = line_ary.reject { |l| l.strip == '#' }
406
+ return '' if useful.empty?
150
407
 
151
- external_sig = signature_provider&.signature_for(
152
- container: insertion.container,
153
- scope: scope,
154
- name: name
155
- )
408
+ line_ary.map { |l| "#{l}\n" }.join
409
+ end
156
410
 
157
- returns_spec = Docscribe::Infer.returns_spec_from_node(
158
- node,
159
- fallback_type: config.fallback_type,
160
- nil_as_optional: config.nil_as_optional?,
161
- param_types: param_types,
162
- core_rbs_provider: core_rbs_provider
163
- )
411
+ # Build initial line ary
412
+ #
413
+ # @note module_function: defines #build_initial_line_ary (visibility: private)
414
+ # @param [Array<String>] existing_lines existing doc comment lines being merged
415
+ # @param [String] indent indentation string for the doc line
416
+ # @return [Array<String>]
417
+ def build_initial_line_ary(existing_lines, indent)
418
+ existing_lines.any? && existing_lines.last.strip != '#' ? ["#{indent}#"] : []
419
+ end
164
420
 
165
- normal_type = return_type_override || external_sig&.return_type || returns_spec[:normal]
166
- rescue_specs = returns_spec[:rescues] || []
421
+ # Merge all tag lines
422
+ #
423
+ # @note module_function: defines #merge_all_tag_lines (visibility: private)
424
+ # @param [Array<String>] base_ary initial line array
425
+ # @param [Object] ctx context hash with setup, config, info, insertion, param_types
426
+ # @return [Array<String>]
427
+ def merge_all_tag_lines(base_ary, **ctx)
428
+ line_ary = base_ary.dup
429
+ merge_tag_lines_core(line_ary, ctx)
430
+ line_ary.concat(merge_rescue_return_lines(ctx[:i], ctx[:s][:rescue_specs], ctx[:config], ctx[:info]))
431
+ line_ary
432
+ end
167
433
 
168
- lines = []
169
- lines << "#{indent}#" if existing_lines.any? && existing_lines.last.strip != '#'
434
+ # Merge tag lines core
435
+ #
436
+ # @note module_function: defines #merge_tag_lines_core (visibility: private)
437
+ # @param [Array<String>] line_ary output line array
438
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
439
+ # @return [void]
440
+ def merge_tag_lines_core(line_ary, ctx)
441
+ append_merge_tag_lines(line_ary, ctx)
442
+ merge_return_line(line_ary, ctx[:i], ctx[:s], ctx[:config], ctx[:info])
443
+ end
170
444
 
171
- if config.emit_visibility_tags?
172
- if visibility == :private && !info[:has_private]
173
- lines << "#{indent}# @private"
174
- elsif visibility == :protected && !info[:has_protected]
175
- lines << "#{indent}# @protected"
176
- end
177
- end
445
+ # Append merge tag lines
446
+ #
447
+ # @note module_function: defines #append_merge_tag_lines (visibility: private)
448
+ # @param [Array<String>] line_ary output line array
449
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
450
+ # @return [void]
451
+ def append_merge_tag_lines(line_ary, ctx)
452
+ line_ary.concat(build_all_merge_tags(ctx))
453
+ end
178
454
 
179
- if insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
180
- included_vis = insertion.included_instance_visibility || :private
181
- lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
182
- "(instance visibility: #{included_vis})"
183
- end
455
+ # Build all merge tags
456
+ #
457
+ # @note module_function: defines #build_all_merge_tags (visibility: private)
458
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
459
+ # @return [Array<String>]
460
+ def build_all_merge_tags(ctx)
461
+ i = ctx[:i]
462
+ s = ctx[:s]
463
+ c = ctx[:config]
464
+ info = ctx[:info]
465
+ [merge_visibility_tag_lines(i, s[:visibility], c, info),
466
+ merge_module_function_note_lines(i, ctx[:insertion], s[:name], info),
467
+ merge_param_lines(s[:node], i, config: c, external_sig: s[:external_sig],
468
+ param_types: ctx[:param_types], info: info),
469
+ merge_raise_tag_lines(s[:node], i, c, info)].flatten
470
+ end
184
471
 
185
- if config.emit_param_tags?
186
- all_params = build_params_lines(node, indent, external_sig: external_sig, config: config, param_types_override: param_types)
187
- all_params&.each do |pl|
188
- pname = extract_param_name_from_param_line(pl)
189
- next if pname.nil? || info[:param_names].include?(pname)
472
+ # Merge return line
473
+ #
474
+ # @note module_function: defines #merge_return_line (visibility: private)
475
+ # @param [Array<String>] line_ary output line array
476
+ # @param [String] indent indentation string for doc comment lines
477
+ # @param [Hash<Symbol, Object>] setup method setup hash with node, name, types, scope
478
+ # @param [Docscribe::Config] config Docscribe configuration object
479
+ # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
480
+ # @return [void]
481
+ def merge_return_line(line_ary, indent, setup, config, info)
482
+ emit_ret = config.emit_return_tag?(setup[:scope], setup[:visibility])
483
+ ret_line = merge_return_tag_line(indent, setup[:normal_type], config: config, scope: setup[:scope],
484
+ visibility: setup[:visibility], info: info)
190
485
 
191
- lines << pl
192
- end
193
- end
486
+ line_ary << ret_line if emit_ret && ret_line
487
+ end
194
488
 
195
- if config.emit_raise_tags?
196
- inferred = Docscribe::Infer.infer_raises_from_node(node)
197
- existing = info[:raise_types] || {}
198
- missing = inferred.reject { |rt| existing[rt] }
199
- missing.each { |rt| lines << "#{indent}# @raise [#{rt}]" }
200
- end
489
+ # Collect all missing
490
+ #
491
+ # @note module_function: defines #collect_all_missing (visibility: private)
492
+ # @param [Hash<Symbol, Object>] setup resolved setup hash with node, name, indent, types
493
+ # @param [Hash<Symbol, Object>] info parsed existing doc tag information
494
+ # @param [Object] insertion the collected method insertion object
495
+ # @param [Docscribe::Config] config Docscribe configuration object
496
+ # @param [Hash<Symbol, Object>] options additional options hash forwarded to missing collector
497
+ # @return [Hash<Symbol, Object>]
498
+ def collect_all_missing(setup, info, insertion, config, options)
499
+ s = setup
500
+ ctx = { node: s[:node], indent: s[:indent], config: config, external_sig: s[:external_sig],
501
+ info: info, strategy: options[:strategy], scope: s[:scope], visibility: s[:visibility],
502
+ normal_type: s[:normal_type], rescue_specs: s[:rescue_specs], insertion: insertion,
503
+ param_types: options[:param_types], override_tags: options[:override_tags] }
504
+ collect_missing_all(ctx)
505
+ end
201
506
 
202
- if config.emit_return_tag?(scope, visibility) && !info[:has_return]
203
- lines << "#{indent}# @return [#{normal_type}]"
204
- end
507
+ # Collect missing all
508
+ #
509
+ # @note module_function: defines #collect_missing_all (visibility: private)
510
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
511
+ # @return [Hash<Symbol, Object>]
512
+ def collect_missing_all(ctx)
513
+ lines = [] #: Array[String]
514
+ reasons = [] #: Array[Hash[Symbol, untyped]]
515
+ collect_missing_visibility!(lines, reasons, **ctx)
516
+ collect_missing_module_function_note!(lines, reasons, **ctx)
517
+ collect_missing_params!(lines, reasons, **ctx)
518
+ collect_missing_raises!(lines, reasons, **ctx)
519
+ collect_missing_return!(lines, reasons, **ctx)
520
+ collect_missing_rescue_returns!(lines, reasons, **ctx)
521
+ collect_missing_plugin_tags!(lines, reasons, **ctx)
522
+ { lines: lines, reasons: reasons }
523
+ end
205
524
 
206
- if config.emit_rescue_conditional_returns? && !info[:has_return]
207
- rescue_specs.each do |exceptions, rtype|
208
- lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
209
- end
210
- end
525
+ # Extract param info
526
+ #
527
+ # @note module_function: defines #extract_all_comment_tags (visibility: private)
528
+ # @param [String] line single comment line
529
+ # @param [Hash<Symbol, Object>] info parse info hash
530
+ # @return [void]
531
+ def extract_all_comment_tags(line, info)
532
+ extract_param_info(line, info[:param_names], info[:param_types], info[:param_descriptions])
533
+ extract_return_info(line, info)
534
+ extract_visibility_info(line, info)
535
+ extract_raise_info(line, info[:raise_types])
536
+ extract_plugin_info(line, info[:plugin_tags])
537
+ end
211
538
 
212
- useful = lines.reject { |l| l.strip == '#' }
213
- return '' if useful.empty?
539
+ # Extract param info from tag line
540
+ #
541
+ # @note module_function: defines #extract_param_info (visibility: private)
542
+ # @param [String] line a single doc comment line to parse
543
+ # @param [Hash<String, Boolean>] param_names hash tracking existing @param names
544
+ # @param [Hash<String, String>] param_types hash tracking existing @param types
545
+ # @param [Hash<String, String>, nil] param_descriptions param descriptions hash
546
+ # @return [void]
547
+ def extract_param_info(line, param_names, param_types, param_descriptions = nil)
548
+ return unless (pname = extract_param_name_from_param_line(line))
214
549
 
215
- lines.map { |l| "#{l}\n" }.join
216
- rescue StandardError => e
217
- debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_merge_additions')
218
- nil
550
+ param_names[pname] = true
551
+ ptype = extract_param_type_from_param_line(line)
552
+ return unless ptype
553
+
554
+ param_types[pname] = ptype
555
+ return unless param_descriptions
556
+
557
+ desc = extract_param_description(line)
558
+ param_descriptions[pname] = desc if desc
219
559
  end
220
560
 
221
- # Build structured missing-line information for safe merge mode.
561
+ # Extract return info
222
562
  #
223
- # Returns both:
224
- # - generated missing lines
225
- # - structured reasons used by `--explain`
226
- #
227
- # @note module_function: when included, also defines #build_missing_merge_result (instance visibility: private)
228
- # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
229
- # @param [Array<String>] existing_lines
230
- # @param [Docscribe::Config] config
231
- # @param [Object, nil] signature_provider
232
- # @param [nil] core_rbs_provider Param documentation.
233
- # @param [nil] param_types Param documentation.
234
- # @param [nil] strategy Param documentation.
235
- # @param [nil] return_type_override Param documentation.
236
- # @param [nil] override_tags Param documentation.
237
- # @raise [StandardError]
238
- # @return [Hash]
239
- def build_missing_merge_result(insertion, existing_lines:, config:, signature_provider: nil,
240
- core_rbs_provider: nil, param_types: nil, strategy: nil, return_type_override: nil, override_tags: nil)
241
- node = insertion.node
242
- name = SourceHelpers.node_name(node)
243
- return { lines: [], reasons: [] } unless name
244
-
245
- indent = SourceHelpers.line_indent(node)
246
- info = parse_existing_doc_tags(existing_lines)
247
- scope = insertion.scope
248
- visibility = insertion.visibility
563
+ # @note module_function: defines #extract_return_info (visibility: private)
564
+ # @param [String] line a single doc comment line to parse
565
+ # @param [Hash<Symbol, Object>] info parse info hash to update with return data
566
+ # @return [void]
567
+ def extract_return_info(line, info)
568
+ return unless line.match?(/^\s*#\s*@return\b/)
249
569
 
250
- external_sig = signature_provider&.signature_for(
251
- container: insertion.container,
252
- scope: scope,
253
- name: name
254
- )
570
+ info[:has_return] = true
571
+ content = line.sub(/^\s*#\s*/, '')
572
+ return unless (m = content.match(/@return\s+/))
255
573
 
256
- returns_spec = Docscribe::Infer.returns_spec_from_node(
257
- node,
258
- fallback_type: config.fallback_type,
259
- nil_as_optional: config.nil_as_optional?,
260
- param_types: param_types,
261
- core_rbs_provider: core_rbs_provider
262
- )
574
+ return_type, return_desc = parse_return_rest(m.post_match)
575
+ info[:return_type] = return_type if return_type
576
+ info[:return_description] = return_desc if return_desc
577
+ end
263
578
 
264
- normal_type = return_type_override || external_sig&.return_type || returns_spec[:normal]
265
- rescue_specs = returns_spec[:rescues] || []
579
+ # Parse return type from rest string
580
+ #
581
+ # @note module_function: defines #parse_return_rest (visibility: private)
582
+ # @param [String] rest remaining tag content
583
+ # @return [(String, String, nil), nil]
584
+ def parse_return_rest(rest)
585
+ return unless rest[0] == '['
266
586
 
267
- lines = []
268
- reasons = []
587
+ type_end = find_matching_close_bracket(rest) or return
269
588
 
270
- if config.emit_visibility_tags?
271
- if visibility == :private && !info[:has_private]
272
- lines << "#{indent}# @private\n"
273
- reasons << { type: :missing_visibility, message: 'missing @private' }
274
- elsif visibility == :protected && !info[:has_protected]
275
- lines << "#{indent}# @protected\n"
276
- reasons << { type: :missing_visibility, message: 'missing @protected' }
277
- end
278
- end
589
+ return_type = rest[1...type_end] #: String
590
+ desc = rest[(type_end + 1)..]&.strip
591
+ [return_type, desc&.empty? ? nil : desc]
592
+ end
279
593
 
280
- if insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
281
- included_vis = insertion.included_instance_visibility || :private
282
- lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
283
- "(instance visibility: #{included_vis})\n"
284
- reasons << { type: :missing_module_function_note, message: 'missing module_function note' }
285
- end
286
-
287
- if config.emit_param_tags?
288
- all_params = build_params_lines(node, indent, external_sig: external_sig, config: config, param_types_override: param_types)
289
-
290
- all_params&.each do |pl|
291
- pname = extract_param_name_from_param_line(pl)
292
- next unless pname
293
-
294
- if !info[:param_names].include?(pname)
295
- lines << "#{pl}\n"
296
- reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
297
- elsif external_sig && info[:param_types][pname]
298
- new_type = extract_param_type_from_param_line(pl)
299
- if new_type && info[:param_types][pname] != new_type
300
- lines << "#{pl}\n" unless strategy == :safe
301
- reasons << {
302
- type: :updated_param,
303
- message: "updated @param #{pname} from #{info[:param_types][pname]} to #{new_type}",
304
- extra: { param: pname }
305
- }
306
- end
307
- end
308
- end
309
- end
594
+ # Extract all comment tags from line
595
+ #
596
+ # @note module_function: defines #track_last_tag (visibility: private)
597
+ # @param [String] content
598
+ # @param [Hash<Symbol, Object>] info parse info hash
599
+ # @return [void]
600
+ def track_last_tag(content, info)
601
+ tag = content.match(/@(\w+)/)&.[](1)&.to_sym
602
+ info[:last_tag] = tag
603
+ return unless tag == :param
310
604
 
311
- if config.emit_raise_tags?
312
- inferred = Docscribe::Infer.infer_raises_from_node(node)
313
- existing = info[:raise_types] || {}
314
- missing = inferred.reject { |rt| existing[rt] }
605
+ pname = extract_param_name_from_param_line(content)
606
+ info[:last_param] = pname if pname
607
+ end
315
608
 
316
- missing.each do |rt|
317
- lines << "#{indent}# @raise [#{rt}]\n"
318
- reasons << { type: :missing_raise, message: "missing @raise [#{rt}]", extra: { raise_type: rt } }
319
- end
320
- end
609
+ # Append continuation to current tag
610
+ #
611
+ # @note module_function: defines #append_tag_continuation (visibility: private)
612
+ # @param [String] content tag continuation text
613
+ # @param [Hash<Symbol, Object>] info parse info hash
614
+ # @return [void]
615
+ def append_tag_continuation(content, info)
616
+ text = content.strip
617
+ return if text.empty?
321
618
 
322
- if config.emit_return_tag?(scope, visibility)
323
- if !info[:has_return]
324
- lines << "#{indent}# @return [#{normal_type}]\n"
325
- reasons << { type: :missing_return, message: 'missing @return' }
326
- elsif external_sig && info[:return_type] && info[:return_type] != normal_type
327
- lines << "#{indent}# @return [#{normal_type}]\n" unless strategy == :safe
328
- reasons << {
329
- type: :updated_return,
330
- message: "updated @return from #{info[:return_type]} to #{normal_type}"
331
- }
332
- end
333
- end
619
+ append_to_return_description(text, info) if info[:last_tag] == :return
620
+ append_to_param_description(text, info) if info[:last_tag] == :param
621
+ end
334
622
 
335
- if config.emit_rescue_conditional_returns? && !info[:has_return]
336
- rescue_specs.each do |exceptions, rtype|
337
- lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}\n"
338
- reasons << {
339
- type: :missing_return,
340
- message: "missing conditional @return for #{exceptions.join(', ')}"
341
- }
342
- end
623
+ # Append text to return description
624
+ #
625
+ # @note module_function: defines #append_to_return_description (visibility: private)
626
+ # @param [String] text text to append
627
+ # @param [Hash<Symbol, Object>] info parse info hash
628
+ # @return [void]
629
+ def append_to_return_description(text, info)
630
+ if info[:return_description]
631
+ info[:return_description] += "\n#{text}"
632
+ else
633
+ info[:return_description] = text
343
634
  end
344
- plugin_tags = Docscribe::Plugin.run_tag_plugins(build_plugin_context(insertion, normal_type: normal_type))
345
- plugin_tags.concat(Array(override_tags)) if override_tags
635
+ end
346
636
 
347
- plugin_tags.each do |tag|
348
- next if info[:plugin_tags]&.[](tag.name)
637
+ # Append text to param description
638
+ #
639
+ # @note module_function: defines #append_to_param_description (visibility: private)
640
+ # @param [String] text text to append
641
+ # @param [Hash<Symbol, Object>] info parse info hash
642
+ # @return [void]
643
+ def append_to_param_description(text, info)
644
+ pname = info[:last_param]
645
+ return unless pname
349
646
 
350
- rendered = render_plugin_tags([tag], indent).first
351
- lines << "#{rendered}\n"
352
- reasons << { type: :missing_plugin_tag, message: "missing @#{tag.name}" }
647
+ if info[:param_descriptions][pname]
648
+ info[:param_descriptions][pname] += "\n#{text}"
649
+ else
650
+ info[:param_descriptions][pname] = text
353
651
  end
354
- { lines: lines, reasons: reasons }
355
- rescue StandardError => e
356
- debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_missing_merge_result')
357
- { lines: [], reasons: [] }
358
652
  end
359
653
 
360
- # Parse existing doc comment lines and extract known YARD tags.
654
+ # Extract visibility info
361
655
  #
362
- # Extracts: `@param` names, `@return`, `@raise`, `@private`, `@protected`,
363
- # `@module_function` notes, and `@option` lines.
364
- #
365
- # @note module_function: when included, also defines #parse_existing_doc_tags (instance visibility: private)
366
- # @param [Array<String>] lines existing doc comment lines
367
- # @return [Hash] parsed tag info
368
- def parse_existing_doc_tags(lines)
369
- param_names = {}
370
- param_types = {}
371
- has_return = false
372
- return_type = nil
373
- has_private = false
374
- has_protected = false
375
- has_module_function_note = false
376
- raise_types = {}
377
- plugin_tags = {}
378
-
379
- Array(lines).each do |line|
380
- if (m = line.match(/^\s*#\s*@(\w+)\b/))
381
- plugin_tags[m[1]] = true
382
- end
383
- if (pname = extract_param_name_from_param_line(line))
384
- param_names[pname] = true
385
- if (type_match = line.match(/@param\s+\[([^\]]+)\]\s+\S+/) || line.match(/@param\s+\S+\s+\[([^\]]+)\]/))
386
- param_types[pname] = type_match[1]
387
- end
388
- end
656
+ # @note module_function: defines #extract_visibility_info (visibility: private)
657
+ # @param [String] line a single doc comment line to parse
658
+ # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
659
+ # @return [void]
660
+ def extract_visibility_info(line, info)
661
+ info[:has_private] ||= line.match?(/^\s*#\s*@private\b/)
662
+ info[:has_protected] ||= line.match?(/^\s*#\s*@protected\b/)
663
+ info[:has_module_function_note] ||= line.match?(/^\s*#\s*@note\s+module_function:/)
664
+ end
389
665
 
390
- if line.match?(/^\s*#\s*@return\b/)
391
- has_return = true
392
- if (m = line.match(/@return\s+\[([^\]]+)\]/))
393
- return_type = m[1]
394
- end
395
- end
396
- has_private ||= line.match?(/^\s*#\s*@private\b/)
397
- has_protected ||= line.match?(/^\s*#\s*@protected\b/)
398
- has_module_function_note ||= line.match?(/^\s*#\s*@note\s+module_function:/)
666
+ # Extract raise info
667
+ #
668
+ # @note module_function: defines #extract_raise_info (visibility: private)
669
+ # @param [String] line a single doc comment line to parse
670
+ # @param [Hash<String, Boolean>] raise_types hash tracking existing @raise types
671
+ # @return [void]
672
+ def extract_raise_info(line, raise_types)
673
+ extract_raise_types_from_line(line).each { |t| raise_types[t || ''] = true }
674
+ end
399
675
 
400
- extract_raise_types_from_line(line).each { |t| raise_types[t] = true }
401
- end
676
+ # Extract plugin info
677
+ #
678
+ # @note module_function: defines #extract_plugin_info (visibility: private)
679
+ # @param [String] line a single doc comment line to parse
680
+ # @param [Hash<String, Boolean>] plugin_tags hash tracking existing plugin tag names
681
+ # @return [void]
682
+ def extract_plugin_info(line, plugin_tags)
683
+ return unless (m = line.match(/^\s*#\s*@(\w+)\b/))
402
684
 
403
- {
404
- param_names: param_names,
405
- param_types: param_types,
406
- has_return: has_return,
407
- return_type: return_type,
408
- raise_types: raise_types,
409
- has_private: has_private,
410
- has_protected: has_protected,
411
- has_module_function_note: has_module_function_note,
412
- plugin_tags: plugin_tags
413
- }
685
+ plugin_tags[m[1] || ''] = true
414
686
  end
415
687
 
416
- # Extract exception names from a `@raise` doc line.
688
+ # Extract raise types from line
417
689
  #
418
- # @note module_function: when included, also defines #extract_raise_types_from_line (instance visibility: private)
690
+ # @note module_function: defines #extract_raise_types_from_line (visibility: private)
419
691
  # @param [String] line a `@raise` doc line
420
692
  # @raise [StandardError]
421
- # @return [String, nil] the exception name or nil
422
- # @return [Array] if StandardError or line not matched
693
+ # @return [Array<String, nil>] if StandardError
694
+ # @return [Array] if StandardError
423
695
  def extract_raise_types_from_line(line)
424
696
  return [] unless line.match?(/^\s*#\s*@raise\b/)
425
697
 
426
698
  if (m = line.match(/^\s*#\s*@raise\s*\[([^\]]+)\]/))
427
- parse_raise_bracket_list(m[1])
699
+ parse_raise_bracket_list(m[1]) # steep:ignore ArgumentTypeMismatch
428
700
  elsif (m = line.match(/^\s*#\s*@raise\s+([A-Z]\w*(?:::[A-Z]\w*)*)/))
429
701
  [m[1]]
430
702
  else
@@ -434,361 +706,1311 @@ module Docscribe
434
706
  []
435
707
  end
436
708
 
437
- # Parse exception names from a `@raise [ExceptionA, ExceptionB]` line.
709
+ # Parse raise bracket list
438
710
  #
439
- # @note module_function: when included, also defines #parse_raise_bracket_list (instance visibility: private)
440
- # @param [String] s the `@raise` line text
441
- # @return [Array<String>, nil] the exception names or nil
442
- def parse_raise_bracket_list(s)
443
- s.to_s.split(',').map(&:strip).reject(&:empty?)
711
+ # @note module_function: defines #parse_raise_bracket_list (visibility: private)
712
+ # @param [String] str comma-separated exception names string from @raise brackets
713
+ # @return [Array<String>] the exception names or nil
714
+ def parse_raise_bracket_list(str)
715
+ str.to_s.split(',').map(&:strip).reject(&:empty?)
444
716
  end
445
717
 
446
- # Build a param name => type map from a method node.
718
+ # Build param types from node
447
719
  #
448
- # @note module_function: when included, also defines #build_param_types_from_node (instance visibility: private)
449
- # @private
720
+ # @note module_function: defines #build_param_types_from_node (visibility: private)
450
721
  # @param [Parser::AST::Node] node def or defs node
451
- # @param [Object, nil] external_sig external signature if available
452
- # @param [Docscribe::Config] config
453
- # @return [Hash{String => String}, nil]
722
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external signature if available
723
+ # @param [Docscribe::Config] config Docscribe configuration object
724
+ # @return [Hash<String, String>, nil]
454
725
  def build_param_types_from_node(node, external_sig:, config:)
455
- return nil unless node
726
+ return unless node
456
727
 
457
- args =
458
- case node.type
459
- when :def then node.children[1]
460
- when :defs then node.children[2]
461
- end
728
+ args = extract_args_from_node(node)
729
+ return unless args
462
730
 
463
- return nil unless args
731
+ param_types = {} #: Hash[String, String]
732
+ collect_all_param_types(args, param_types, external_sig, config)
733
+ param_types.empty? ? nil : param_types
734
+ end
464
735
 
465
- param_types = {}
466
-
467
- (args.children || []).each do |a|
468
- case a.type
469
- when :arg
470
- pname = a.children.first.to_s
471
- ty = external_sig&.param_types&.[](pname) ||
472
- Infer.infer_param_type(
473
- pname,
474
- nil,
475
- fallback_type: config.fallback_type,
476
- treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
477
- )
478
- param_types[pname] = ty
479
-
480
- when :optarg
481
- pname, default = *a
482
- pname = pname.to_s
483
- default_src = default&.loc&.expression&.source
484
- ty = external_sig&.param_types&.[](pname) ||
485
- Infer.infer_param_type(
486
- pname,
487
- default_src,
488
- fallback_type: config.fallback_type,
489
- treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
490
- )
491
- param_types[pname] = ty
492
-
493
- when :kwarg
494
- pname = a.children.first.to_s
495
- ty = external_sig&.param_types&.[](pname) ||
496
- Infer.infer_param_type(
497
- "#{pname}:",
498
- nil,
499
- fallback_type: config.fallback_type,
500
- treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
501
- )
502
- param_types[pname] = ty
503
-
504
- when :kwoptarg
505
- pname, default = *a
506
- pname = pname.to_s
507
- default_src = default&.loc&.expression&.source
508
- ty = external_sig&.param_types&.[](pname) ||
509
- Infer.infer_param_type(
510
- "#{pname}:",
511
- default_src,
512
- fallback_type: config.fallback_type,
513
- treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?
514
- )
515
- param_types[pname] = ty
736
+ # Collect all param types
737
+ #
738
+ # @note module_function: defines #collect_all_param_types (visibility: private)
739
+ # @param [Parser::AST::Node] args arguments AST node
740
+ # @param [Hash<String, String>] param_types hash accumulating parameter name-to-type mappings
741
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
742
+ # @param [Docscribe::Config] config Docscribe configuration object
743
+ # @return [void]
744
+ def collect_all_param_types(args, param_types, external_sig, config)
745
+ # Pre-seed param_types with positional (unnamed) RBS types so that
746
+ # collectors can keep them when external_sig lacks param names.
747
+ positional = Array(external_sig&.positional_types)
748
+ (args.children || []).each_with_index do |a, idx|
749
+ if (ptype = positional[idx])
750
+ pname = a.children.first
751
+ param_types[pname.to_s] = ptype if pname
516
752
  end
753
+ collector = PARAM_TYPE_COLLECTORS[a.type]
754
+ collector&.call(a, param_types, external_sig, config)
517
755
  end
756
+ end
518
757
 
519
- param_types.empty? ? nil : param_types
758
+ # Collect param type
759
+ #
760
+ # @note module_function: defines #collect_param_type (visibility: private)
761
+ # @param [Parser::AST::Node] arg_node AST node for the required/keyword argument
762
+ # @param [Hash<String, String>] param_types hash accumulating parameter name-to-type mappings
763
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
764
+ # @param [Docscribe::Config] config Docscribe configuration for fallback type options
765
+ # @param [Proc, nil] infer_name lambda to transform parameter name for inference
766
+ # @return [void]
767
+ def collect_param_type(arg_node, param_types, external_sig, config, infer_name:)
768
+ pname = arg_node.children.first.to_s
769
+ param_types[pname] ||= begin
770
+ infer_pname = resolve_infer_name(pname, infer_name)
771
+ external_sig&.param_types&.[](pname) ||
772
+ Infer.infer_param_type(infer_pname, nil,
773
+ fallback_type: config.fallback_type,
774
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?)
775
+ end
520
776
  end
521
777
 
522
- # Build generated `@param` / `@option` lines for a method node.
778
+ # Collect optarg param type
523
779
  #
524
- # External signatures take precedence over inferred parameter types.
780
+ # @note module_function: defines #collect_optarg_param_type (visibility: private)
781
+ # @param [Parser::AST::Node] arg_node AST node for the optional/keyword optional argument
782
+ # @param [Hash<String, String>] param_types hash accumulating parameter name-to-type mappings
783
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
784
+ # @param [Docscribe::Config] config Docscribe configuration for fallback type options
785
+ # @param [Proc, nil] infer_name lambda to transform parameter name for inference
786
+ # @return [void]
787
+ def collect_optarg_param_type(arg_node, param_types, external_sig, config, infer_name:)
788
+ pname, default = *arg_node
789
+ pname = pname.to_s
790
+ param_types[pname] ||= begin
791
+ default_src = source_from_node(default)
792
+ infer_pname = resolve_infer_name(pname, infer_name)
793
+ external_sig&.param_types&.[](pname) ||
794
+ Infer.infer_param_type(infer_pname, default_src,
795
+ fallback_type: config.fallback_type,
796
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?)
797
+ end
798
+ end
799
+
800
+ # Merge visibility tag lines
525
801
  #
526
- # @note module_function: when included, also defines #build_params_lines (instance visibility: private)
527
- # @param [Parser::AST::Node] node
528
- # @param [String] indent
529
- # @param [Docscribe::Types::MethodSignature, nil] external_sig
530
- # @param [Docscribe::Config] config
531
- # @param [nil] param_types_override Param documentation.
532
- # @return [Array<String>, nil]
533
- def build_params_lines(node, indent, external_sig:, config:, param_types_override: nil)
534
- fallback_type = config.fallback_type
535
- treat_options_keyword_as_hash = config.treat_options_keyword_as_hash?
536
- param_tag_style = config.param_tag_style
537
- param_documentation = config.include_param_documentation? ? config.param_documentation : ''
538
-
539
- args =
540
- case node.type
541
- when :def then node.children[1]
542
- when :defs then node.children[2]
543
- end
802
+ # @note module_function: defines #merge_visibility_tag_lines (visibility: private)
803
+ # @param [String] indent indentation string for the doc line
804
+ # @param [Symbol] visibility method visibility symbol
805
+ # @param [Docscribe::Config] config Docscribe configuration object
806
+ # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
807
+ # @return [Array<String>]
808
+ def merge_visibility_tag_lines(indent, visibility, config, info)
809
+ return [] unless config.emit_visibility_tags?
544
810
 
545
- return nil unless args
811
+ if visibility == :private && !info[:has_private]
812
+ ["#{indent}# @private"]
813
+ elsif visibility == :protected && !info[:has_protected]
814
+ ["#{indent}# @protected"]
815
+ else
816
+ []
817
+ end
818
+ end
546
819
 
547
- params = []
548
-
549
- (args.children || []).each do |a|
550
- case a.type
551
- when :arg
552
- pname = a.children.first.to_s
553
- ty = external_sig&.param_types&.[](pname) ||
554
- override_param_type_for(pname, param_types_override) ||
555
- Infer.infer_param_type(
556
- pname,
557
- nil,
558
- fallback_type: fallback_type,
559
- treat_options_keyword_as_hash: treat_options_keyword_as_hash
560
- )
561
- params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
562
-
563
- when :optarg
564
- pname, default = *a
565
- pname = pname.to_s
566
- default_src = default&.loc&.expression&.source
567
- ty = external_sig&.param_types&.[](pname) ||
568
- override_param_type_for(pname, param_types_override) ||
569
- Infer.infer_param_type(
570
- pname,
571
- default_src,
572
- fallback_type: fallback_type,
573
- treat_options_keyword_as_hash: treat_options_keyword_as_hash
574
- )
575
- params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
576
-
577
- hash_option_pairs(default).each do |pair|
578
- key_node, value_node = pair.children
579
- option_key = option_key_name(key_node)
580
- option_type = Infer::Literals.type_from_literal(value_node, fallback_type: fallback_type)
581
- option_default = node_default_literal(value_node)
582
-
583
- line = "#{indent}# @option #{pname} [#{option_type}] :#{option_key}"
584
- line += " (#{option_default})" if option_default
585
- line += ' Option documentation.'
586
- params << line
587
- end
588
-
589
- when :kwarg
590
- pname = a.children.first.to_s
591
- ty = external_sig&.param_types&.[](pname) ||
592
- override_param_type_for(pname, param_types_override) ||
593
- Infer.infer_param_type(
594
- "#{pname}:",
595
- nil,
596
- fallback_type: fallback_type,
597
- treat_options_keyword_as_hash: treat_options_keyword_as_hash
598
- )
599
- params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
600
-
601
- when :kwoptarg
602
- pname, default = *a
603
- pname = pname.to_s
604
- default_src = default&.loc&.expression&.source
605
- ty = external_sig&.param_types&.[](pname) ||
606
- override_param_type_for(pname, param_types_override) ||
607
- Infer.infer_param_type(
608
- "#{pname}:",
609
- default_src,
610
- fallback_type: fallback_type,
611
- treat_options_keyword_as_hash: treat_options_keyword_as_hash
612
- )
613
- params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
614
-
615
- when :restarg
616
- pname = (a.children.first || 'args').to_s
617
- ty =
618
- if external_sig&.rest_positional&.element_type
619
- "Array<#{external_sig.rest_positional.element_type}>"
620
- else
621
- override_param_type_for(pname, param_types_override) ||
622
- Infer.infer_param_type(
623
- "*#{pname}",
624
- nil,
625
- fallback_type: fallback_type,
626
- treat_options_keyword_as_hash: treat_options_keyword_as_hash
627
- )
628
- end
629
- params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
630
-
631
- when :kwrestarg
632
- pname = (a.children.first || 'kwargs').to_s
633
- ty = external_sig&.rest_keywords&.type ||
634
- override_param_type_for(pname, param_types_override) ||
635
- Infer.infer_param_type(
636
- "**#{pname}",
637
- nil,
638
- fallback_type: fallback_type,
639
- treat_options_keyword_as_hash: treat_options_keyword_as_hash
640
- )
641
- params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
642
-
643
- when :blockarg
644
- pname = (a.children.first || 'block').to_s
645
- ty = external_sig&.param_types&.[](pname) ||
646
- override_param_type_for(pname, param_types_override) ||
647
- Infer.infer_param_type(
648
- "&#{pname}",
649
- nil,
650
- fallback_type: fallback_type,
651
- treat_options_keyword_as_hash: treat_options_keyword_as_hash
652
- )
653
- params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
654
-
655
- when :forward_arg
656
- # skip
657
- end
820
+ # Merge module function note lines
821
+ #
822
+ # @note module_function: defines #merge_module_function_note_lines (visibility: private)
823
+ # @param [String] indent indentation string for the doc line
824
+ # @param [Object] insertion the collected method insertion object
825
+ # @param [String] name the method name string
826
+ # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
827
+ # @return [Array<String>]
828
+ def merge_module_function_note_lines(indent, insertion, name, info)
829
+ unless insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
830
+ return []
658
831
  end
659
832
 
660
- params.empty? ? nil : params
833
+ included_vis = insertion.included_instance_visibility || :private
834
+ ["#{indent}# @note module_function: defines ##{name} (visibility: #{included_vis})"]
661
835
  end
662
836
 
663
- # Method documentation.
837
+ # Merge param lines
664
838
  #
665
- # @note module_function: when included, also defines #override_param_type_for (instance visibility: private)
666
- # @param [Object] pname Param documentation.
667
- # @param [Object] override_map Param documentation.
668
- # @return [Object]
669
- def override_param_type_for(pname, override_map)
670
- return nil unless override_map
839
+ # @note module_function: defines #merge_param_lines (visibility: private)
840
+ # @param [Parser::AST::Node] node AST node whose source text to extract
841
+ # @param [String] indent indentation string for the doc line
842
+ # @param [Docscribe::Config] config Docscribe configuration object
843
+ # @param [Object] opts additional options including external_sig, param_types, info
844
+ # @return [Array<String>]
845
+ def merge_param_lines(node, indent, config:, **opts)
846
+ return [] unless config.emit_param_tags?
671
847
 
672
- key = pname.to_s
673
- override_map[key] || override_map[:"#{key}"] || override_map["#{key}:"] || override_map[:"#{key}:"]
848
+ all_params = build_params_lines(node, indent, external_sig: opts[:external_sig], config: config,
849
+ param_types_override: opts[:param_types])
850
+ return [] unless all_params
851
+
852
+ info = opts[:info]
853
+ all_params.each_with_object([]) do |pl, result|
854
+ pname = extract_param_name_from_param_line(pl)
855
+ next if pname.nil? || info[:param_names].include?(pname)
856
+
857
+ result << pl
858
+ end
674
859
  end
675
860
 
676
- # Format a `@param` tag line using the configured param tag style.
861
+ # Merge raise tag lines
677
862
  #
678
- # @note module_function: when included, also defines #format_param_tag (instance visibility: private)
679
- # @param [String] indent leading whitespace
680
- # @param [String] name parameter name
681
- # @param [String] type parameter type
682
- # @param [String] documentation optional documentation text
683
- # @param [String, Symbol] style param tag style (`"name_type"` or `"type_name"`)
684
- # @return [String]
685
- def format_param_tag(indent, name, type, documentation, style:)
686
- doc = documentation.to_s.strip
687
- type = type.to_s
688
-
689
- line = case style.to_s
690
- when 'name_type'
691
- "#{indent}# @param #{name} [#{type}]"
692
- else
693
- "#{indent}# @param [#{type}] #{name}"
694
- end
863
+ # @note module_function: defines #merge_raise_tag_lines (visibility: private)
864
+ # @param [Parser::AST::Node] node AST node whose source text to extract
865
+ # @param [String] indent indentation string for the doc line
866
+ # @param [Docscribe::Config] config Docscribe configuration object
867
+ # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
868
+ # @return [Array<String>]
869
+ def merge_raise_tag_lines(node, indent, config, info)
870
+ return [] unless config.emit_raise_tags?
695
871
 
696
- doc.empty? ? line : "#{line} #{doc}"
872
+ inferred = Docscribe::Infer.infer_raises_from_node(node)
873
+ existing = info[:raise_types] || {}
874
+ inferred.reject { |rt| existing[rt] }
875
+ .map { |rt| "#{indent}# @raise [#{rt}]" }
697
876
  end
698
877
 
699
- # Extract keyword argument option pairs from a hash default value.
878
+ # Merge return tag line
700
879
  #
701
- # @note module_function: when included, also defines #hash_option_pairs (instance visibility: private)
702
- # @param [Parser::AST::Node, nil] node a `:hash` node
703
- # @return [Array<Parser::AST::Node>] the `:pair` children
704
- def hash_option_pairs(node)
705
- return [] unless node&.type == :hash
880
+ # @note module_function: defines #merge_return_tag_line (visibility: private)
881
+ # @param [String] indent indentation string for the doc line
882
+ # @param [String] normal_type resolved return type
883
+ # @param [Docscribe::Config] config Docscribe configuration object
884
+ # @param [Object] opts additional options including scope, visibility, info
885
+ # @return [String, nil]
886
+ def merge_return_tag_line(indent, normal_type, config:, **opts)
887
+ return unless config.emit_return_tag?(opts[:scope], opts[:visibility])
888
+ return if opts[:info][:has_return]
706
889
 
707
- node.children.select { |child| child.is_a?(Parser::AST::Node) && child.type == :pair }
890
+ "#{indent}# @return [#{normal_type}]"
708
891
  end
709
892
 
710
- # Get the symbol name from an option key node.
893
+ # Merge rescue return lines
711
894
  #
712
- # @note module_function: when included, also defines #option_key_name (instance visibility: private)
713
- # @param [Parser::AST::Node] key_node a `:sym` node
714
- # @return [String] the option key name
715
- def option_key_name(key_node)
716
- case key_node&.type
717
- when :sym, :str
718
- key_node.children.first.to_s
719
- else
720
- key_node&.loc&.expression&.source.to_s.sub(/\A:/, '')
895
+ # @note module_function: defines #merge_rescue_return_lines (visibility: private)
896
+ # @param [String] indent indentation string for the doc line
897
+ # @param [Array<(Array<String>, String)>] rescue_specs rescue type specs
898
+ # @param [Docscribe::Config] config Docscribe configuration object
899
+ # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
900
+ # @return [Array<String>]
901
+ def merge_rescue_return_lines(indent, rescue_specs, config, info)
902
+ return [] unless config.emit_rescue_conditional_returns?
903
+ return [] if info[:has_return]
904
+
905
+ rescue_specs.map do |exceptions, rtype|
906
+ "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
721
907
  end
722
908
  end
723
909
 
724
- # Get the raw source literal for a default value node.
910
+ # Collect missing visibility
725
911
  #
726
- # @note module_function: when included, also defines #node_default_literal (instance visibility: private)
727
- # @param [Parser::AST::Node, nil] node a default value node
728
- # @return [String, nil] the source literal or nil
729
- def node_default_literal(node)
730
- node&.loc&.expression&.source
912
+ # @note module_function: defines #collect_missing_visibility! (visibility: private)
913
+ # @param [Array<String>] lines array of output doc lines being accumulated
914
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
915
+ # @param [Object] ctx merged context hash with info and indent
916
+ # @return [void]
917
+ def collect_missing_visibility!(lines, reasons, **ctx)
918
+ return unless ctx[:config].emit_visibility_tags?
919
+
920
+ add_missing_private(lines, reasons, ctx)
921
+ add_missing_protected(lines, reasons, ctx)
731
922
  end
732
923
 
733
- # Extract the parameter name from a `@param` doc line.
924
+ # Add missing private
734
925
  #
735
- # Handles both `"@param [Type] name"` and `"@param name [Type]"` styles.
926
+ # @note module_function: defines #add_missing_private (visibility: private)
927
+ # @param [Array<String>] lines array of output doc lines being accumulated
928
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
929
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
930
+ # @return [void]
931
+ def add_missing_private(lines, reasons, ctx)
932
+ return unless ctx[:visibility] == :private && !ctx[:info][:has_private]
933
+
934
+ lines << "#{ctx[:indent]}# @private\n"
935
+ reasons << { type: :missing_visibility, message: 'missing @private' }
936
+ end
937
+
938
+ # Add missing protected
736
939
  #
737
- # @note module_function: when included, also defines #extract_param_name_from_param_line (instance visibility: private)
940
+ # @note module_function: defines #add_missing_protected (visibility: private)
941
+ # @param [Array<String>] lines array of output doc lines being accumulated
942
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
943
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
944
+ # @return [void]
945
+ def add_missing_protected(lines, reasons, ctx)
946
+ return unless ctx[:visibility] == :protected && !ctx[:info][:has_protected]
947
+
948
+ lines << "#{ctx[:indent]}# @protected\n"
949
+ reasons << { type: :missing_visibility, message: 'missing @protected' }
950
+ end
951
+
952
+ # Collect missing module function note
953
+ #
954
+ # @note module_function: defines #collect_missing_module_function_note! (visibility: private)
955
+ # @param [Array<String>] lines array of output doc lines being accumulated
956
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
957
+ # @param [Object] ctx merged context hash with info and indent
958
+ # @return [void]
959
+ def collect_missing_module_function_note!(lines, reasons, **ctx)
960
+ insertion = ctx[:insertion]
961
+ unless insertion.respond_to?(:module_function) && insertion.module_function &&
962
+ !ctx[:info][:has_module_function_note]
963
+ return
964
+ end
965
+
966
+ included_vis = insertion.included_instance_visibility || :private
967
+ lines << "#{ctx[:indent]}# @note module_function: defines ##{ctx[:name]} (visibility: #{included_vis})\n"
968
+ reasons << { type: :missing_module_function_note, message: 'missing module_function note' }
969
+ end
970
+
971
+ # Collect missing params
972
+ #
973
+ # @note module_function: defines #collect_missing_params! (visibility: private)
974
+ # @param [Array<String>] lines array of output doc lines being accumulated
975
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
976
+ # @param [Object] ctx merged context hash with info and indent
977
+ # @return [void]
978
+ def collect_missing_params!(lines, reasons, **ctx)
979
+ return unless ctx[:config].emit_param_tags?
980
+
981
+ all_params = build_params_lines(ctx[:node], ctx[:indent],
982
+ external_sig: ctx[:external_sig], config: ctx[:config],
983
+ param_types_override: ctx[:param_types])
984
+ return unless all_params
985
+
986
+ all_params.each { |pl| collect_param_from_line(pl, lines, reasons, ctx) }
987
+ end
988
+
989
+ # Collect param from line
990
+ #
991
+ # @note module_function: defines #collect_param_from_line (visibility: private)
992
+ # @param [String] param_line a single @param tag line to evaluate
993
+ # @param [Array<String>] lines array of output doc lines being accumulated
994
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
995
+ # @param [Hash<Symbol, Object>] ctx merged context hash with build parameters
996
+ # @return [void]
997
+ def collect_param_from_line(param_line, lines, reasons, ctx)
998
+ pname = extract_param_name_from_param_line(param_line)
999
+ return unless pname
1000
+
1001
+ if !ctx[:info][:param_names].include?(pname)
1002
+ lines << "#{param_line}\n"
1003
+ reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
1004
+ elsif ctx[:external_sig] && ctx[:info][:param_types][pname]
1005
+ collect_updated_param(param_line, pname, lines, reasons, ctx)
1006
+ end
1007
+ end
1008
+
1009
+ # Collect updated param
1010
+ #
1011
+ # @note module_function: defines #collect_updated_param (visibility: private)
1012
+ # @param [String] param_line a single @param tag line to evaluate
1013
+ # @param [String] pname the parameter name string
1014
+ # @param [Array<String>] lines array of output doc lines being accumulated
1015
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1016
+ # @param [Hash<Symbol, Object>] ctx merged context hash with build parameters
1017
+ # @return [void]
1018
+ def collect_updated_param(param_line, pname, lines, reasons, ctx)
1019
+ new_type = extract_param_type_from_param_line(param_line)
1020
+ return unless new_type && ctx[:info][:param_types][pname] != new_type
1021
+
1022
+ lines << "#{param_line}\n" unless ctx[:strategy] == :safe
1023
+ reasons << {
1024
+ type: :updated_param,
1025
+ message: "updated @param #{pname} from #{ctx[:info][:param_types][pname]} to #{new_type}",
1026
+ extra: { param: pname }
1027
+ }
1028
+ end
1029
+
1030
+ # Build params lines
1031
+ #
1032
+ # @note module_function: defines #build_params_lines (visibility: private)
1033
+ # @param [Parser::AST::Node] node AST node whose source text to extract
1034
+ # @param [String] indent indentation string for the doc line
1035
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1036
+ # @param [Docscribe::Config] config Docscribe configuration object
1037
+ # @param [Hash] kwargs additional keyword args including insertion, params_lines, raise_types, override_tags
1038
+ # @return [Array<String>, nil]
1039
+ def build_params_lines(node, indent, external_sig:, config:, **kwargs)
1040
+ args = extract_args_from_node(node)
1041
+ return nil unless args
1042
+
1043
+ build_all_param_lines(args, indent, config, external_sig: external_sig, **kwargs)
1044
+ end
1045
+
1046
+ # Build all param lines
1047
+ #
1048
+ # @note module_function: defines #build_all_param_lines (visibility: private)
1049
+ # @param [Parser::AST::Node] args arguments AST node
1050
+ # @param [String] indent indentation string for the doc line
1051
+ # @param [Docscribe::Config] config Docscribe configuration object
1052
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1053
+ # @param [Object] kwargs additional keyword args including insertion, params_lines, raise_types, override_tags
1054
+ # @return [Array<String>, nil]
1055
+ def build_all_param_lines(args, indent, config, external_sig: nil, **kwargs)
1056
+ param_lines = [] #: Array[String]
1057
+ params = (args.children || []).each_with_object(param_lines) do |a, p|
1058
+ p.concat(build_param_line(a, indent, external_sig, kwargs[:param_types_override],
1059
+ skip_anonymous_block_params: config.skip_anonymous_block_params?,
1060
+ fallback_type: config.fallback_type,
1061
+ treat_options_keyword_as_hash: config.treat_options_keyword_as_hash?,
1062
+ param_documentation: param_doc_for_arg(a, kwargs, config),
1063
+ param_tag_style: config.param_tag_style))
1064
+ end
1065
+ params.empty? ? nil : params
1066
+ end
1067
+
1068
+ # Get param doc for argument
1069
+ #
1070
+ # @note module_function: defines #param_doc_for_arg (visibility: private)
1071
+ # @param [Parser::AST::Node] arg individual argument node
1072
+ # @param [Hash<Symbol, Object>] kwargs keyword args hash
1073
+ # @param [Docscribe::Config] config doc configuration
1074
+ # @return [String]
1075
+ def param_doc_for_arg(arg, kwargs, config)
1076
+ (kwargs[:param_descriptions] || {})[param_name_from_arg(arg)] ||
1077
+ (config.include_param_documentation? ? config.param_documentation : '')
1078
+ end
1079
+
1080
+ # Build doc lines
1081
+ #
1082
+ # @note module_function: defines #build_doc_lines (visibility: private)
1083
+ # @param [Hash<Symbol, Object>] setup method setup hash with indent, name, types, scope
1084
+ # @param [Docscribe::Config] config Docscribe configuration object
1085
+ # @param [Object] kwargs additional keyword args including insertion, params_lines, raise_types, override_tags
1086
+ # @return [Array<String>]
1087
+ def build_doc_lines(setup, config:, **kwargs)
1088
+ i = setup[:indent]
1089
+ assemble_doc_lines(i, setup, config: config, insertion: kwargs[:insertion],
1090
+ params_lines: kwargs[:params_lines],
1091
+ raise_types: kwargs[:raise_types], override_tags: kwargs[:override_tags],
1092
+ return_description: kwargs[:return_description],
1093
+ description: kwargs[:description])
1094
+ end
1095
+
1096
+ # Assemble doc lines
1097
+ #
1098
+ # @note module_function: defines #assemble_doc_lines (visibility: private)
1099
+ # @param [String] indent indent
1100
+ # @param [Hash<Symbol, Object>] setup setup
1101
+ # @param [Object] ctx context hash with config, insertion, params_lines, raise_types, override_tags
1102
+ # @return [Array<String>]
1103
+ def assemble_doc_lines(indent, setup, **ctx)
1104
+ line_ary = build_header_lines(
1105
+ indent,
1106
+ config: ctx[:config],
1107
+ container: setup[:container], method_symbol: setup[:method_symbol], name: setup[:name],
1108
+ normal_type: setup[:normal_type]
1109
+ )
1110
+
1111
+ append_assemble_body_lines(line_ary, indent, setup, ctx)
1112
+ line_ary
1113
+ end
1114
+
1115
+ # Append assemble body lines
1116
+ #
1117
+ # @note module_function: defines #append_assemble_body_lines (visibility: private)
1118
+ # @param [Array<String>] line_ary output line array
1119
+ # @param [String] indent indentation string for doc comment lines
1120
+ # @param [Hash<Symbol, Object>] setup method setup hash with name, types, scope
1121
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1122
+ # @return [void]
1123
+ def append_assemble_body_lines(line_ary, indent, setup, ctx)
1124
+ line_ary.concat(build_all_body_tags(indent, setup, ctx))
1125
+ end
1126
+
1127
+ # Build all body tags
1128
+ #
1129
+ # @note module_function: defines #build_all_body_tags (visibility: private)
1130
+ # @param [String] indent indentation string for doc comment lines
1131
+ # @param [Hash<Symbol, Object>] setup method setup hash with name, types, scope
1132
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1133
+ # @return [Array<String>]
1134
+ def build_all_body_tags(indent, setup, ctx)
1135
+ result = core_body_tags(indent, setup, ctx)
1136
+ result.insert(4, ctx[:params_lines]) if ctx[:params_lines]
1137
+ result.flatten
1138
+ end
1139
+
1140
+ # Core body tags
1141
+ #
1142
+ # @note module_function: defines #core_body_tags (visibility: private)
1143
+ # @param [String] indent indentation string for doc comment lines
1144
+ # @param [Hash<Symbol, Object>] setup method setup hash with name, types, scope
1145
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1146
+ # @return [Array<Object>]
1147
+ def core_body_tags(indent, setup, ctx)
1148
+ config, insertion = ctx.values_at(:config, :insertion)
1149
+ [
1150
+ defaults_and_visibility(indent, config, setup[:scope], setup[:visibility], description: ctx[:description]),
1151
+ build_module_function_note_lines(indent, insertion, setup[:name]),
1152
+ ctx.dig(:info, :note_lines) || [],
1153
+ build_raise_tag_lines(indent, ctx[:raise_types], config),
1154
+ build_return_line_if_needed(indent, setup, config, ctx),
1155
+ build_rescue_return_lines(indent, setup[:rescue_specs], config),
1156
+ build_plugin_tag_lines(insertion, indent, setup[:normal_type], ctx[:override_tags])
1157
+ ]
1158
+ end
1159
+
1160
+ # Defaults and visibility
1161
+ #
1162
+ # @note module_function: defines #defaults_and_visibility (visibility: private)
1163
+ # @param [String] indent indentation string for doc comment lines
1164
+ # @param [Docscribe::Config] config Docscribe configuration object
1165
+ # @param [Symbol] scope method scope symbol
1166
+ # @param [Symbol] visibility method visibility symbol
1167
+ # @param [Array<String>, nil] description optional description lines
1168
+ # @return [Array<String>]
1169
+ def defaults_and_visibility(indent, config, scope, visibility, description: nil)
1170
+ [
1171
+ build_default_msg_lines(indent, config, scope, visibility, description: description),
1172
+ build_visibility_tag_lines(indent, visibility, config)
1173
+ ].flatten
1174
+ end
1175
+
1176
+ # Build return line if needed
1177
+ #
1178
+ # @note module_function: defines #build_return_line_if_needed (visibility: private)
1179
+ # @param [String] indent indentation string for doc comment lines
1180
+ # @param [Hash<Symbol, Object>] setup method setup hash with name, normal_type, scope, visibility
1181
+ # @param [Docscribe::Config] config Docscribe configuration object
1182
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1183
+ # @return [Array<String>]
1184
+ def build_return_line_if_needed(indent, setup, config, ctx)
1185
+ ret_line = build_return_tag_line(indent, setup[:normal_type], config, setup[:scope], setup[:visibility])
1186
+ rd = ctx[:return_description]
1187
+ if ret_line && rd && !rd.empty?
1188
+ lines = rd.split("\n")
1189
+ ret_line = +"#{ret_line} #{lines.first}"
1190
+ lines[1..]&.each { |l| ret_line << "\n#{indent}# #{l}" }
1191
+ end
1192
+ ret_line ? [ret_line] : []
1193
+ end
1194
+
1195
+ # Extract args from node
1196
+ #
1197
+ # @note module_function: defines #extract_args_from_node (visibility: private)
1198
+ # @param [Parser::AST::Node] node AST node whose source text to extract
1199
+ # @return [Parser::AST::Node, nil]
1200
+ def extract_args_from_node(node)
1201
+ case node.type
1202
+ when :def then node.children[1]
1203
+ when :defs then node.children[2]
1204
+ end
1205
+ end
1206
+
1207
+ # Build param line
1208
+ #
1209
+ # @note module_function: defines #build_param_line (visibility: private)
1210
+ # @param [Parser::AST::Node] arg_node AST node for the argument
1211
+ # @param [String] indent indentation string for doc comment lines
1212
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1213
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1214
+ # @param [Object] opts additional options for param formatting (fallback_type, param_tag_style, etc.)
1215
+ # @return [Array<String>]
1216
+ def build_param_line(arg_node, indent, external_sig, param_types_override, **opts)
1217
+ method_name = :"build_#{arg_node.type}_line"
1218
+ if respond_to?(method_name, true)
1219
+ return [] if arg_node.type == :blockarg && opts[:skip_anonymous_block_params] && arg_node.children.first.nil?
1220
+
1221
+ return [send(method_name, arg_node, indent, external_sig, param_types_override, **opts)]
1222
+ end
1223
+
1224
+ method_name = :"build_#{arg_node.type}_lines"
1225
+ if respond_to?(method_name, true)
1226
+ return send(method_name, arg_node, indent, external_sig, param_types_override, **opts)
1227
+ end
1228
+
1229
+ []
1230
+ end
1231
+
1232
+ # Build header lines
1233
+ #
1234
+ # @note module_function: defines #build_header_lines (visibility: private)
1235
+ # @param [String] indent indentation string for the doc line
1236
+ # @param [Docscribe::Config] config Docscribe configuration object
1237
+ # @param [Object] opts additional options including container, method_symbol, name, normal_type
1238
+ # @return [Array<String>]
1239
+ def build_header_lines(indent, config:, **opts)
1240
+ if config.emit_header?
1241
+ c = opts[:container]
1242
+ ms = opts[:method_symbol]
1243
+ n = opts[:name]
1244
+ nt = opts[:normal_type]
1245
+ ["#{indent}# +#{c}#{ms}#{n}+ -> #{nt}", "#{indent}#"]
1246
+ else
1247
+ []
1248
+ end
1249
+ end
1250
+
1251
+ # Build default msg lines
1252
+ #
1253
+ # @note module_function: defines #build_default_msg_lines (visibility: private)
1254
+ # @param [String] indent indentation string for the doc line
1255
+ # @param [Docscribe::Config] config Docscribe configuration object
1256
+ # @param [Symbol] scope method scope symbol
1257
+ # @param [Symbol] visibility method visibility symbol
1258
+ # @param [Array<String>, nil] description optional description lines
1259
+ # @return [Array<String>]
1260
+ def build_default_msg_lines(indent, config, scope, visibility, description: nil)
1261
+ if description&.any?
1262
+ result = description.map { |line| line.empty? ? "#{indent}#" : "#{indent}# #{line}" }
1263
+ result << "#{indent}#" unless result.last == "#{indent}#"
1264
+ result
1265
+ elsif config.include_default_message?
1266
+ ["#{indent}# #{config.default_message(scope, visibility)}", "#{indent}#"]
1267
+ else
1268
+ []
1269
+ end
1270
+ end
1271
+
1272
+ # Build visibility tag lines
1273
+ #
1274
+ # @note module_function: defines #build_visibility_tag_lines (visibility: private)
1275
+ # @param [String] indent indentation string for the doc line
1276
+ # @param [Symbol] visibility method visibility symbol
1277
+ # @param [Docscribe::Config] config Docscribe configuration object
1278
+ # @return [Array<String>]
1279
+ def build_visibility_tag_lines(indent, visibility, config)
1280
+ return [] unless config.emit_visibility_tags?
1281
+
1282
+ case visibility
1283
+ when :private then ["#{indent}# @private"]
1284
+ when :protected then ["#{indent}# @protected"]
1285
+ else []
1286
+ end
1287
+ end
1288
+
1289
+ # Build module function note lines
1290
+ #
1291
+ # @note module_function: defines #build_module_function_note_lines (visibility: private)
1292
+ # @param [String] indent indentation string for the doc line
1293
+ # @param [Object] insertion the collected method insertion object
1294
+ # @param [String] name the method name string
1295
+ # @return [Array<String>]
1296
+ def build_module_function_note_lines(indent, insertion, name)
1297
+ return [] unless insertion.respond_to?(:module_function) && insertion.module_function
1298
+
1299
+ included_vis =
1300
+ if insertion.respond_to?(:included_instance_visibility) && insertion.included_instance_visibility
1301
+ insertion.included_instance_visibility
1302
+ else
1303
+ :private
1304
+ end
1305
+
1306
+ ["#{indent}# @note module_function: defines ##{name} (visibility: #{included_vis})"]
1307
+ end
1308
+
1309
+ # Build raise tag lines
1310
+ #
1311
+ # @note module_function: defines #build_raise_tag_lines (visibility: private)
1312
+ # @param [String] indent indentation string for the doc line
1313
+ # @param [Array<String>] raise_types hash tracking existing @raise types
1314
+ # @param [Docscribe::Config] config Docscribe configuration object
1315
+ # @return [Array<String>]
1316
+ def build_raise_tag_lines(indent, raise_types, config)
1317
+ return [] unless config.emit_raise_tags?
1318
+
1319
+ raise_types.map { |rt| "#{indent}# @raise [#{rt}]" }
1320
+ end
1321
+
1322
+ # Build return tag line
1323
+ #
1324
+ # @note module_function: defines #build_return_tag_line (visibility: private)
1325
+ # @param [String] indent indentation string for the doc line
1326
+ # @param [String] normal_type resolved return type
1327
+ # @param [Docscribe::Config] config Docscribe configuration object
1328
+ # @param [Symbol] scope method scope symbol
1329
+ # @param [Symbol] visibility method visibility symbol
1330
+ # @return [String, nil]
1331
+ def build_return_tag_line(indent, normal_type, config, scope, visibility)
1332
+ return unless config.emit_return_tag?(scope, visibility)
1333
+
1334
+ "#{indent}# @return [#{normal_type}]"
1335
+ end
1336
+
1337
+ # Build rescue return lines
1338
+ #
1339
+ # @note module_function: defines #build_rescue_return_lines (visibility: private)
1340
+ # @param [String] indent indentation string for the doc line
1341
+ # @param [Array<(Array<String>, String)>] rescue_specs rescue type specs
1342
+ # @param [Docscribe::Config] config Docscribe configuration object
1343
+ # @return [Array<String>]
1344
+ def build_rescue_return_lines(indent, rescue_specs, config)
1345
+ return [] unless config.emit_rescue_conditional_returns?
1346
+
1347
+ rescue_specs.map do |exceptions, rtype|
1348
+ "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
1349
+ end
1350
+ end
1351
+
1352
+ # Build plugin tag lines
1353
+ #
1354
+ # @note module_function: defines #build_plugin_tag_lines (visibility: private)
1355
+ # @param [Object] insertion the collected method insertion object
1356
+ # @param [String] indent indentation string for the doc line
1357
+ # @param [String] normal_type resolved return type
1358
+ # @param [Array<Object>, nil] override_tags plugin tag overrides
1359
+ # @return [Array<String>]
1360
+ def build_plugin_tag_lines(insertion, indent, normal_type, override_tags)
1361
+ plugin_tags = Docscribe::Plugin.run_tag_plugins(build_plugin_context(insertion, normal_type: normal_type))
1362
+ plugin_tags.concat(Array(override_tags)) if override_tags
1363
+ render_plugin_tags(plugin_tags, indent)
1364
+ end
1365
+
1366
+ # Build arg line
1367
+ #
1368
+ # @note module_function: defines #build_arg_line (visibility: private)
1369
+ # @param [Parser::AST::Node] arg_node AST node for the required argument
1370
+ # @param [String] indent indentation string for doc comment lines
1371
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1372
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1373
+ # @param [Object] opts additional options for param formatting
1374
+ # @return [String]
1375
+ def build_arg_line(arg_node, indent, external_sig, param_types_override, **opts)
1376
+ pname = arg_node.children.first.to_s
1377
+ ty = lookup_param_type(external_sig, param_types_override, pname, pname,
1378
+ infer_default: nil,
1379
+ fallback_type: opts[:fallback_type],
1380
+ treat_options_keyword_as_hash: opts[:treat_options_keyword_as_hash])
1381
+ format_param_tag(indent, pname, ty, opts[:param_documentation], style: opts[:param_tag_style])
1382
+ end
1383
+
1384
+ # Build optarg lines
1385
+ #
1386
+ # @note module_function: defines #build_optarg_lines (visibility: private)
1387
+ # @param [Parser::AST::Node] arg_node AST node for the optional argument
1388
+ # @param [String] indent indentation string for doc comment lines
1389
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1390
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1391
+ # @param [Object] opts additional options for param formatting
1392
+ # @return [Array<String>]
1393
+ def build_optarg_lines(arg_node, indent, external_sig, param_types_override, **opts)
1394
+ pname, default = *arg_node
1395
+ pname = pname.to_s
1396
+ ty = optarg_type(pname, default, external_sig, param_types_override, opts)
1397
+ lines = [format_param_tag(indent, pname, ty, opts[:param_documentation], style: opts[:param_tag_style])]
1398
+
1399
+ append_option_lines(lines, default, indent, pname, opts[:fallback_type])
1400
+ lines
1401
+ end
1402
+
1403
+ # Optarg type
1404
+ #
1405
+ # @note module_function: defines #optarg_type (visibility: private)
1406
+ # @param [String] pname the parameter name to look up
1407
+ # @param [Parser::AST::Node] default default value node
1408
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1409
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1410
+ # @param [Hash<Symbol, Object>] opts additional options including
1411
+ # @return [String]
1412
+ def optarg_type(pname, default, external_sig, param_types_override, opts)
1413
+ default_src = source_from_node(default)
1414
+ lookup_param_type(external_sig, param_types_override, pname, pname,
1415
+ infer_default: default_src,
1416
+ fallback_type: opts[:fallback_type],
1417
+ treat_options_keyword_as_hash: opts[:treat_options_keyword_as_hash])
1418
+ end
1419
+
1420
+ # Source from node
1421
+ #
1422
+ # @note module_function: defines #source_from_node (visibility: private)
1423
+ # @param [Parser::AST::Node] node AST node whose source text to extract
1424
+ # @return [String, nil]
1425
+ def source_from_node(node)
1426
+ loc = node&.loc
1427
+ loc&.expression&.source
1428
+ end
1429
+
1430
+ # Resolve infer name
1431
+ #
1432
+ # @note module_function: defines #resolve_infer_name (visibility: private)
1433
+ # @param [String] pname the parameter name to look up
1434
+ # @param [Proc, nil] infer_name parameter name string or transformed version for inference
1435
+ # @return [String]
1436
+ def resolve_infer_name(pname, infer_name)
1437
+ infer_name ? infer_name.call(pname) : pname
1438
+ end
1439
+
1440
+ # Build kwarg line
1441
+ #
1442
+ # @note module_function: defines #build_kwarg_line (visibility: private)
1443
+ # @param [Parser::AST::Node] arg_node AST node for the keyword argument
1444
+ # @param [String] indent indentation string for doc comment lines
1445
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1446
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1447
+ # @param [Object] opts additional options for param formatting
1448
+ # @return [String]
1449
+ def build_kwarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1450
+ pname = arg_node.children.first.to_s
1451
+ ty = lookup_param_type(external_sig, param_types_override, pname, "#{pname}:",
1452
+ infer_default: nil,
1453
+ fallback_type: opts[:fallback_type],
1454
+ treat_options_keyword_as_hash: opts[:treat_options_keyword_as_hash])
1455
+ format_param_tag(indent, pname, ty, opts[:param_documentation], style: opts[:param_tag_style])
1456
+ end
1457
+
1458
+ # Build kwoptarg line
1459
+ #
1460
+ # @note module_function: defines #build_kwoptarg_line (visibility: private)
1461
+ # @param [Parser::AST::Node] arg_node AST node for the optional keyword argument
1462
+ # @param [String] indent indentation string for doc comment lines
1463
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1464
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1465
+ # @param [Object] opts additional options for param formatting
1466
+ # @return [String]
1467
+ def build_kwoptarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1468
+ pname, default = *arg_node
1469
+ pname = pname.to_s
1470
+ default_loc = default&.loc
1471
+ default_src = default_loc&.expression&.source
1472
+ ty = lookup_param_type(external_sig, param_types_override, pname, "#{pname}:",
1473
+ infer_default: default_src,
1474
+ fallback_type: opts[:fallback_type],
1475
+ treat_options_keyword_as_hash: opts[:treat_options_keyword_as_hash])
1476
+ format_param_tag(indent, pname, ty, opts[:param_documentation], style: opts[:param_tag_style])
1477
+ end
1478
+
1479
+ # Build restarg line
1480
+ #
1481
+ # @note module_function: defines #build_restarg_line (visibility: private)
1482
+ # @param [Parser::AST::Node] arg_node AST node for the rest argument (*args)
1483
+ # @param [String] indent indentation string for doc comment lines
1484
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1485
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1486
+ # @param [Object] opts additional options for param formatting
1487
+ # @return [String]
1488
+ def build_restarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1489
+ pname = (arg_node.children.first || 'args').to_s
1490
+ rest_pos = external_sig&.rest_positional
1491
+ ty = if rest_pos&.element_type
1492
+ "Array<#{rest_pos.element_type}>"
1493
+ else
1494
+ lookup_param_type_by_infer(param_types_override, pname, "*#{pname}",
1495
+ opts[:fallback_type], opts[:treat_options_keyword_as_hash])
1496
+ end
1497
+ format_param_tag(indent, pname, ty, opts[:param_documentation], style: opts[:param_tag_style])
1498
+ end
1499
+
1500
+ # Build kwrestarg line
1501
+ #
1502
+ # @note module_function: defines #build_kwrestarg_line (visibility: private)
1503
+ # @param [Parser::AST::Node] arg_node AST node for the keyword rest argument (**kwargs)
1504
+ # @param [String] indent indentation string for doc comment lines
1505
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1506
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1507
+ # @param [Object] opts additional options for param formatting
1508
+ # @return [String]
1509
+ def build_kwrestarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1510
+ pname = (arg_node.children.first || 'kwargs').to_s
1511
+ ty = external_sig&.rest_keywords&.type ||
1512
+ lookup_param_type_by_infer(param_types_override, pname, "**#{pname}",
1513
+ opts[:fallback_type], opts[:treat_options_keyword_as_hash])
1514
+ format_param_tag(indent, pname, ty, opts[:param_documentation], style: opts[:param_tag_style])
1515
+ end
1516
+
1517
+ # Build blockarg line
1518
+ #
1519
+ # @note module_function: defines #build_blockarg_line (visibility: private)
1520
+ # @param [Parser::AST::Node] arg_node AST node for the block argument (&block)
1521
+ # @param [String] indent indentation string for doc comment lines
1522
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1523
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1524
+ # @param [Object] opts additional options for param formatting
1525
+ # @return [String]
1526
+ def build_blockarg_line(arg_node, indent, external_sig, param_types_override, **opts)
1527
+ pname = (arg_node.children.first || 'block').to_s
1528
+ ty = lookup_param_type(external_sig, param_types_override, pname, "&#{pname}",
1529
+ infer_default: nil,
1530
+ fallback_type: opts[:fallback_type],
1531
+ treat_options_keyword_as_hash: opts[:treat_options_keyword_as_hash])
1532
+ format_param_tag(indent, pname, ty, opts[:param_documentation], style: opts[:param_tag_style])
1533
+ end
1534
+
1535
+ # Lookup param type
1536
+ #
1537
+ # @note module_function: defines #lookup_param_type (visibility: private)
1538
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig external method signature for type overrides
1539
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1540
+ # @param [String] pname the parameter name string
1541
+ # @param [String] infer_name parameter name string or transformed version for inference
1542
+ # @param [Object] opts additional options including infer_default, fallback_type, treat_options_keyword_as_hash
1543
+ # @return [String]
1544
+ def lookup_param_type(external_sig, param_types_override, pname, infer_name, **opts)
1545
+ external_sig&.param_types&.[](pname) ||
1546
+ override_param_type_for(pname, param_types_override) ||
1547
+ Infer.infer_param_type(infer_name, opts[:infer_default],
1548
+ fallback_type: opts[:fallback_type],
1549
+ treat_options_keyword_as_hash: opts[:treat_options_keyword_as_hash])
1550
+ end
1551
+
1552
+ # Lookup param type by infer
1553
+ #
1554
+ # @note module_function: defines #lookup_param_type_by_infer (visibility: private)
1555
+ # @param [Hash<String, String>, nil] param_types_override map of parameter name to override type
1556
+ # @param [String] pname the parameter name string
1557
+ # @param [String] infer_name parameter name string or transformed version for inference
1558
+ # @param [String] fallback_type default type string when inference fails
1559
+ # @param [Boolean, nil] treat_options_keyword_as_hash whether to treat options keyword as Hash type
1560
+ # @return [String]
1561
+ def lookup_param_type_by_infer(param_types_override, pname, infer_name, fallback_type,
1562
+ treat_options_keyword_as_hash)
1563
+ override_param_type_for(pname, param_types_override) ||
1564
+ Infer.infer_param_type(infer_name, nil,
1565
+ fallback_type: fallback_type,
1566
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash || false)
1567
+ end
1568
+
1569
+ # Format param tag
1570
+ #
1571
+ # @note module_function: defines #format_param_tag (visibility: private)
1572
+ # @param [String] indent indentation string for the doc line
1573
+ # @param [String] name the parameter name
1574
+ # @param [String] type the parameter type string
1575
+ # @param [String] documentation optional documentation text appended to the tag
1576
+ # @param [Symbol, String] style param tag style (:type_name or :name_type)
1577
+ # @return [String]
1578
+ def format_param_tag(indent, name, type, documentation, style:)
1579
+ doc = documentation.to_s.strip
1580
+ type = type.to_s
1581
+ line = build_param_tag_base(indent, name, type, style)
1582
+ doc.empty? ? line : append_param_doc(line, doc, indent)
1583
+ end
1584
+
1585
+ # Build param tag base string
1586
+ #
1587
+ # @note module_function: defines #build_param_tag_base (visibility: private)
1588
+ # @param [String] indent indentation string
1589
+ # @param [String] name parameter name
1590
+ # @param [String] type parameter type string
1591
+ # @param [String, Symbol] style tag style symbol
1592
+ # @return [String]
1593
+ def build_param_tag_base(indent, name, type, style)
1594
+ case style.to_s
1595
+ when 'name_type' then "#{indent}# @param #{name} [#{type}]"
1596
+ else "#{indent}# @param [#{type}] #{name}"
1597
+ end
1598
+ end
1599
+
1600
+ # Append param doc text
1601
+ #
1602
+ # @note module_function: defines #append_param_doc (visibility: private)
1603
+ # @param [String] line existing param tag line
1604
+ # @param [String] doc documentation text
1605
+ # @param [String] indent indentation string
1606
+ # @return [String]
1607
+ def append_param_doc(line, doc, indent)
1608
+ parts = doc.split("\n")
1609
+ result = +"#{line} #{parts.first}"
1610
+ parts[1..]&.each { |l| result << "\n#{indent}# #{l}" }
1611
+ result
1612
+ end
1613
+
1614
+ # Append option lines
1615
+ #
1616
+ # @note module_function: defines #append_option_lines (visibility: private)
1617
+ # @param [Array<String>] lines array of output doc lines being accumulated
1618
+ # @param [Parser::AST::Node] default default value node
1619
+ # @param [String] indent indentation string for the doc line
1620
+ # @param [String] pname the parameter name to look up
1621
+ # @param [String] fallback_type default type string when inference fails
1622
+ # @return [void]
1623
+ def append_option_lines(lines, default, indent, pname, fallback_type)
1624
+ hash_option_pairs(default).each do |pair|
1625
+ lines << build_option_line(pair, indent, pname, fallback_type)
1626
+ end
1627
+ end
1628
+
1629
+ # Hash option pairs
1630
+ #
1631
+ # @note module_function: defines #hash_option_pairs (visibility: private)
1632
+ # @param [Parser::AST::Node] node AST node for the default value, expected to be :hash type
1633
+ # @return [Array<Parser::AST::Node>]
1634
+ def hash_option_pairs(node)
1635
+ return [] unless node&.type == :hash
1636
+
1637
+ node.children.select { |child| child.is_a?(Parser::AST::Node) && child.type == :pair }
1638
+ end
1639
+
1640
+ # Build option line
1641
+ #
1642
+ # @note module_function: defines #build_option_line (visibility: private)
1643
+ # @param [Parser::AST::Node] pair AST pair node containing key and value
1644
+ # @param [String] indent indentation string for the doc line
1645
+ # @param [String] pname the parent parameter name for @option scope
1646
+ # @param [String] fallback_type default type string when inference fails
1647
+ # @return [String]
1648
+ def build_option_line(pair, indent, pname, fallback_type)
1649
+ key_node, value_node = pair.children
1650
+ option_key = option_key_name(key_node)
1651
+ option_type = Infer::Literals.type_from_literal(value_node, fallback_type: fallback_type)
1652
+ option_default = node_default_literal(value_node)
1653
+
1654
+ line = "#{indent}# @option #{pname} [#{option_type}] :#{option_key}"
1655
+ line += " (#{option_default})" if option_default
1656
+ line += ' Description of this option.'
1657
+ line
1658
+ end
1659
+
1660
+ # Option key name
1661
+ #
1662
+ # @note module_function: defines #option_key_name (visibility: private)
1663
+ # @param [Parser::AST::Node] key_node AST node for the hash key (:sym or :str type)
1664
+ # @return [String]
1665
+ def option_key_name(key_node)
1666
+ case key_node&.type
1667
+ when :sym, :str
1668
+ key_node.children.first.to_s
1669
+ else
1670
+ expression = key_node&.loc&.expression
1671
+ expression&.source.to_s.sub(/\A:/, '')
1672
+ end
1673
+ end
1674
+
1675
+ # Node default literal
1676
+ #
1677
+ # @note module_function: defines #node_default_literal (visibility: private)
1678
+ # @param [Parser::AST::Node] node AST node whose source text to extract
1679
+ # @return [String, nil]
1680
+ def node_default_literal(node)
1681
+ expression = node&.loc&.expression
1682
+ expression&.source
1683
+ end
1684
+
1685
+ # Override param type for
1686
+ #
1687
+ # @note module_function: defines #override_param_type_for (visibility: private)
1688
+ # @param [String] pname the parameter name to look up
1689
+ # @param [Hash<Object, Object>, nil] override_map hash map of parameter name to override type
1690
+ # @return [String, nil]
1691
+ def override_param_type_for(pname, override_map)
1692
+ return nil unless override_map
1693
+
1694
+ key = pname.to_s
1695
+ override_map[key] || override_map[:"#{key}"] || override_map["#{key}:"] || override_map[:"#{key}:"]
1696
+ end
1697
+
1698
+ # Extract param description
1699
+ #
1700
+ # @note module_function: defines #extract_param_description (visibility: private)
1701
+ # @param [String] line a `@param` tag line
1702
+ # @return [String, nil]
1703
+ def extract_param_description(line)
1704
+ after = param_rest_after_type(line)
1705
+ return nil unless after
1706
+
1707
+ parts = after.split(/\s+/, 2)
1708
+ parts[1] if parts.length > 1 && !parts[1].empty?
1709
+ end
1710
+
1711
+ # Extract everything after the type bracket in a @param line.
1712
+ #
1713
+ # @note module_function: defines #param_rest_after_type (visibility: private)
1714
+ # @param [String] line a @param doc line
1715
+ # @return [String?] the text after the closing `]`, or nil
1716
+ def param_rest_after_type(line)
1717
+ content = line.sub(/^\s*#\s*/, '')
1718
+ if (m = content.match(/@param\s+(\S+\s+)?\[/))
1719
+ brace_end = m.end(0) #: Integer
1720
+ rest = content[(brace_end - 1)..] #: String
1721
+ type_end = find_matching_close_bracket(rest)
1722
+ return rest[(type_end + 1)..]&.strip if type_end
1723
+ end
1724
+ nil
1725
+ end
1726
+ ARG_DEFAULT_NAMES = { restarg: 'args', kwrestarg: 'kwargs', blockarg: 'block' }.freeze
1727
+
1728
+ # Param name from arg
1729
+ #
1730
+ # @note module_function: defines #param_name_from_arg (visibility: private)
1731
+ # @param [Parser::AST::Node] arg_node AST node for the block argument (&block)
1732
+ # @return [String, nil]
1733
+ def param_name_from_arg(arg_node)
1734
+ return nil if arg_node.type == :forward_arg
1735
+
1736
+ (arg_node.children.first || ARG_DEFAULT_NAMES[arg_node.type] || '').to_s
1737
+ end
1738
+
1739
+ # Extract param name from param line
1740
+ #
1741
+ # @note module_function: defines #extract_param_name_from_param_line (visibility: private)
738
1742
  # @param [String] line a `@param` doc line
739
1743
  # @return [String, nil] the parameter name or nil
740
1744
  def extract_param_name_from_param_line(line)
741
- return Regexp.last_match(1) if line =~ /@param\b\s+\[[^\]]+\]\s+(\S+)/
742
- return Regexp.last_match(1) if line =~ /@param\b\s+(\S+)\s+\[[^\]]+\]/
1745
+ content = line.sub(/^\s*#\s*/, '')
1746
+ if (m = content.match(/@param\s+(\S+)\s+\[/))
1747
+ return m[1]
1748
+ elsif (m = content.match(/@param\s+\[/))
1749
+ name_end = m.end(0) #: Integer
1750
+ rest = content[(name_end - 1)..] #: String
1751
+ type_end = find_matching_close_bracket(rest)
1752
+ return name_after_type_bracket(rest, type_end) if type_end
1753
+ end
743
1754
 
744
1755
  nil
745
1756
  end
746
1757
 
747
- # Method documentation.
1758
+ # Extract name after type bracket
748
1759
  #
749
- # @note module_function: when included, also defines #extract_param_type_from_param_line (instance visibility: private)
750
- # @param [Object] line Param documentation.
751
- # @return [Object]
1760
+ # @note module_function: defines #name_after_type_bracket (visibility: private)
1761
+ # @param [String] rest tag content after bracket
1762
+ # @param [Integer] type_end closing bracket position
1763
+ # @return [String?]
1764
+ def name_after_type_bracket(rest, type_end)
1765
+ rest[(type_end + 1)..].to_s.strip.split(/\s+/).first
1766
+ end
1767
+
1768
+ # Extract param type from param line
1769
+ #
1770
+ # @note module_function: defines #extract_param_type_from_param_line (visibility: private)
1771
+ # @param [String] line a `@param` tag line
1772
+ # @return [String, nil]
752
1773
  def extract_param_type_from_param_line(line)
753
- if (m = line.match(/@param\s+\[([^\]]+)\]\s+\S+/) || line.match(/@param\s+\S+\s+\[([^\]]+)\]/))
754
- m[1]
1774
+ content = line.sub(/^\s*#\s*/, '')
1775
+ if (m = content.match(/@param\s+(\S+\s+)?\[/))
1776
+ name_end = m.end(0) #: Integer
1777
+ rest = content[(name_end - 1)..] #: String
1778
+ type_end = find_matching_close_bracket(rest)
1779
+ return rest[1...type_end] if type_end
1780
+ end
1781
+ nil
1782
+ end
1783
+
1784
+ # Find matching close bracket
1785
+ #
1786
+ # @note module_function: defines #find_matching_close_bracket (visibility: private)
1787
+ # @param [String] str string to scan
1788
+ # @return [Integer, nil]
1789
+ def find_matching_close_bracket(str)
1790
+ depth = 0
1791
+ str.each_char.with_index do |c, i|
1792
+ case c
1793
+ when '[' then depth += 1
1794
+ when ']'
1795
+ depth -= 1
1796
+ return i if depth.zero?
1797
+ end
1798
+ end
1799
+ nil
1800
+ end
1801
+
1802
+ # Collect missing raises
1803
+ #
1804
+ # @note module_function: defines #collect_missing_raises! (visibility: private)
1805
+ # @param [Array<String>] lines array of output doc lines being accumulated
1806
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1807
+ # @param [Object] ctx merged context hash with info and indent
1808
+ # @return [void]
1809
+ def collect_missing_raises!(lines, reasons, **ctx)
1810
+ return unless ctx[:config].emit_raise_tags?
1811
+
1812
+ inferred = Docscribe::Infer.infer_raises_from_node(ctx[:node])
1813
+ existing = ctx[:info][:raise_types] || {}
1814
+ missing = inferred.reject { |rt| existing[rt] }
1815
+
1816
+ missing.each do |rt|
1817
+ lines << "#{ctx[:indent]}# @raise [#{rt}]\n"
1818
+ reasons << { type: :missing_raise, message: "missing @raise [#{rt}]", extra: { raise_type: rt } }
755
1819
  end
756
1820
  end
757
1821
 
758
- # Build a Plugin::Context from a collected insertion.
1822
+ # Collect missing return
759
1823
  #
760
- # @note module_function
761
- # @note module_function: when included, also defines #build_plugin_context (instance visibility: private)
762
- # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
1824
+ # @note module_function: defines #collect_missing_return! (visibility: private)
1825
+ # @param [Array<String>] lines array of output doc lines being accumulated
1826
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1827
+ # @param [Object] ctx merged context hash with info and indent
1828
+ # @return [void]
1829
+ def collect_missing_return!(lines, reasons, **ctx)
1830
+ return unless ctx[:config].emit_return_tag?(ctx[:scope], ctx[:visibility])
1831
+
1832
+ if !ctx[:info][:has_return]
1833
+ record_missing_return(lines, reasons, ctx)
1834
+ elsif return_type_changed?(ctx)
1835
+ record_updated_return(lines, reasons, ctx)
1836
+ end
1837
+ end
1838
+
1839
+ # Record missing return
1840
+ #
1841
+ # @note module_function: defines #record_missing_return (visibility: private)
1842
+ # @param [Array<String>] lines array of output doc lines being accumulated
1843
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1844
+ # @param [Hash<Symbol, Object>] ctx merged context hash with normal_type and indent
1845
+ # @return [void]
1846
+ def record_missing_return(lines, reasons, ctx)
1847
+ lines << "#{ctx[:indent]}# @return [#{ctx[:normal_type]}]\n"
1848
+ reasons << { type: :missing_return, message: 'missing @return' }
1849
+ end
1850
+
1851
+ # Record updated return
1852
+ #
1853
+ # @note module_function: defines #record_updated_return (visibility: private)
1854
+ # @param [Array<String>] lines array of output doc lines being accumulated
1855
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1856
+ # @param [Hash<Symbol, Object>] ctx merged context hash with normal_type and info
1857
+ # @return [void]
1858
+ def record_updated_return(lines, reasons, ctx)
1859
+ lines << "#{ctx[:indent]}# @return [#{ctx[:normal_type]}]\n" unless ctx[:strategy] == :safe
1860
+ reasons << { type: :updated_return,
1861
+ message: "updated @return from #{ctx[:info][:return_type]} to #{ctx[:normal_type]}" }
1862
+ end
1863
+
1864
+ # Return type changed
1865
+ #
1866
+ # @note module_function: defines #return_type_changed? (visibility: private)
1867
+ # @param [Hash<Symbol, Object>] ctx merged context hash with external_sig, info, and normal_type
1868
+ # @return [Boolean]
1869
+ def return_type_changed?(ctx)
1870
+ ctx[:external_sig] && ctx[:info][:return_type] && ctx[:info][:return_type] != ctx[:normal_type]
1871
+ end
1872
+
1873
+ # Collect missing rescue returns
1874
+ #
1875
+ # @note module_function: defines #collect_missing_rescue_returns! (visibility: private)
1876
+ # @param [Array<String>] lines array of output doc lines being accumulated
1877
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1878
+ # @param [Object] ctx merged context hash with info and indent
1879
+ # @return [void]
1880
+ def collect_missing_rescue_returns!(lines, reasons, **ctx)
1881
+ return unless ctx[:config].emit_rescue_conditional_returns?
1882
+ return if ctx[:info][:has_return]
1883
+
1884
+ ctx[:rescue_specs].each do |exceptions, rtype|
1885
+ lines << "#{ctx[:indent]}# @return [#{rtype}] if #{exceptions.join(', ')}\n"
1886
+ reasons << {
1887
+ type: :missing_return,
1888
+ message: "missing conditional @return for #{exceptions.join(', ')}"
1889
+ }
1890
+ end
1891
+ end
1892
+
1893
+ # Collect missing plugin tags
1894
+ #
1895
+ # @note module_function: defines #collect_missing_plugin_tags! (visibility: private)
1896
+ # @param [Array<String>] lines array of output doc lines being accumulated
1897
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1898
+ # @param [Object] ctx merged context hash with info and indent
1899
+ # @return [void]
1900
+ def collect_missing_plugin_tags!(lines, reasons, **ctx)
1901
+ plugin_tags = Docscribe::Plugin.run_tag_plugins(build_plugin_context(ctx[:insertion],
1902
+ normal_type: ctx[:normal_type]))
1903
+ plugin_tags.concat(Array(ctx[:override_tags])) if ctx[:override_tags]
1904
+
1905
+ plugin_tags.each { |tag| record_plugin_tag(tag, lines, reasons, ctx) }
1906
+ end
1907
+
1908
+ # Record plugin tag
1909
+ #
1910
+ # @note module_function: defines #record_plugin_tag (visibility: private)
1911
+ # @param [Object] tag plugin tag object to render and record
1912
+ # @param [Array<String>] lines array of output doc lines being accumulated
1913
+ # @param [Array<Hash<Symbol, Object>>] reasons array of reason hashes for --explain output
1914
+ # @param [Hash<Symbol, Object>] ctx merged context hash with info and indent
1915
+ # @return [void]
1916
+ def record_plugin_tag(tag, lines, reasons, ctx)
1917
+ return if ctx[:info][:plugin_tags]&.[](tag.name)
1918
+
1919
+ rendered = render_plugin_tags([tag], ctx[:indent]).first
1920
+ lines << "#{rendered}\n"
1921
+ reasons << { type: :missing_plugin_tag, message: "missing @#{tag.name}" }
1922
+ end
1923
+
1924
+ # Debug warn
1925
+ #
1926
+ # @note module_function: defines #debug_warn (visibility: private)
1927
+ # @param [StandardError] error the error that occurred
1928
+ # @param [Object] insertion the method insertion being processed
1929
+ # @param [String] name the method name
1930
+ # @param [String] phase the processing phase
1931
+ # @return [void]
1932
+ def debug_warn(error, insertion:, name:, phase:)
1933
+ return unless debug?
1934
+
1935
+ where = build_debug_location(insertion, name)
1936
+ warn "Docscribe DEBUG: #{phase} failed at #{where}: #{error.class}: #{error.message}"
1937
+ end
1938
+
1939
+ # Build debug location
1940
+ #
1941
+ # @note module_function: defines #build_debug_location (visibility: private)
1942
+ # @param [Object] insertion the collected method insertion object
1943
+ # @param [String] name the method name string
1944
+ # @return [String]
1945
+ def build_debug_location(insertion, name)
1946
+ return name.to_s unless insertion
1947
+
1948
+ expr = insertion.node.loc.expression
1949
+ buf = expr.source_buffer.name
1950
+ sym = insertion.scope == :class ? '.' : '#'
1951
+ ctr = insertion.container || 'Object'
1952
+ +"#{buf}:#{expr.line} #{ctr}#{sym}#{name}"
1953
+ end
1954
+
1955
+ # Debug
1956
+ #
1957
+ # @note module_function: defines #debug? (visibility: private)
1958
+ # @return [Boolean]
1959
+ def debug?
1960
+ ENV['DOCSCRIBE_DEBUG'] == '1'
1961
+ end
1962
+
1963
+ # Build plugin context
1964
+ #
1965
+ # @note module_function: defines #build_plugin_context (visibility: private)
1966
+ # @param [Object] insertion the collected method insertion object
763
1967
  # @param [String] normal_type resolved return type
764
- # @raise [StandardError]
765
- # @return [Docscribe::Plugin::Context]
1968
+ # @return [Object]
766
1969
  def build_plugin_context(insertion, normal_type:)
767
1970
  node = insertion.node
768
- source = begin
769
- node.loc.expression.source
770
- rescue StandardError
771
- ''
772
- end
1971
+ source = safe_node_source(node)
1972
+ new_plugin_context(insertion, node, source, normal_type)
1973
+ end
773
1974
 
1975
+ # New plugin context
1976
+ #
1977
+ # @note module_function: defines #new_plugin_context (visibility: private)
1978
+ # @param [Object] insertion the collected method insertion object
1979
+ # @param [Parser::AST::Node] node AST node whose source text to extract
1980
+ # @param [String] source method source text
1981
+ # @param [String] normal_type resolved return type
1982
+ # @return [Object]
1983
+ def new_plugin_context(insertion, node, source, normal_type)
774
1984
  Docscribe::Plugin::Context.new(
775
1985
  node: node,
776
1986
  container: insertion.container,
777
1987
  scope: insertion.scope,
778
1988
  visibility: insertion.visibility,
779
- method_name: SourceHelpers.node_name(node),
1989
+ method_name: SourceHelpers.node_name(node), #: Symbol
780
1990
  inferred_params: {},
781
1991
  inferred_return: normal_type,
782
1992
  source: source
783
1993
  )
784
1994
  end
785
1995
 
786
- # Render plugin tags as indented comment lines.
1996
+ # Safe node source
1997
+ #
1998
+ # @note module_function: defines #safe_node_source (visibility: private)
1999
+ # @param [Parser::AST::Node] node AST node whose source text to extract
2000
+ # @raise [StandardError]
2001
+ # @return [String] if StandardError
2002
+ # @return [String] if StandardError
2003
+ def safe_node_source(node)
2004
+ node.loc.expression.source
2005
+ rescue StandardError
2006
+ ''
2007
+ end
2008
+
2009
+ # Render plugin tags
787
2010
  #
788
- # @note module_function
789
- # @note module_function: when included, also defines #render_plugin_tags (instance visibility: private)
790
- # @param [Array<Docscribe::Plugin::Tag>] tags
791
- # @param [String] indent
2011
+ # @note module_function: defines #render_plugin_tags (visibility: private)
2012
+ # @param [Array<Object>] tags plugin tag objects
2013
+ # @param [String] indent indentation string for the doc line
792
2014
  # @return [Array<String>]
793
2015
  def render_plugin_tags(tags, indent)
794
2016
  tags.map do |tag|
@@ -797,39 +2019,6 @@ module Docscribe
797
2019
  "#{indent}# @#{tag.name}#{type_part}#{text_part}"
798
2020
  end
799
2021
  end
800
-
801
- # Print a debug warning for a failed doc build phase.
802
- #
803
- # @note module_function: when included, also defines #debug_warn (instance visibility: private)
804
- # @param [StandardError] e the error that occurred
805
- # @param [Collector::Insertion] insertion the method insertion being processed
806
- # @param [String] name the method name
807
- # @param [String] phase the processing phase
808
- # @return [void]
809
- def debug_warn(e, insertion:, name:, phase:)
810
- return unless debug?
811
-
812
- node = insertion&.node
813
- buf_name = node&.loc&.expression&.source_buffer&.name || '(unknown)'
814
- line = node&.loc&.expression&.line
815
- scope = insertion&.scope
816
- method_symbol = scope == :class ? '.' : '#'
817
- container = insertion&.container || 'Object'
818
-
819
- where = +buf_name.to_s
820
- where << ":#{line}" if line
821
- where << " #{container}#{method_symbol}#{name}"
822
-
823
- warn "Docscribe DEBUG: #{phase} failed at #{where}: #{e.class}: #{e.message}"
824
- end
825
-
826
- # Check whether debug mode is enabled.
827
- #
828
- # @note module_function: when included, also defines #debug? (instance visibility: private)
829
- # @return [Boolean]
830
- def debug?
831
- ENV['DOCSCRIBE_DEBUG'] == '1'
832
- end
833
2022
  end
834
2023
  end
835
2024
  end