docscribe 1.0.0 → 1.2.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +692 -180
  3. data/exe/docscribe +2 -74
  4. data/lib/docscribe/cli/config_builder.rb +62 -0
  5. data/lib/docscribe/cli/init.rb +58 -0
  6. data/lib/docscribe/cli/options.rb +204 -0
  7. data/lib/docscribe/cli/run.rb +415 -0
  8. data/lib/docscribe/cli.rb +31 -0
  9. data/lib/docscribe/config/defaults.rb +71 -0
  10. data/lib/docscribe/config/emit.rb +126 -0
  11. data/lib/docscribe/config/filtering.rb +160 -0
  12. data/lib/docscribe/config/loader.rb +59 -0
  13. data/lib/docscribe/config/rbs.rb +51 -0
  14. data/lib/docscribe/config/sorbet.rb +87 -0
  15. data/lib/docscribe/config/sorting.rb +23 -0
  16. data/lib/docscribe/config/template.rb +176 -0
  17. data/lib/docscribe/config/utils.rb +102 -0
  18. data/lib/docscribe/config.rb +20 -230
  19. data/lib/docscribe/infer/ast_walk.rb +28 -0
  20. data/lib/docscribe/infer/constants.rb +11 -0
  21. data/lib/docscribe/infer/literals.rb +55 -0
  22. data/lib/docscribe/infer/names.rb +43 -0
  23. data/lib/docscribe/infer/params.rb +62 -0
  24. data/lib/docscribe/infer/raises.rb +68 -0
  25. data/lib/docscribe/infer/returns.rb +171 -0
  26. data/lib/docscribe/infer.rb +110 -259
  27. data/lib/docscribe/inline_rewriter/collector.rb +845 -0
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +383 -0
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +605 -0
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +228 -0
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +244 -0
  32. data/lib/docscribe/inline_rewriter.rb +604 -425
  33. data/lib/docscribe/parsing.rb +120 -0
  34. data/lib/docscribe/types/provider_chain.rb +37 -0
  35. data/lib/docscribe/types/rbs/provider.rb +213 -0
  36. data/lib/docscribe/types/rbs/type_formatter.rb +132 -0
  37. data/lib/docscribe/types/signature.rb +65 -0
  38. data/lib/docscribe/types/sorbet/base_provider.rb +217 -0
  39. data/lib/docscribe/types/sorbet/rbi_provider.rb +35 -0
  40. data/lib/docscribe/types/sorbet/source_provider.rb +25 -0
  41. data/lib/docscribe/version.rb +1 -1
  42. data/lib/docscribe.rb +1 -0
  43. metadata +85 -17
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -11
  46. data/.rubocop_todo.yml +0 -73
  47. data/CODE_OF_CONDUCT.md +0 -84
  48. data/Gemfile +0 -6
  49. data/Gemfile.lock +0 -73
  50. data/Rakefile +0 -12
  51. data/rakelib/docs.rake +0 -73
  52. data/stingray_docs_internal.gemspec +0 -41
