docscribe 1.1.0 → 1.2.1

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