@@ -0,0 +1,605 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'docscribe/infer'
4
+ require 'docscribe/inline_rewriter/source_helpers'
5
+
6
+ module Docscribe
7
+ module InlineRewriter
8
+ # Build generated YARD-style doc lines for methods and attribute helpers.
9
+ #
10
+ # DocBuilder combines:
11
+ # - Ruby visibility/container metadata from Collector
12
+ # - optional external signatures from Sorbet/RBS providers
13
+ # - fallback AST inference from Docscribe::Infer
14
+ #
15
+ # It is responsible for producing complete doc blocks for aggressive mode
16
+ # and "missing lines only" payloads for safe merge mode.
17
+ module DocBuilder
18
+ module_function
19
+
20
+ # Build a complete doc block for one collected method insertion.
21
+ #
22
+ # External signatures, when available, override inferred param and return
23
+ # types.
24
+ #
25
+ # @note module_function: when included, also defines #build (instance visibility: private)
26
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
27
+ # @param [Docscribe::Config] config
28
+ # @param [Object, nil] signature_provider provider responding to
29
+ # `signature_for(container:, scope:, name:)`
30
+ # @raise [StandardError]
31
+ # @return [String, nil]
32
+ def build(insertion, config:, signature_provider: nil)
33
+ node = insertion.node
34
+ name = SourceHelpers.node_name(node)
35
+ return nil unless name
36
+
37
+ indent = SourceHelpers.line_indent(node)
38
+ scope = insertion.scope
39
+ visibility = insertion.visibility
40
+ container = insertion.container
41
+ method_symbol = scope == :instance ? '#' : '.'
42
+
43
+ external_sig = signature_provider&.signature_for(
44
+ container: container,
45
+ scope: scope,
46
+ name: name
47
+ )
48
+
49
+ if config.emit_param_tags?
50
+ params_lines = build_params_lines(node, indent, external_sig: external_sig,
51
+ config: config)
52
+ end
53
+ raise_types = config.emit_raise_tags? ? Docscribe::Infer.infer_raises_from_node(node) : []
54
+
55
+ returns_spec = Docscribe::Infer.returns_spec_from_node(
56
+ node,
57
+ fallback_type: config.fallback_type,
58
+ nil_as_optional: config.nil_as_optional?
59
+ )
60
+
61
+ normal_type = external_sig&.return_type || returns_spec[:normal]
62
+ rescue_specs = returns_spec[:rescues] || []
63
+
64
+ lines = []
65
+
66
+ if config.emit_header?
67
+ lines << "#{indent}# +#{container}#{method_symbol}#{name}+ -> #{normal_type}"
68
+ lines << "#{indent}#"
69
+ end
70
+
71
+ lines << "#{indent}# #{config.default_message(scope, visibility)}"
72
+ lines << "#{indent}#"
73
+
74
+ if config.emit_visibility_tags?
75
+ case visibility
76
+ when :private
77
+ lines << "#{indent}# @private"
78
+ when :protected
79
+ lines << "#{indent}# @protected"
80
+ end
81
+ end
82
+
83
+ if insertion.respond_to?(:module_function) && insertion.module_function
84
+ included_vis =
85
+ if insertion.respond_to?(:included_instance_visibility) && insertion.included_instance_visibility
86
+ insertion.included_instance_visibility
87
+ else
88
+ :private
89
+ end
90
+
91
+ lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
92
+ "(instance visibility: #{included_vis})"
93
+ end
94
+
95
+ lines.concat(params_lines) if params_lines
96
+ raise_types.each { |rt| lines << "#{indent}# @raise [#{rt}]" } if config.emit_raise_tags?
97
+ lines << "#{indent}# @return [#{normal_type}]" if config.emit_return_tag?(scope, visibility)
98
+
99
+ if config.emit_rescue_conditional_returns?
100
+ rescue_specs.each do |exceptions, rtype|
101
+ lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
102
+ end
103
+ end
104
+
105
+ lines.map { |l| "#{l}\n" }.join
106
+ rescue StandardError => e
107
+ debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build')
108
+ nil
109
+ end
110
+
111
+ # Build only the missing doc lines that should be merged into an existing
112
+ # doc-like block.
113
+ #
114
+ # This is used by safe mode for non-destructive updates.
115
+ #
116
+ # @note module_function: when included, also defines #build_merge_additions (instance visibility: private)
117
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
118
+ # @param [Array<String>] existing_lines
119
+ # @param [Docscribe::Config] config
120
+ # @param [Object, nil] signature_provider
121
+ # @raise [StandardError]
122
+ # @return [String, nil]
123
+ def build_merge_additions(insertion, existing_lines:, config:, signature_provider: nil)
124
+ node = insertion.node
125
+ name = SourceHelpers.node_name(node)
126
+ return '' unless name
127
+
128
+ indent = SourceHelpers.line_indent(node)
129
+ info = parse_existing_doc_tags(existing_lines)
130
+ scope = insertion.scope
131
+ visibility = insertion.visibility
132
+
133
+ external_sig = signature_provider&.signature_for(
134
+ container: insertion.container,
135
+ scope: scope,
136
+ name: name
137
+ )
138
+
139
+ returns_spec = Docscribe::Infer.returns_spec_from_node(
140
+ node,
141
+ fallback_type: config.fallback_type,
142
+ nil_as_optional: config.nil_as_optional?
143
+ )
144
+
145
+ normal_type = external_sig&.return_type || returns_spec[:normal]
146
+ rescue_specs = returns_spec[:rescues] || []
147
+
148
+ lines = []
149
+ lines << "#{indent}#" if existing_lines.any? && existing_lines.last.strip != '#'
150
+
151
+ if config.emit_visibility_tags?
152
+ if visibility == :private && !info[:has_private]
153
+ lines << "#{indent}# @private"
154
+ elsif visibility == :protected && !info[:has_protected]
155
+ lines << "#{indent}# @protected"
156
+ end
157
+ end
158
+
159
+ if insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
160
+ included_vis = insertion.included_instance_visibility || :private
161
+ lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
162
+ "(instance visibility: #{included_vis})"
163
+ end
164
+
165
+ if config.emit_param_tags?
166
+ all_params = build_params_lines(node, indent, external_sig: external_sig, config: config)
167
+ all_params&.each do |pl|
168
+ pname = extract_param_name_from_param_line(pl)
169
+ next if pname.nil? || info[:param_names].include?(pname)
170
+
171
+ lines << pl
172
+ end
173
+ end
174
+
175
+ if config.emit_raise_tags?
176
+ inferred = Docscribe::Infer.infer_raises_from_node(node)
177
+ existing = info[:raise_types] || {}
178
+ missing = inferred.reject { |rt| existing[rt] }
179
+ missing.each { |rt| lines << "#{indent}# @raise [#{rt}]" }
180
+ end
181
+
182
+ if config.emit_return_tag?(scope, visibility) && !info[:has_return]
183
+ lines << "#{indent}# @return [#{normal_type}]"
184
+ end
185
+
186
+ if config.emit_rescue_conditional_returns? && !info[:has_return]
187
+ rescue_specs.each do |exceptions, rtype|
188
+ lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
189
+ end
190
+ end
191
+
192
+ useful = lines.reject { |l| l.strip == '#' }
193
+ return '' if useful.empty?
194
+
195
+ lines.map { |l| "#{l}\n" }.join
196
+ rescue StandardError => e
197
+ debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_merge_additions')
198
+ nil
199
+ end
200
+
201
+ # Build structured missing-line information for safe merge mode.
202
+ #
203
+ # Returns both:
204
+ # - generated missing lines
205
+ # - structured reasons used by `--explain`
206
+ #
207
+ # @note module_function: when included, also defines #build_missing_merge_result (instance visibility: private)
208
+ # @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
209
+ # @param [Array<String>] existing_lines
210
+ # @param [Docscribe::Config] config
211
+ # @param [Object, nil] signature_provider
212
+ # @raise [StandardError]
213
+ # @return [Hash]
214
+ def build_missing_merge_result(insertion, existing_lines:, config:, signature_provider: nil)
215
+ node = insertion.node
216
+ name = SourceHelpers.node_name(node)
217
+ return { lines: [], reasons: [] } unless name
218
+
219
+ indent = SourceHelpers.line_indent(node)
220
+ info = parse_existing_doc_tags(existing_lines)
221
+ scope = insertion.scope
222
+ visibility = insertion.visibility
223
+
224
+ external_sig = signature_provider&.signature_for(
225
+ container: insertion.container,
226
+ scope: scope,
227
+ name: name
228
+ )
229
+
230
+ returns_spec = Docscribe::Infer.returns_spec_from_node(
231
+ node,
232
+ fallback_type: config.fallback_type,
233
+ nil_as_optional: config.nil_as_optional?
234
+ )
235
+
236
+ normal_type = external_sig&.return_type || returns_spec[:normal]
237
+ rescue_specs = returns_spec[:rescues] || []
238
+
239
+ lines = []
240
+ reasons = []
241
+
242
+ if config.emit_visibility_tags?
243
+ if visibility == :private && !info[:has_private]
244
+ lines << "#{indent}# @private\n"
245
+ reasons << { type: :missing_visibility, message: 'missing @private' }
246
+ elsif visibility == :protected && !info[:has_protected]
247
+ lines << "#{indent}# @protected\n"
248
+ reasons << { type: :missing_visibility, message: 'missing @protected' }
249
+ end
250
+ end
251
+
252
+ if insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
253
+ included_vis = insertion.included_instance_visibility || :private
254
+ lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
255
+ "(instance visibility: #{included_vis})\n"
256
+ reasons << { type: :missing_module_function_note, message: 'missing module_function note' }
257
+ end
258
+
259
+ if config.emit_param_tags?
260
+ all_params = build_params_lines(node, indent, external_sig: external_sig, config: config)
261
+
262
+ all_params&.each do |pl|
263
+ pname = extract_param_name_from_param_line(pl)
264
+ next if pname.nil? || info[:param_names].include?(pname)
265
+
266
+ lines << "#{pl}\n"
267
+ reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
268
+ end
269
+ end
270
+
271
+ if config.emit_raise_tags?
272
+ inferred = Docscribe::Infer.infer_raises_from_node(node)
273
+ existing = info[:raise_types] || {}
274
+ missing = inferred.reject { |rt| existing[rt] }
275
+
276
+ missing.each do |rt|
277
+ lines << "#{indent}# @raise [#{rt}]\n"
278
+ reasons << { type: :missing_raise, message: "missing @raise [#{rt}]", extra: { raise_type: rt } }
279
+ end
280
+ end
281
+
282
+ if config.emit_return_tag?(scope, visibility) && !info[:has_return]
283
+ lines << "#{indent}# @return [#{normal_type}]\n"
284
+ reasons << { type: :missing_return, message: 'missing @return' }
285
+ end
286
+
287
+ if config.emit_rescue_conditional_returns? && !info[:has_return]
288
+ rescue_specs.each do |exceptions, rtype|
289
+ lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}\n"
290
+ reasons << {
291
+ type: :missing_return,
292
+ message: "missing conditional @return for #{exceptions.join(', ')}"
293
+ }
294
+ end
295
+ end
296
+
297
+ { lines: lines, reasons: reasons }
298
+ rescue StandardError => e
299
+ debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_missing_merge_result')
300
+ { lines: [], reasons: [] }
301
+ end
302
+
303
+ # Method documentation.
304
+ #
305
+ # @note module_function: when included, also defines #parse_existing_doc_tags (instance visibility: private)
306
+ # @param [Object] lines Param documentation.
307
+ # @return [Hash]
308
+ def parse_existing_doc_tags(lines)
309
+ param_names = {}
310
+ has_return = false
311
+ has_private = false
312
+ has_protected = false
313
+ has_module_function_note = false
314
+ raise_types = {}
315
+
316
+ Array(lines).each do |line|
317
+ if (pname = extract_param_name_from_param_line(line))
318
+ param_names[pname] = true
319
+ end
320
+
321
+ has_return ||= line.match?(/^\s*#\s*@return\b/)
322
+ has_private ||= line.match?(/^\s*#\s*@private\b/)
323
+ has_protected ||= line.match?(/^\s*#\s*@protected\b/)
324
+ has_module_function_note ||= line.match?(/^\s*#\s*@note\s+module_function:/)
325
+
326
+ extract_raise_types_from_line(line).each { |t| raise_types[t] = true }
327
+ end
328
+
329
+ {
330
+ param_names: param_names,
331
+ has_return: has_return,
332
+ raise_types: raise_types,
333
+ has_private: has_private,
334
+ has_protected: has_protected,
335
+ has_module_function_note: has_module_function_note
336
+ }
337
+ end
338
+
339
+ # Method documentation.
340
+ #
341
+ # @note module_function: when included, also defines #extract_raise_types_from_line (instance visibility: private)
342
+ # @param [Object] line Param documentation.
343
+ # @raise [StandardError]
344
+ # @return [Object]
345
+ # @return [Array] if StandardError
346
+ def extract_raise_types_from_line(line)
347
+ return [] unless line.match?(/^\s*#\s*@raise\b/)
348
+
349
+ if (m = line.match(/^\s*#\s*@raise\s*\[([^\]]+)\]/))
350
+ parse_raise_bracket_list(m[1])
351
+ elsif (m = line.match(/^\s*#\s*@raise\s+([A-Z]\w*(?:::[A-Z]\w*)*)/))
352
+ [m[1]]
353
+ else
354
+ []
355
+ end
356
+ rescue StandardError
357
+ []
358
+ end
359
+
360
+ # Method documentation.
361
+ #
362
+ # @note module_function: when included, also defines #parse_raise_bracket_list (instance visibility: private)
363
+ # @param [Object] s Param documentation.
364
+ # @return [Object]
365
+ def parse_raise_bracket_list(s)
366
+ s.to_s.split(',').map(&:strip).reject(&:empty?)
367
+ end
368
+
369
+ # Build generated `@param` / `@option` lines for a method node.
370
+ #
371
+ # External signatures take precedence over inferred parameter types.
372
+ #
373
+ # @note module_function: when included, also defines #build_params_lines (instance visibility: private)
374
+ # @param [Parser::AST::Node] node
375
+ # @param [String] indent
376
+ # @param [Docscribe::Types::MethodSignature, nil] external_sig
377
+ # @param [Docscribe::Config] config
378
+ # @return [Array<String>, nil]
379
+ def build_params_lines(node, indent, external_sig:, config:)
380
+ fallback_type = config.fallback_type
381
+ treat_options_keyword_as_hash = config.treat_options_keyword_as_hash?
382
+ param_tag_style = config.param_tag_style
383
+ param_documentation = config.param_documentation
384
+
385
+ args =
386
+ case node.type
387
+ when :def then node.children[1]
388
+ when :defs then node.children[2]
389
+ end
390
+
391
+ return nil unless args
392
+
393
+ params = []
394
+
395
+ (args.children || []).each do |a|
396
+ case a.type
397
+ when :arg
398
+ pname = a.children.first.to_s
399
+ ty = external_sig&.param_types&.[](pname) ||
400
+ Infer.infer_param_type(
401
+ pname,
402
+ nil,
403
+ fallback_type: fallback_type,
404
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
405
+ )
406
+ params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
407
+
408
+ when :optarg
409
+ pname, default = *a
410
+ pname = pname.to_s
411
+ default_src = default&.loc&.expression&.source
412
+ ty = external_sig&.param_types&.[](pname) ||
413
+ Infer.infer_param_type(
414
+ pname,
415
+ default_src,
416
+ fallback_type: fallback_type,
417
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
418
+ )
419
+ params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
420
+
421
+ hash_option_pairs(default).each do |pair|
422
+ key_node, value_node = pair.children
423
+ option_key = option_key_name(key_node)
424
+ option_type = Infer::Literals.type_from_literal(value_node, fallback_type: fallback_type)
425
+ option_default = node_default_literal(value_node)
426
+
427
+ line = "#{indent}# @option #{pname} [#{option_type}] :#{option_key}"
428
+ line += " (#{option_default})" if option_default
429
+ line += ' Option documentation.'
430
+ params << line
431
+ end
432
+
433
+ when :kwarg
434
+ pname = a.children.first.to_s
435
+ ty = external_sig&.param_types&.[](pname) ||
436
+ Infer.infer_param_type(
437
+ "#{pname}:",
438
+ nil,
439
+ fallback_type: fallback_type,
440
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
441
+ )
442
+ params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
443
+
444
+ when :kwoptarg
445
+ pname, default = *a
446
+ pname = pname.to_s
447
+ default_src = default&.loc&.expression&.source
448
+ ty = external_sig&.param_types&.[](pname) ||
449
+ Infer.infer_param_type(
450
+ "#{pname}:",
451
+ default_src,
452
+ fallback_type: fallback_type,
453
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
454
+ )
455
+ params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
456
+
457
+ when :restarg
458
+ pname = (a.children.first || 'args').to_s
459
+ ty =
460
+ if external_sig&.rest_positional&.element_type
461
+ "Array<#{external_sig.rest_positional.element_type}>"
462
+ else
463
+ Infer.infer_param_type(
464
+ "*#{pname}",
465
+ nil,
466
+ fallback_type: fallback_type,
467
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
468
+ )
469
+ end
470
+ params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
471
+
472
+ when :kwrestarg
473
+ pname = (a.children.first || 'kwargs').to_s
474
+ ty = external_sig&.rest_keywords&.type ||
475
+ Infer.infer_param_type(
476
+ "**#{pname}",
477
+ nil,
478
+ fallback_type: fallback_type,
479
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
480
+ )
481
+ params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
482
+
483
+ when :blockarg
484
+ pname = (a.children.first || 'block').to_s
485
+ ty = external_sig&.param_types&.[](pname) ||
486
+ Infer.infer_param_type(
487
+ "&#{pname}",
488
+ nil,
489
+ fallback_type: fallback_type,
490
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
491
+ )
492
+ params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
493
+
494
+ when :forward_arg
495
+ # skip
496
+ end
497
+ end
498
+
499
+ params.empty? ? nil : params
500
+ end
501
+
502
+ # Method documentation.
503
+ #
504
+ # @note module_function: when included, also defines #format_param_tag (instance visibility: private)
505
+ # @param [Object] indent Param documentation.
506
+ # @param [Object] name Param documentation.
507
+ # @param [Object] type Param documentation.
508
+ # @param [Object] documentation Param documentation.
509
+ # @param [Object] style Param documentation.
510
+ # @return [Object]
511
+ def format_param_tag(indent, name, type, documentation, style:)
512
+ doc = documentation.to_s.strip
513
+ type = type.to_s
514
+
515
+ line = case style.to_s
516
+ when 'name_type'
517
+ "#{indent}# @param #{name} [#{type}]"
518
+ else
519
+ "#{indent}# @param [#{type}] #{name}"
520
+ end
521
+
522
+ doc.empty? ? line : "#{line} #{doc}"
523
+ end
524
+
525
+ # Method documentation.
526
+ #
527
+ # @note module_function: when included, also defines #hash_option_pairs (instance visibility: private)
528
+ # @param [Object] node Param documentation.
529
+ # @return [Object]
530
+ def hash_option_pairs(node)
531
+ return [] unless node&.type == :hash
532
+
533
+ node.children.select { |child| child.is_a?(Parser::AST::Node) && child.type == :pair }
534
+ end
535
+
536
+ # Method documentation.
537
+ #
538
+ # @note module_function: when included, also defines #option_key_name (instance visibility: private)
539
+ # @param [Object] key_node Param documentation.
540
+ # @return [Object]
541
+ def option_key_name(key_node)
542
+ case key_node&.type
543
+ when :sym, :str
544
+ key_node.children.first.to_s
545
+ else
546
+ key_node&.loc&.expression&.source.to_s.sub(/\A:/, '')
547
+ end
548
+ end
549
+
550
+ # Method documentation.
551
+ #
552
+ # @note module_function: when included, also defines #node_default_literal (instance visibility: private)
553
+ # @param [Object] node Param documentation.
554
+ # @return [Object]
555
+ def node_default_literal(node)
556
+ node&.loc&.expression&.source
557
+ end
558
+
559
+ # Method documentation.
560
+ #
561
+ # @note module_function: when included, also defines #extract_param_name_from_param_line (instance visibility: private)
562
+ # @param [Object] line Param documentation.
563
+ # @return [nil]
564
+ def extract_param_name_from_param_line(line)
565
+ return Regexp.last_match(1) if line =~ /@param\b\s+\[[^\]]+\]\s+(\S+)/
566
+ return Regexp.last_match(1) if line =~ /@param\b\s+(\S+)\s+\[[^\]]+\]/
567
+
568
+ nil
569
+ end
570
+
571
+ # Method documentation.
572
+ #
573
+ # @note module_function: when included, also defines #debug_warn (instance visibility: private)
574
+ # @param [Object] e Param documentation.
575
+ # @param [Object] insertion Param documentation.
576
+ # @param [Object] name Param documentation.
577
+ # @param [Object] phase Param documentation.
578
+ # @return [Object]
579
+ def debug_warn(e, insertion:, name:, phase:)
580
+ return unless debug?
581
+
582
+ node = insertion&.node
583
+ buf_name = node&.loc&.expression&.source_buffer&.name || '(unknown)'
584
+ line = node&.loc&.expression&.line
585
+ scope = insertion&.scope
586
+ method_symbol = scope == :class ? '.' : '#'
587
+ container = insertion&.container || 'Object'
588
+
589
+ where = +buf_name.to_s
590
+ where << ":#{line}" if line
591
+ where << " #{container}#{method_symbol}#{name}"
592
+
593
+ warn "Docscribe DEBUG: #{phase} failed at #{where}: #{e.class}: #{e.message}"
594
+ end
595
+
596
+ # Method documentation.
597
+ #
598
+ # @note module_function: when included, also defines #debug? (instance visibility: private)
599
+ # @return [Object]
600
+ def debug?
601
+ ENV['DOCSCRIBE_DEBUG'] == '1'
602
+ end
603
+ end
604
+ end
605
+ end