docscribe 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +149 -0
  3. data/lib/docscribe/cli/config_builder.rb +125 -35
  4. data/lib/docscribe/cli/generate.rb +288 -117
  5. data/lib/docscribe/cli/init.rb +49 -13
  6. data/lib/docscribe/cli/options.rb +302 -127
  7. data/lib/docscribe/cli/run.rb +391 -135
  8. data/lib/docscribe/cli.rb +23 -5
  9. data/lib/docscribe/config/defaults.rb +11 -11
  10. data/lib/docscribe/config/emit.rb +1 -0
  11. data/lib/docscribe/config/filtering.rb +24 -11
  12. data/lib/docscribe/config/loader.rb +7 -4
  13. data/lib/docscribe/config/plugin.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +31 -22
  15. data/lib/docscribe/config/sorbet.rb +41 -15
  16. data/lib/docscribe/config/sorting.rb +1 -0
  17. data/lib/docscribe/config/template.rb +1 -0
  18. data/lib/docscribe/config/utils.rb +1 -0
  19. data/lib/docscribe/config.rb +1 -0
  20. data/lib/docscribe/infer/constants.rb +15 -0
  21. data/lib/docscribe/infer/literals.rb +43 -25
  22. data/lib/docscribe/infer/names.rb +24 -15
  23. data/lib/docscribe/infer/params.rb +52 -6
  24. data/lib/docscribe/infer/raises.rb +24 -14
  25. data/lib/docscribe/infer/returns.rb +365 -182
  26. data/lib/docscribe/infer.rb +10 -9
  27. data/lib/docscribe/inline_rewriter/collector.rb +766 -375
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +217 -74
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +1488 -602
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +100 -52
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +109 -48
  32. data/lib/docscribe/inline_rewriter.rb +1009 -595
  33. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -3
  34. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -1
  35. data/lib/docscribe/plugin/registry.rb +34 -7
  36. data/lib/docscribe/plugin.rb +48 -17
  37. data/lib/docscribe/types/rbs/collection_loader.rb +0 -1
  38. data/lib/docscribe/types/rbs/provider.rb +75 -26
  39. data/lib/docscribe/types/rbs/type_formatter.rb +127 -59
  40. data/lib/docscribe/types/sorbet/base_provider.rb +31 -12
  41. data/lib/docscribe/version.rb +1 -1
  42. metadata +2 -2
@@ -19,6 +19,13 @@ module Docscribe
19
19
  # - receiver-based containers (`def Foo.bar`, `class << Foo`)
20
20
  # - Sorbet-aware anchoring for methods with leading `sig` declarations
21
21
  class Collector < Parser::AST::Processor
22
+ PROCESS_STMT_HANDLERS = {
23
+ def: :process_def_stmt,
24
+ defs: :process_defs_stmt,
25
+ sclass: :process_sclass_stmt,
26
+ send: :process_send_stmt
27
+ }.freeze
28
+
22
29
  # One method that Docscribe intends to document.
23
30
  #
24
31
  # @!attribute node
@@ -132,21 +139,48 @@ module Docscribe
132
139
  #
133
140
  # @return [VisibilityCtx]
134
141
  def dup
135
- c = VisibilityCtx.new
136
- c.default_instance_vis = default_instance_vis
137
- c.default_class_vis = default_class_vis
138
- c.inside_sclass = inside_sclass
142
+ VisibilityCtx.new.tap do |ctx|
143
+ copy_visibility_state(ctx)
144
+ copy_module_function_state(ctx)
145
+ copy_container_state(ctx)
146
+ end
147
+ end
148
+
149
+ private
139
150
 
140
- c.module_function_default = module_function_default
141
- c.module_function_explicit.merge!(module_function_explicit)
151
+ # Copy default instance/class visibility and sclass state into a new context.
152
+ #
153
+ # @private
154
+ # @param [VisibilityCtx] ctx the target context to copy state into
155
+ # @return [void]
156
+ def copy_visibility_state(ctx)
157
+ ctx.default_instance_vis = default_instance_vis
158
+ ctx.default_class_vis = default_class_vis
159
+ ctx.inside_sclass = inside_sclass
160
+
161
+ ctx.explicit_instance.merge!(explicit_instance)
162
+ ctx.explicit_class.merge!(explicit_class)
163
+ end
142
164
 
143
- c.explicit_instance.merge!(explicit_instance)
144
- c.explicit_class.merge!(explicit_class)
165
+ # Copy module_function default and explicit state into a new context.
166
+ #
167
+ # @private
168
+ # @param [VisibilityCtx] ctx the target context to copy state into
169
+ # @return [void]
170
+ def copy_module_function_state(ctx)
171
+ ctx.module_function_default = module_function_default
172
+ ctx.module_function_explicit.merge!(module_function_explicit)
173
+ end
145
174
 
146
- c.container_override = container_override
147
- c.container_is_module = container_is_module
148
- c.extend_self = extend_self
149
- c
175
+ # Copy container override, module flag, and extend_self state into a new context.
176
+ #
177
+ # @private
178
+ # @param [VisibilityCtx] ctx the target context to copy state into
179
+ # @return [void]
180
+ def copy_container_state(ctx)
181
+ ctx.container_override = container_override
182
+ ctx.container_is_module = container_is_module
183
+ ctx.extend_self = extend_self
150
184
  end
151
185
  end
152
186
 
@@ -216,11 +250,7 @@ module Docscribe
216
250
  process_body(body, ctx)
217
251
 
218
252
  # If `extend self` is active for this module, document all instance defs as module methods (M.foo).
219
- if ctx.extend_self
220
- promote_extend_self_container(container: container)
221
- @module_states[container] ||= {}
222
- @module_states[container][:extend_self] = true
223
- end
253
+ persist_extend_self_state(ctx, container)
224
254
 
225
255
  @name_stack.pop
226
256
  node
@@ -234,7 +264,7 @@ module Docscribe
234
264
  # @param [Parser::AST::Node] node a `:casgn` node
235
265
  # @return [Parser::AST::Node] the original node
236
266
  def on_casgn(node)
237
- return node if process_struct_casgn(node)
267
+ return node if process_struct_casgn?(node)
238
268
 
239
269
  node.children.each do |child|
240
270
  process(child) if child.is_a?(Parser::AST::Node)
@@ -278,114 +308,112 @@ module Docscribe
278
308
 
279
309
  private
280
310
 
281
- # Process a single AST node for documentation insertion targets.
282
- #
283
- # Dispatches to specific handlers based on node type (`:def`, `:defs`,
284
- # `:sclass`, `:send` with visibility modifiers, etc.) and records
285
- # `Insertion` objects for methods that need documentation.
311
+ # Process a `:def` node for documentation insertion.
286
312
  #
287
313
  # @private
288
- # @param [Parser::AST::Node, nil] node the AST node to process
289
- # @param [VisibilityCtx] ctx current visibility and container context
290
- # @param [Parser::AST::Node, nil] pending_sig_anchor Sorbet `sig` node waiting for a method
314
+ # @param [Parser::AST::Node] node
315
+ # @param [VisibilityCtx] ctx
316
+ # @param [Parser::AST::Node, nil] pending_sig_anchor
291
317
  # @return [void]
292
- def process_stmt(node, ctx, pending_sig_anchor: nil)
293
- return unless node
318
+ def process_def_stmt(node, ctx, pending_sig_anchor:)
319
+ name, = *node
320
+ anchor_node = pending_sig_anchor || node
294
321
 
295
- case node.type
296
- when :def
297
- name, _args, _body = *node
298
- anchor_node = pending_sig_anchor || node
322
+ return process_module_function_def(node, name, ctx, anchor_node) if module_function_applies?(ctx, name)
323
+ return process_extend_self_def(node, name, ctx, anchor_node) if extend_self_applies?(ctx)
324
+
325
+ scope, visibility = def_scope_visibility(ctx, name)
299
326
 
300
- if module_function_applies?(ctx, name)
301
- scope = :class
302
- vis = ctx.explicit_class[name] || ctx.default_class_vis
327
+ @insertions << Insertion.new(node, scope, visibility, container_for(ctx), nil, nil, anchor_node)
328
+ end
303
329
 
304
- # module_function makes included instance method private by default,
305
- # but explicit named visibility can override it (e.g. `public :foo`).
306
- included_vis = ctx.explicit_instance[name] || :private
330
+ # Process a `:defs` node (singleton method) for documentation insertion.
331
+ #
332
+ # @private
333
+ # @param [Parser::AST::Node] node the `:defs` AST node
334
+ # @param [VisibilityCtx] ctx current visibility context
335
+ # @param [Parser::AST::Node, nil] pending_sig_anchor Sorbet `sig` node waiting for a method
336
+ # @return [void]
337
+ def process_defs_stmt(node, ctx, pending_sig_anchor:)
338
+ recv, name, _args, _body = *node
339
+ vis = ctx.explicit_class[name] || ctx.default_class_vis
307
340
 
308
- @insertions << Insertion.new(node, scope, vis, container_for(ctx), true, included_vis, anchor_node)
309
- return
341
+ container =
342
+ if const_receiver?(recv)
343
+ const_name(recv)
344
+ else
345
+ container_for(ctx)
310
346
  end
311
347
 
312
- if extend_self_applies?(ctx)
313
- # Under `extend self` in a module, instance methods are callable as module methods (M.foo).
314
- scope = :class
315
- vis = ctx.explicit_instance[name] || ctx.default_instance_vis
348
+ @insertions << Insertion.new(node, :class, vis, container, nil, nil, pending_sig_anchor || node)
349
+ end
316
350
 
317
- @insertions << Insertion.new(node, scope, vis, container_for(ctx), nil, nil, anchor_node)
318
- return
319
- end
351
+ # Process a `:sclass` node for documentation insertion.
352
+ #
353
+ # @private
354
+ # @param [Parser::AST::Node] node
355
+ # @param [VisibilityCtx] ctx
356
+ # @return [void]
357
+ def process_sclass_stmt(node, ctx)
358
+ # `class << self` — affects default visibility for singleton methods and changes scope.
359
+ recv, body = *node
360
+ inner_ctx = ctx.dup
320
361
 
321
- # existing behavior for non-module_function:
322
- if ctx.inside_sclass
323
- vis = ctx.explicit_class[name] || ctx.default_class_vis
324
- scope = :class
325
- else
326
- vis = ctx.explicit_instance[name] || ctx.default_instance_vis
327
- scope = :instance
328
- end
362
+ configure_sclass_context(inner_ctx, recv)
329
363
 
330
- @insertions << Insertion.new(node, scope, vis, container_for(ctx), nil, nil, anchor_node)
364
+ process_body(body, inner_ctx)
365
+ end
331
366
 
332
- when :defs
333
- recv, name, _args, _body = *node
334
- vis = ctx.explicit_class[name] || ctx.default_class_vis
335
-
336
- container =
337
- if const_receiver?(recv)
338
- const_name(recv)
339
- else
340
- container_for(ctx)
341
- end
342
-
343
- @insertions << Insertion.new(node, :class, vis, container, nil, nil, pending_sig_anchor || node)
344
-
345
- when :sclass
346
- # `class << self` — affects default visibility for singleton methods and changes scope.
347
- recv, body = *node
348
- inner_ctx = ctx.dup
349
-
350
- if self_node?(recv)
351
- # class << self
352
- inner_ctx.inside_sclass = true
353
- inner_ctx.container_override = nil
354
- elsif const_receiver?(recv)
355
- # class << Foo (const receiver) — document methods under Foo
356
- inner_ctx.inside_sclass = true
357
- inner_ctx.container_override = const_name(recv)
358
- else
359
- # Unknown receiver (e.g. class << obj) — keep prior behavior
360
- inner_ctx.inside_sclass = false
361
- inner_ctx.container_override = nil
362
- end
367
+ # Configure the new context with sclass receiver tracking and container override.
368
+ #
369
+ # @private
370
+ # @param [VisibilityCtx] ctx the inner context to configure
371
+ # @param [Parser::AST::Node] recv the receiver node of `class <<`
372
+ # @return [void]
373
+ def configure_sclass_context(ctx, recv)
374
+ ctx.inside_sclass = sclass_receiver?(recv)
375
+ ctx.container_override = sclass_container_override(recv)
376
+ end
363
377
 
364
- # NOTE: we intentionally do NOT reset default_class_vis here; we inherit via ctx.dup.
365
- process_body(body, inner_ctx)
378
+ # Check if the receiver is `self` or a constant reference (enables sclass semantics).
379
+ #
380
+ # @private
381
+ # @param [Parser::AST::Node] recv the receiver node of `class <<`
382
+ # @return [Boolean]
383
+ def sclass_receiver?(recv)
384
+ self_node?(recv) || const_receiver?(recv)
385
+ end
366
386
 
367
- when :casgn
368
- if process_struct_casgn(node)
369
- # handled
370
- else
371
- process(node)
372
- end
387
+ # Return the constant name for a non-self receiver, or nil for `class << self`.
388
+ #
389
+ # @private
390
+ # @param [Parser::AST::Node] recv the receiver node of `class <<`
391
+ # @return [String, nil] the container name for constant receivers, nil for `self`
392
+ def sclass_container_override(recv)
393
+ return nil if self_node?(recv)
394
+ return const_name(recv) if const_receiver?(recv)
373
395
 
374
- when :send
375
- if process_attr_send(node, ctx)
376
- # handled
377
- elsif process_extend_self_send(node, ctx)
378
- # handled
379
- elsif process_module_function_send(node, ctx)
380
- # handled
381
- elsif process_class_method_visibility_send(node, ctx)
382
- # handled
383
- else
384
- process_visibility_send(node, ctx, pending_sig_anchor: pending_sig_anchor)
385
- end
396
+ nil
397
+ end
386
398
 
399
+ # Process a `:send` node for documentation insertion.
400
+ #
401
+ # @private
402
+ # @param [Parser::AST::Node] node
403
+ # @param [VisibilityCtx] ctx
404
+ # @param [Parser::AST::Node, nil] pending_sig_anchor
405
+ # @return [void]
406
+ def process_send_stmt(node, ctx, pending_sig_anchor:)
407
+ if process_attr_send?(node, ctx)
408
+ # handled
409
+ elsif process_extend_self_send?(node, ctx)
410
+ # handled
411
+ elsif process_module_function_send?(node, ctx)
412
+ # handled
413
+ elsif process_class_method_visibility_send?(node, ctx)
414
+ # handled
387
415
  else
388
- process(node)
416
+ process_visibility_send(node, ctx, pending_sig_anchor: pending_sig_anchor)
389
417
  end
390
418
  end
391
419
 
@@ -401,196 +429,255 @@ module Docscribe
401
429
  names = extract_struct_member_names(super_node)
402
430
  return if names.empty?
403
431
 
404
- @attr_insertions << AttrInsertion.new(
405
- node, # insert above the class declaration
406
- :instance, # struct members are instance readers/writers
407
- :public, # Struct fields are public by default
408
- current_container,
409
- :rw,
410
- names
411
- )
432
+ @attr_insertions << AttrInsertion.new(node, :instance, :public, current_container, :rw, names)
412
433
  end
413
434
 
414
- # Check if a constant assignment is `Struct.new` and extract attribute insertions.
435
+ # Detect `attr_reader` / `attr_writer` / `attr_accessor` calls and record attribute insertions.
415
436
  #
416
437
  # @private
417
- # @param [Parser::AST::Node] node a `:casgn` node
418
- # @return [Boolean] true if the node was handled as a struct definition
419
- def process_struct_casgn(node)
420
- _scope, _name, value = *node
421
- return false unless struct_new_node?(value)
438
+ # @param [Parser::AST::Node] node a `:send` node
439
+ # @param [VisibilityCtx] ctx current visibility context
440
+ # @return [Boolean] true if the node was an attr_* call
441
+ def process_attr_send?(node, ctx)
442
+ recv, meth, *args = *node
422
443
 
423
- names = extract_struct_member_names(value)
444
+ return false unless attr_send?(recv, meth)
445
+
446
+ names = args.map { |arg| extract_name_sym(arg) }.compact
424
447
  return true if names.empty?
425
448
 
426
- @attr_insertions << AttrInsertion.new(
427
- node, # insert above the constant assignment
428
- :instance,
429
- :public,
430
- struct_container_name(node),
431
- :rw,
432
- names
433
- )
449
+ scope, visibility = attr_scope_visibility(ctx)
450
+ access = attr_access_type(meth)
451
+
452
+ @attr_insertions << AttrInsertion.new(node, scope, visibility, container_for(ctx), access, names)
434
453
 
435
454
  true
436
455
  end
437
456
 
438
- # Check if a node represents a `Struct.new` call.
457
+ # Detect `extend self` calls inside a module and persist the state.
439
458
  #
440
459
  # @private
441
- # @param [Parser::AST::Node, nil] node an AST node
442
- # @return [Boolean]
443
- def struct_new_node?(node)
444
- return false unless node.is_a?(Parser::AST::Node)
445
- return false unless node.type == :send
460
+ # @param [Parser::AST::Node] node a `:send` node
461
+ # @param [VisibilityCtx] ctx current visibility context
462
+ # @return [Boolean] true if `extend self` was detected
463
+ def process_extend_self_send?(node, ctx)
464
+ recv, meth, *args = *node
446
465
 
447
- recv, meth, *_args = *node
448
- return false unless meth == :new
449
- return false unless recv&.type == :const
466
+ return false unless extend_self_send?(ctx, recv, meth, args)
450
467
 
451
- recv_name = const_name(recv)
452
- %w[Struct ::Struct].include?(recv_name)
468
+ persist_extend_self(ctx)
469
+
470
+ true
453
471
  end
454
472
 
455
- # Extract member names from a `Struct.new` call, stripping the type string argument if present.
473
+ # Mark the context and module state as using `extend self`.
456
474
  #
457
475
  # @private
458
- # @param [Parser::AST::Node] struct_new_node a `:send` node representing `Struct.new`
459
- # @return [Array<Symbol>] extracted member names
460
- def extract_struct_member_names(struct_new_node)
461
- _recv, _meth, *args = *struct_new_node
476
+ # @param [VisibilityCtx] ctx current visibility context
477
+ # @return [void]
478
+ def persist_extend_self(ctx)
479
+ ctx.extend_self = true
462
480
 
463
- # Drop trailing keyword/options hash, e.g. keyword_init: true
464
- args = args.reject { |arg| arg.is_a?(Parser::AST::Node) && arg.type == :hash }
481
+ container = container_for(ctx)
482
+ (@module_states[container] ||= {})[:extend_self] = true
483
+ end
484
+
485
+ # Check if a `:send` node is an `extend self` call inside a module.
486
+ #
487
+ # @private
488
+ # @param [VisibilityCtx] ctx current visibility context
489
+ # @param [Parser::AST::Node, nil] recv the receiver of the send node
490
+ # @param [Symbol] meth the method name being called
491
+ # @param [Array<Parser::AST::Node>] args the arguments to the method call
492
+ # @return [Boolean]
493
+ def extend_self_send?(ctx, recv, meth, args)
494
+ ctx.container_is_module &&
495
+ recv.nil? &&
496
+ meth == :extend &&
497
+ !ctx.inside_sclass &&
498
+ args.any? { |arg| self_node?(arg) }
499
+ end
465
500
 
466
- # Support Struct.new("Foo", :a, :b)
467
- args = args.drop(1) if args.length >= 2 && args.first.is_a?(Parser::AST::Node) && args.first.type == :str
501
+ # Check if a node is a constant or `::` (cbase) receiver.
502
+ #
503
+ # @private
504
+ # @param [Parser::AST::Node, nil] node an AST node
505
+ # @return [Boolean]
506
+ def const_receiver?(node)
507
+ return false unless node.is_a?(Parser::AST::Node)
468
508
 
469
- args.map { |arg| extract_name_sym(arg) }.compact
509
+ %i[const cbase].include?(node.type)
470
510
  end
471
511
 
472
- # Build the container name for a struct constant assignment.
512
+ # Check if a send node is an attr_reader/attr_writer/attr_accessor call.
473
513
  #
474
514
  # @private
475
- # @param [Parser::AST::Node] node a `:casgn` node
476
- # @return [String] the fully qualified container name
477
- def struct_container_name(node)
478
- scope, name, _value = *node
515
+ # @param [Parser::AST::Node, nil] recv the receiver of the send node
516
+ # @param [Symbol] meth the method name being called
517
+ # @return [Boolean]
518
+ def attr_send?(recv, meth)
519
+ recv.nil? && %i[attr_reader attr_writer attr_accessor].include?(meth)
520
+ end
479
521
 
480
- prefix =
481
- if scope
482
- const_name(scope)
483
- elsif current_container == 'Object'
484
- nil
485
- else
486
- current_container
487
- end
522
+ # Determine the scope and visibility for an attribute based on sclass context.
523
+ #
524
+ # @private
525
+ # @param [VisibilityCtx] ctx current visibility context
526
+ # @return [Array(Symbol, Symbol)] the scope (:instance/:class) and visibility (:public/:protected/:private)
527
+ def attr_scope_visibility(ctx)
528
+ if ctx.inside_sclass
529
+ [:class, ctx.default_class_vis]
530
+ else
531
+ [:instance, ctx.default_instance_vis]
532
+ end
533
+ end
488
534
 
489
- [prefix, name.to_s].compact.reject(&:empty?).join('::')
535
+ # Map the attr method name to an access type symbol.
536
+ #
537
+ # @private
538
+ # @param [Symbol] meth the method name (:attr_reader, :attr_writer, or :attr_accessor)
539
+ # @return [Symbol] :r for reader, :w for writer, :rw for accessor
540
+ def attr_access_type(meth)
541
+ case meth
542
+ when :attr_reader then :r
543
+ when :attr_writer then :w
544
+ else :rw
545
+ end
490
546
  end
491
547
 
492
- # Detect `extend self` calls inside a module and persist the state.
548
+ # Detect `module_function` calls and update the visibility context accordingly.
493
549
  #
494
550
  # @private
495
- # @param [Parser::AST::Node] node a `:send` node
551
+ # @param [Parser::AST::Node] node the `:send` node
496
552
  # @param [VisibilityCtx] ctx current visibility context
497
- # @return [Boolean] true if `extend self` was detected
498
- def process_extend_self_send(node, ctx)
553
+ # @return [Boolean] true if the node was a module_function call
554
+ def process_module_function_send?(node, ctx)
499
555
  recv, meth, *args = *node
500
556
 
501
- return false unless ctx.container_is_module
502
- return false unless recv.nil? && meth == :extend
503
- return false if ctx.inside_sclass
504
- return false unless args.any? { |a| self_node?(a) }
557
+ return false unless recv.nil? && meth == :module_function
558
+ return true if ctx.inside_sclass
505
559
 
506
- ctx.extend_self = true
560
+ return enable_default_module_function?(ctx) if args.empty?
507
561
 
508
- # Persist across reopened modules in this file.
509
- container = container_for(ctx)
510
- @module_states[container] ||= {}
511
- @module_states[container][:extend_self] = true
562
+ process_named_module_function(args, ctx)
512
563
 
513
564
  true
514
565
  end
515
566
 
516
- # Check whether `extend self` semantics apply at the current position.
567
+ # Enable default module_function for all subsequent method definitions in the module.
517
568
  #
518
569
  # @private
519
570
  # @param [VisibilityCtx] ctx current visibility context
520
- # @return [Boolean]
521
- def extend_self_applies?(ctx)
522
- ctx.container_is_module && ctx.extend_self && !ctx.inside_sclass
571
+ # @return [Boolean] true
572
+ def enable_default_module_function?(ctx)
573
+ ctx.module_function_default = true
574
+ true
523
575
  end
524
576
 
525
- # Check if a node is a constant or `::` (cbase) receiver.
577
+ # Process a `module_function :foo, :bar` call with named arguments.
526
578
  #
527
579
  # @private
528
- # @param [Parser::AST::Node, nil] node an AST node
529
- # @return [Boolean]
530
- def const_receiver?(node)
531
- return false unless node.is_a?(Parser::AST::Node)
580
+ # @param [Array<Parser::AST::Node>] args the named method arguments
581
+ # @param [VisibilityCtx] ctx current visibility context
582
+ # @return [void]
583
+ def process_named_module_function(args, ctx)
584
+ args.map { |arg| extract_name_sym(arg) }
585
+ .compact
586
+ .each do |sym|
587
+ ctx.module_function_explicit[sym] = true
532
588
 
533
- %i[const cbase].include?(node.type)
589
+ retroactively_promote_module_function(
590
+ sym,
591
+ container: container_for(ctx)
592
+ )
593
+ end
534
594
  end
535
595
 
536
- # Detect `attr_reader` / `attr_writer` / `attr_accessor` calls and record attribute insertions.
596
+ # Retroactively promote a previously collected method to module_function (class scope).
537
597
  #
538
598
  # @private
539
- # @param [Parser::AST::Node] node a `:send` node
540
- # @param [VisibilityCtx] ctx current visibility context
541
- # @return [Boolean] true if the node was an attr_* call
542
- def process_attr_send(node, ctx)
543
- recv, meth, *args = *node
544
- return false unless recv.nil? && %i[attr_reader attr_writer attr_accessor].include?(meth)
545
-
546
- names = args.map { |a| extract_name_sym(a) }.compact
547
- return true if names.empty?
548
-
549
- scope = ctx.inside_sclass ? :class : :instance
550
- visibility = ctx.inside_sclass ? ctx.default_class_vis : ctx.default_instance_vis
551
-
552
- access =
553
- case meth
554
- when :attr_reader then :r
555
- when :attr_writer then :w
556
- else :rw
557
- end
558
-
559
- @attr_insertions << AttrInsertion.new(node, scope, visibility, container_for(ctx), access, names)
599
+ # @param [Symbol] name_sym the method name to promote
600
+ # @param [String] container the container name
601
+ # @return [void]
602
+ def retroactively_promote_module_function(name_sym, container:)
603
+ @insertions.reverse_each do |ins|
604
+ next unless ins.container == container
605
+ next unless ins.node.type == :def
606
+ next unless ins.node.children[0] == name_sym
560
607
 
561
- true
608
+ ins.scope = :class
609
+ ins.visibility = :public
610
+ ins.module_function = true
611
+ ins.included_instance_visibility ||= :private
612
+ break
613
+ end
562
614
  end
563
615
 
564
- # Detect `private_class_method` / `protected_class_method` / `public_class_method` and update class-level visibility.
616
+ # Detect `private_class_method` / `protected_class_method` / `public_class_method` and update class-level
617
+ # visibility.
565
618
  #
566
619
  # @private
567
620
  # @param [Parser::AST::Node] node a `:send` node
568
621
  # @param [VisibilityCtx] ctx current visibility context
569
622
  # @return [Boolean] true if the node was a class visibility modifier
570
- def process_class_method_visibility_send(node, ctx)
623
+ def process_class_method_visibility_send?(node, ctx)
571
624
  recv, meth, *args = *node
625
+ return false unless class_visibility_send?(recv, meth)
572
626
 
573
- return false unless %i[private_class_method protected_class_method public_class_method].include?(meth)
574
- return false unless recv.nil? || self_node?(recv)
627
+ visibility = class_method_visibility(meth)
628
+ apply_class_method_visibility(args, ctx, visibility, container_for(ctx))
575
629
 
576
- visibility =
577
- case meth
578
- when :private_class_method then :private
579
- when :protected_class_method then :protected
580
- else :public
581
- end
630
+ true
631
+ end
582
632
 
583
- container = container_for(ctx)
633
+ # Check if a send node is a private/protected/public_class_method call.
634
+ #
635
+ # @private
636
+ # @param [Parser::AST::Node, nil] recv the receiver of the send node
637
+ # @param [Symbol] meth the method name being called
638
+ # @return [Boolean]
639
+ def class_visibility_send?(recv, meth)
640
+ %i[
641
+ private_class_method
642
+ protected_class_method
643
+ public_class_method
644
+ ].include?(meth) &&
645
+ (recv.nil? || self_node?(recv))
646
+ end
647
+
648
+ # Map a class method visibility modifier name to its visibility symbol.
649
+ #
650
+ # @private
651
+ # @param [Symbol] meth the method name (:private_class_method, etc.)
652
+ # @return [Symbol] :private, :protected, or :public
653
+ def class_method_visibility(meth)
654
+ case meth
655
+ when :private_class_method
656
+ :private
657
+ when :protected_class_method
658
+ :protected
659
+ else
660
+ :public
661
+ end
662
+ end
584
663
 
664
+ # Apply a visibility modifier to named class methods and retroactively update their visibility.
665
+ #
666
+ # @private
667
+ # @param [Array<Parser::AST::Node>] args the method name nodes
668
+ # @param [VisibilityCtx] ctx current visibility context
669
+ # @param [Symbol] visibility the visibility to apply (:public, :protected, :private)
670
+ # @param [String] container the container name
671
+ # @return [void]
672
+ def apply_class_method_visibility(args, ctx, visibility, container)
585
673
  args.each do |arg|
586
674
  sym = extract_name_sym(arg)
587
675
  next unless sym
588
676
 
589
677
  ctx.explicit_class[sym] = visibility
678
+
590
679
  retroactively_set_visibility(sym, visibility, scope: :class, container: container)
591
680
  end
592
-
593
- true
594
681
  end
595
682
 
596
683
  # Detect `private` / `protected` / `public` calls and update visibility state.
@@ -606,104 +693,184 @@ module Docscribe
606
693
  # @return [void]
607
694
  def process_visibility_send(node, ctx, pending_sig_anchor: nil)
608
695
  recv, meth, *args = *node
609
- return unless recv.nil? && %i[private protected public].include?(meth)
610
-
611
- container = container_for(ctx)
612
-
613
- if args.empty?
614
- if ctx.inside_sclass
615
- ctx.default_class_vis = meth
616
- else
617
- ctx.default_instance_vis = meth
618
- end
619
- return
620
- end
621
696
 
622
- # Inline modifier: private def foo / private def self.foo
623
- if args.length == 1 && args[0].is_a?(Parser::AST::Node) && %i[def defs].include?(args[0].type)
624
- def_node = args[0]
625
- anchor_node = pending_sig_anchor || def_node
626
-
627
- case def_node.type
628
- when :def
629
- name, = *def_node
630
-
631
- if module_function_applies?(ctx, name)
632
- mod_vis = ctx.explicit_class[name] || ctx.default_class_vis
633
- included_vis = meth
634
- @insertions << Insertion.new(def_node, :class, mod_vis, container, true, included_vis, anchor_node)
635
- elsif ctx.inside_sclass
636
- @insertions << Insertion.new(def_node, :class, meth, container, nil, nil, anchor_node)
637
- else
638
- @insertions << Insertion.new(def_node, :instance, meth, container, nil, nil, anchor_node)
639
- end
640
-
641
- return
642
-
643
- when :defs
644
- @insertions << Insertion.new(def_node, :class, meth, container, nil, nil, anchor_node)
645
- return
646
- end
647
- end
697
+ return unless visibility_send?(recv, meth)
648
698
 
649
- # Named visibility: private :foo
650
- args.each do |arg|
651
- sym = extract_name_sym(arg)
652
- next unless sym
699
+ process_visibility_args(args, ctx, meth, container_for(ctx), pending_sig_anchor)
700
+ end
653
701
 
654
- if ctx.inside_sclass
655
- ctx.explicit_class[sym] = meth
656
- retroactively_set_visibility(sym, meth, scope: :class, container: container)
657
- else
658
- ctx.explicit_instance[sym] = meth
659
- retroactively_set_visibility(sym, meth, scope: :instance, container: container)
660
- retroactively_set_included_instance_visibility_for_module_function(sym, meth, container: container)
661
- end
662
- end
702
+ # Check if a send node is a private/protected/public call with no receiver.
703
+ #
704
+ # @private
705
+ # @param [Parser::AST::Node, nil] recv the receiver of the send node
706
+ # @param [Symbol] meth the method name being called
707
+ # @return [Boolean]
708
+ def visibility_send?(recv, meth)
709
+ recv.nil? && %i[private protected public].include?(meth)
663
710
  end
664
711
 
665
- # Retroactively update the included instance visibility for a module_function method.
712
+ # Dispatch visibility modifier handling based on whether args are absent, inline defs, or named symbols.
666
713
  #
667
714
  # @private
668
- # @param [Symbol] name_sym the method name
669
- # @param [Symbol] visibility the new visibility (:public, :protected, :private)
715
+ # @param [Array<Parser::AST::Node>] args the arguments to the visibility modifier
716
+ # @param [VisibilityCtx] ctx current visibility context
717
+ # @param [Symbol] meth the visibility method (:private, :protected, :public)
670
718
  # @param [String] container the container name
719
+ # @param [Parser::AST::Node, nil] pending_sig_anchor Sorbet `sig` node waiting for a method
671
720
  # @return [void]
672
- def retroactively_set_included_instance_visibility_for_module_function(name_sym, visibility, container:)
673
- @insertions.reverse_each do |ins|
674
- next unless ins.container == container
675
- next unless ins.module_function
676
- next unless ins.node.type == :def
677
- next unless ins.node.children[0] == name_sym
678
-
679
- ins.included_instance_visibility = visibility
680
- break
721
+ def process_visibility_args(args, ctx, meth, container, pending_sig_anchor)
722
+ if args.empty?
723
+ process_visibility_bare_modifier(ctx, meth)
724
+ elsif inline_visibility_def?(args)
725
+ process_visibility_inline_modifier(args.first, ctx, meth, container, pending_sig_anchor)
726
+ else
727
+ process_visibility_named_modifier(args, ctx, meth, container)
681
728
  end
682
729
  end
683
730
 
684
- # Retroactively update the visibility of a previously collected method.
731
+ # Check if visibility modifier args contain a single inline def/defs node.
732
+ #
733
+ # @private
734
+ # @param [Array<Parser::AST::Node>] args the arguments to the visibility modifier
735
+ # @return [Boolean]
736
+ def inline_visibility_def?(args)
737
+ args.length == 1 &&
738
+ args.first.is_a?(Parser::AST::Node) &&
739
+ %i[def defs].include?(args.first.type)
740
+ end
741
+
742
+ # Process a bare visibility modifier (no args).
743
+ #
744
+ # @private
745
+ # @param [VisibilityCtx] ctx
746
+ # @param [Symbol] meth
747
+ # @return [void]
748
+ def process_visibility_bare_modifier(ctx, meth)
749
+ if ctx.inside_sclass
750
+ ctx.default_class_vis = meth
751
+ else
752
+ ctx.default_instance_vis = meth
753
+ end
754
+ end
755
+
756
+ # Process an inline visibility modifier (private def foo).
757
+ #
758
+ # @private
759
+ # @param [Parser::AST::Node] def_node
760
+ # @param [VisibilityCtx] ctx
761
+ # @param [Symbol] meth
762
+ # @param [String] container
763
+ # @param [Parser::AST::Node, nil] pending_sig_anchor
764
+ # @return [void]
765
+ def process_visibility_inline_modifier(def_node, ctx, meth, container, pending_sig_anchor)
766
+ anchor_node = pending_sig_anchor || def_node
767
+
768
+ case def_node.type
769
+ when :def
770
+ process_visibility_inline_def(def_node, ctx, meth, container, anchor_node)
771
+ when :defs
772
+ @insertions << Insertion.new(def_node, :class, meth, container, nil, nil, anchor_node)
773
+ end
774
+ end
775
+
776
+ # Process an inline def under a visibility modifier.
777
+ #
778
+ # @private
779
+ # @param [Parser::AST::Node] def_node
780
+ # @param [VisibilityCtx] ctx
781
+ # @param [Symbol] meth
782
+ # @param [String] container
783
+ # @param [Parser::AST::Node] anchor_node
784
+ # @return [void]
785
+ def process_visibility_inline_def(def_node, ctx, meth, container, anchor_node)
786
+ name, = *def_node
787
+
788
+ if module_function_applies?(ctx, name)
789
+ mod_vis = ctx.explicit_class[name] || ctx.default_class_vis
790
+ @insertions << Insertion.new(def_node, :class, mod_vis, container, true, meth, anchor_node)
791
+ elsif ctx.inside_sclass
792
+ @insertions << Insertion.new(def_node, :class, meth, container, nil, nil, anchor_node)
793
+ else
794
+ @insertions << Insertion.new(def_node, :instance, meth, container, nil, nil, anchor_node)
795
+ end
796
+ end
797
+
798
+ # Process a named visibility modifier (private :foo).
799
+ #
800
+ # @private
801
+ # @param [Array<Parser::AST::Node>] args
802
+ # @param [VisibilityCtx] ctx
803
+ # @param [Symbol] meth
804
+ # @param [String] container
805
+ # @return [void]
806
+ def process_visibility_named_modifier(args, ctx, meth, container)
807
+ args.each do |arg|
808
+ apply_visibility_modifier_arg(arg, ctx, meth, container)
809
+ end
810
+ end
811
+
812
+ # Apply a visibility modifier to a single named method symbol, dispatching to class or instance handling.
813
+ #
814
+ # @private
815
+ # @param [Parser::AST::Node] arg the AST node for the method name
816
+ # @param [VisibilityCtx] ctx current visibility context
817
+ # @param [Symbol] meth the visibility method (:private, :protected, :public)
818
+ # @param [String] container the container name
819
+ # @return [void]
820
+ def apply_visibility_modifier_arg(arg, ctx, meth, container)
821
+ sym = extract_name_sym(arg)
822
+ return unless sym
823
+
824
+ if ctx.inside_sclass
825
+ apply_class_visibility_modifier(sym, ctx, meth, container)
826
+ else
827
+ apply_instance_visibility_modifier(sym, ctx, meth, container)
828
+ end
829
+ end
830
+
831
+ # Record and retroactively apply a class-scope visibility modifier for a named method.
832
+ #
833
+ # @private
834
+ # @param [Symbol] sym the method name
835
+ # @param [VisibilityCtx] ctx current visibility context
836
+ # @param [Symbol] meth the visibility method (:private, :protected, :public)
837
+ # @param [String] container the container name
838
+ # @return [void]
839
+ def apply_class_visibility_modifier(sym, ctx, meth, container)
840
+ ctx.explicit_class[sym] = meth
841
+
842
+ retroactively_set_visibility(sym, meth, scope: :class, container: container)
843
+ end
844
+
845
+ # Record and retroactively apply an instance-scope visibility modifier for a named method.
846
+ #
847
+ # @private
848
+ # @param [Symbol] sym the method name
849
+ # @param [VisibilityCtx] ctx current visibility context
850
+ # @param [Symbol] meth the visibility method (:private, :protected, :public)
851
+ # @param [String] container the container name
852
+ # @return [void]
853
+ def apply_instance_visibility_modifier(sym, ctx, meth, container)
854
+ ctx.explicit_instance[sym] = meth
855
+ retroactively_set_visibility(sym, meth, scope: :instance, container: container)
856
+ retroactively_set_included_instance_visibility_for_module_function(sym, meth, container: container)
857
+ end
858
+
859
+ # Retroactively update the included instance visibility for a module_function method.
685
860
  #
686
861
  # @private
687
862
  # @param [Symbol] name_sym the method name
688
- # @param [Symbol] visibility the new visibility
689
- # @param [Symbol] scope the method scope (`:instance` or `:class`)
863
+ # @param [Symbol] visibility the new visibility (:public, :protected, :private)
690
864
  # @param [String] container the container name
691
865
  # @return [void]
692
- def retroactively_set_visibility(name_sym, visibility, scope:, container:)
866
+ def retroactively_set_included_instance_visibility_for_module_function(name_sym, visibility, container:)
693
867
  @insertions.reverse_each do |ins|
694
868
  next unless ins.container == container
695
- next unless ins.scope == scope
696
-
697
- n = ins.node
698
- method_name =
699
- case n.type
700
- when :def then n.children[0]
701
- when :defs then n.children[1]
702
- end
703
-
704
- next unless method_name == name_sym
869
+ next unless ins.module_function
870
+ next unless ins.node.type == :def
871
+ next unless ins.node.children[0] == name_sym
705
872
 
706
- ins.visibility = visibility
873
+ ins.included_instance_visibility = visibility
707
874
  break
708
875
  end
709
876
  end
@@ -720,67 +887,287 @@ module Docscribe
720
887
  ctx.module_function_default || ctx.module_function_explicit[name]
721
888
  end
722
889
 
723
- # Detect `module_function` calls (bare or named) and update visibility state.
890
+ # Handle a def where module_function applies, recording it with class scope and module_function semantics.
724
891
  #
725
892
  # @private
726
- # @param [Parser::AST::Node] node a `:send` node
893
+ # @param [Parser::AST::Node] node the `:def` AST node
894
+ # @param [Symbol] name the method name
727
895
  # @param [VisibilityCtx] ctx current visibility context
728
- # @return [Boolean] true if the node was a `module_function` call
729
- def process_module_function_send(node, ctx)
730
- recv, meth, *args = *node
731
- return false unless recv.nil? && meth == :module_function
732
- return true if ctx.inside_sclass
733
-
734
- if args.empty?
735
- ctx.module_function_default = true
736
- return true
737
- end
896
+ # @param [Parser::AST::Node] anchor_node the anchor node for comment placement
897
+ # @return [void]
898
+ def process_module_function_def(node, name, ctx, anchor_node)
899
+ @insertions << Insertion.new(node, :class, ctx.explicit_class[name] || ctx.default_class_vis,
900
+ container_for(ctx), true,
901
+ ctx.explicit_instance[name] || :private, anchor_node)
902
+ end
738
903
 
739
- names = args.map { |arg| extract_name_sym(arg) }.compact
740
- names.each do |sym|
741
- ctx.module_function_explicit[sym] = true
742
- retroactively_promote_module_function(sym, container: container_for(ctx))
743
- end
904
+ # Check if extend self semantics should apply to the current definition.
905
+ #
906
+ # @private
907
+ # @param [VisibilityCtx] ctx current visibility context
908
+ # @return [Boolean]
909
+ def extend_self_applies?(ctx)
910
+ ctx.container_is_module && ctx.extend_self && !ctx.inside_sclass
911
+ end
744
912
 
745
- true
913
+ # Process a def under extend self semantics, recording it as a class method.
914
+ #
915
+ # @private
916
+ # @param [Parser::AST::Node] node the `:def` AST node
917
+ # @param [Symbol] name the method name
918
+ # @param [VisibilityCtx] ctx current visibility context
919
+ # @param [Parser::AST::Node] anchor_node the anchor node for comment placement
920
+ # @return [void]
921
+ def process_extend_self_def(node, name, ctx, anchor_node)
922
+ @insertions << Insertion.new(node, :class, ctx.explicit_instance[name] || ctx.default_instance_vis,
923
+ container_for(ctx), nil, nil, anchor_node)
746
924
  end
747
925
 
748
- # Get the effective container name, using `container_override` when set.
926
+ # Determine scope and visibility for a def based on sclass context and explicit visibility.
749
927
  #
750
928
  # @private
751
929
  # @param [VisibilityCtx] ctx current visibility context
752
- # @return [String] the container name
753
- def container_for(ctx)
754
- ctx.container_override || current_container
930
+ # @param [Symbol] name the method name
931
+ # @return [Array(Symbol, Symbol)] the scope (:instance/:class) and visibility (:public/:protected/:private)
932
+ def def_scope_visibility(ctx, name)
933
+ if ctx.inside_sclass
934
+ [:class, ctx.explicit_class[name] || ctx.default_class_vis]
935
+ else
936
+ [:instance, ctx.explicit_instance[name] || ctx.default_instance_vis]
937
+ end
755
938
  end
756
939
 
757
- # Retroactively promote a previously collected instance method to a class method under module_function.
940
+ # Retroactively update the visibility of a previously collected method.
758
941
  #
759
942
  # @private
760
943
  # @param [Symbol] name_sym the method name
944
+ # @param [Symbol] visibility the new visibility
945
+ # @param [Symbol] scope the method scope (`:instance` or `:class`)
761
946
  # @param [String] container the container name
762
947
  # @return [void]
763
- def retroactively_promote_module_function(name_sym, container:)
764
- @insertions.reverse_each do |ins|
765
- next unless ins.container == container
766
- next unless ins.node.type == :def
767
- next unless ins.node.children[0] == name_sym
948
+ def retroactively_set_visibility(name_sym, visibility, scope:, container:)
949
+ @insertions.reverse_each do |insertion|
950
+ next unless visibility_target?(insertion, scope, container)
951
+ next unless insertion_method_name(insertion.node) == name_sym
768
952
 
769
- ins.scope = :class
770
- ins.visibility = :public
771
- ins.module_function = true
772
- ins.included_instance_visibility ||= :private
953
+ insertion.visibility = visibility
773
954
  break
774
955
  end
775
956
  end
776
957
 
958
+ # Check if an Insertion matches the given scope and container for visibility updates.
959
+ #
960
+ # @private
961
+ # @param [Insertion] insertion the Insertion struct to check
962
+ # @param [Symbol] scope the scope to match (:instance or :class)
963
+ # @param [String] container the container name to match
964
+ # @return [Boolean]
965
+ def visibility_target?(insertion, scope, container)
966
+ insertion.container == container && insertion.scope == scope
967
+ end
968
+
969
+ # Extract the method name symbol from a def or defs AST node.
970
+ #
971
+ # @private
972
+ # @param [Parser::AST::Node] node the `:def` or `:defs` AST node
973
+ # @return [Symbol, nil] the method name
974
+ def insertion_method_name(node)
975
+ case node.type
976
+ when :def
977
+ node.children[0]
978
+ when :defs
979
+ node.children[1]
980
+ end
981
+ end
982
+
777
983
  # Check if a node is a `self` literal.
778
984
  #
779
985
  # @private
780
986
  # @param [Parser::AST::Node, nil] node an AST node
781
987
  # @return [Boolean]
782
988
  def self_node?(node)
783
- node && node.type == :self
989
+ !!(node && node.type == :self)
990
+ end
991
+
992
+ # Process all nodes in a class/module body for documentation insertion targets.
993
+ #
994
+ # Handles Sorbet `sig` nodes by deferring them as pending anchors for the
995
+ # next method definition.
996
+ #
997
+ # @private
998
+ # @param [Parser::AST::Node, nil] body the body node
999
+ # @param [VisibilityCtx] ctx current visibility context
1000
+ # @return [void]
1001
+ def process_body(body, ctx)
1002
+ return unless body
1003
+
1004
+ nodes = body.type == :begin ? body.children : [body]
1005
+ pending_sig_nodes = [] #: Array[Parser::AST::Node]
1006
+
1007
+ nodes.each do |child|
1008
+ process_body_child(child, ctx, pending_sig_nodes)
1009
+ end
1010
+ end
1011
+
1012
+ # Process a single child node, collecting Sorbet sigs as pending anchors and dispatching statements.
1013
+ #
1014
+ # @private
1015
+ # @param [Parser::AST::Node] child the child AST node to process
1016
+ # @param [VisibilityCtx] ctx current visibility context
1017
+ # @param [Array<Parser::AST::Node>] pending_sig_nodes accumulator for Sorbet sig nodes
1018
+ # @return [void]
1019
+ def process_body_child(child, ctx, pending_sig_nodes)
1020
+ if sorbet_sig_node?(child)
1021
+ pending_sig_nodes << child
1022
+ return
1023
+ end
1024
+
1025
+ process_stmt(child, ctx, pending_sig_anchor: pending_sig_nodes.first)
1026
+ pending_sig_nodes.clear
1027
+ end
1028
+
1029
+ # Process a single AST node for documentation insertion targets.
1030
+ #
1031
+ # Dispatches to specific handlers based on node type (`:def`, `:defs`,
1032
+ # `:sclass`, `:send` with visibility modifiers, etc.) and records
1033
+ # `Insertion` objects for methods that need documentation.
1034
+ #
1035
+ # @private
1036
+ # @param [Parser::AST::Node, nil] node the AST node to process
1037
+ # @param [VisibilityCtx] ctx current visibility and container context
1038
+ # @param [Parser::AST::Node, nil] pending_sig_anchor Sorbet `sig` node waiting for a method
1039
+ # @return [void]
1040
+ def process_stmt(node, ctx, pending_sig_anchor: nil)
1041
+ return unless node
1042
+ return process_casgn_stmt(node) if node.type == :casgn
1043
+
1044
+ handler = PROCESS_STMT_HANDLERS[node.type]
1045
+
1046
+ if handler
1047
+ dispatch_process_stmt(handler, node, ctx, pending_sig_anchor)
1048
+ else
1049
+ process(node)
1050
+ end
1051
+ end
1052
+
1053
+ # Process a constant assignment statement, skipping Struct.new assignments.
1054
+ #
1055
+ # @private
1056
+ # @param [Parser::AST::Node] node the `:casgn` AST node
1057
+ # @return [void]
1058
+ def process_casgn_stmt(node)
1059
+ process(node) unless process_struct_casgn?(node)
1060
+ end
1061
+
1062
+ # Dispatch the statement to the appropriate handler method based on node type.
1063
+ #
1064
+ # @private
1065
+ # @param [Symbol] handler the method name to dispatch to
1066
+ # @param [Parser::AST::Node] node the AST node to process
1067
+ # @param [VisibilityCtx] ctx current visibility context
1068
+ # @param [Parser::AST::Node, nil] pending_sig_anchor Sorbet `sig` node waiting for a method
1069
+ # @return [void]
1070
+ def dispatch_process_stmt(handler, node, ctx, pending_sig_anchor)
1071
+ if %i[process_def_stmt process_defs_stmt process_send_stmt].include?(handler)
1072
+ __send__(handler, node, ctx, pending_sig_anchor: pending_sig_anchor)
1073
+ else
1074
+ __send__(handler, node, ctx)
1075
+ end
1076
+ end
1077
+
1078
+ # Check if a constant assignment is `Struct.new` and extract attribute insertions.
1079
+ #
1080
+ # @private
1081
+ # @param [Parser::AST::Node] node a `:casgn` node
1082
+ # @return [Boolean] true if the node was handled as a struct definition
1083
+ def process_struct_casgn?(node)
1084
+ _scope, _name, value = *node
1085
+ return false unless struct_new_node?(value)
1086
+
1087
+ names = extract_struct_member_names(value)
1088
+ return true if names.empty?
1089
+
1090
+ @attr_insertions << AttrInsertion.new(node, :instance, :public, struct_container_name(node), :rw, names)
1091
+
1092
+ true
1093
+ end
1094
+
1095
+ # Check if a node represents a `Struct.new` call.
1096
+ #
1097
+ # @private
1098
+ # @param [Parser::AST::Node, nil] node an AST node
1099
+ # @return [Boolean]
1100
+ def struct_new_node?(node)
1101
+ return false unless node.is_a?(Parser::AST::Node)
1102
+ return false unless node.type == :send
1103
+
1104
+ recv, meth, *_args = *node
1105
+ return false unless meth == :new
1106
+ return false unless recv&.type == :const
1107
+
1108
+ recv_name = const_name(recv)
1109
+ %w[Struct ::Struct].include?(recv_name)
1110
+ end
1111
+
1112
+ # If `extend self` is active for this module, document all instance defs as module methods (M.foo).
1113
+ #
1114
+ # @private
1115
+ # @param [VisibilityCtx] ctx current visibility context
1116
+ # @param [String] container the container name
1117
+ # @return [void]
1118
+ def persist_extend_self_state(ctx, container)
1119
+ return unless ctx.extend_self
1120
+
1121
+ promote_extend_self_container(container: container)
1122
+ (@module_states[container] ||= {})[:extend_self] = true
1123
+ end
1124
+
1125
+ # Extract member names from a `Struct.new` call, stripping the type string argument if present.
1126
+ #
1127
+ # @private
1128
+ # @param [Parser::AST::Node] struct_new_node a `:send` node representing `Struct.new`
1129
+ # @return [Array<Symbol>] extracted member names
1130
+ def extract_struct_member_names(struct_new_node)
1131
+ _recv, _meth, *args = *struct_new_node
1132
+ args ||= []
1133
+
1134
+ args.reject! { |arg| arg.is_a?(Parser::AST::Node) && arg.type == :hash }
1135
+
1136
+ drop_first_if_str!(args) if args.length >= 2
1137
+
1138
+ args.map { |arg| extract_name_sym(arg) }.compact
1139
+ end
1140
+
1141
+ # Drop the first argument if it is a string (e.g. Struct.new("Name", ...)).
1142
+ #
1143
+ # @private
1144
+ # @param [Array] args the destructured arguments from Struct.new
1145
+ # @return [void]
1146
+ def drop_first_if_str!(args)
1147
+ return unless args.first.is_a?(Parser::AST::Node)
1148
+ return unless args.first.type == :str
1149
+
1150
+ args.shift
1151
+ end
1152
+
1153
+ # Build the container name for a struct constant assignment.
1154
+ #
1155
+ # @private
1156
+ # @param [Parser::AST::Node] node a `:casgn` node
1157
+ # @return [String] the fully qualified container name
1158
+ def struct_container_name(node)
1159
+ scope, name, _value = *node
1160
+
1161
+ prefix =
1162
+ if scope
1163
+ const_name(scope)
1164
+ elsif current_container == 'Object'
1165
+ nil
1166
+ else
1167
+ current_container
1168
+ end
1169
+
1170
+ [prefix, name.to_s].compact.reject(&:empty?).join('::')
784
1171
  end
785
1172
 
786
1173
  # Extract a Ruby symbol name from an AST node (`:sym` or `:str`).
@@ -805,9 +1192,7 @@ module Docscribe
805
1192
 
806
1193
  case node.type
807
1194
  when :const
808
- scope, name = *node
809
- scope_name = scope ? const_name(scope) : nil
810
- [scope_name, name].compact.join('::')
1195
+ qualified_const_name(node)
811
1196
  when :cbase
812
1197
  ''
813
1198
  else
@@ -815,6 +1200,26 @@ module Docscribe
815
1200
  end
816
1201
  end
817
1202
 
1203
+ # Build a qualified constant name by joining scope and constant parts.
1204
+ #
1205
+ # @private
1206
+ # @param [Parser::AST::Node] node the `:const` AST node
1207
+ # @return [String] the qualified name (e.g. "Foo::Bar")
1208
+ def qualified_const_name(node)
1209
+ scope, name = *node
1210
+ scope_name = scope ? const_name(scope) : nil
1211
+ [scope_name, name].compact.join('::')
1212
+ end
1213
+
1214
+ # Get the effective container name, using `container_override` when set.
1215
+ #
1216
+ # @private
1217
+ # @param [VisibilityCtx] ctx current visibility context
1218
+ # @return [String] the container name
1219
+ def container_for(ctx)
1220
+ ctx.container_override || current_container
1221
+ end
1222
+
818
1223
  # Get the current container name from the name stack.
819
1224
  #
820
1225
  # @private
@@ -823,53 +1228,39 @@ module Docscribe
823
1228
  @name_stack.empty? ? 'Object' : @name_stack.join('::')
824
1229
  end
825
1230
 
826
- # Process all nodes in a class/module body for documentation insertion targets.
827
- #
828
- # Handles Sorbet `sig` nodes by deferring them as pending anchors for the
829
- # next method definition.
1231
+ # Check if a node is a Sorbet `sig` declaration (bare `sig` send or `sig { ... }` block).
830
1232
  #
831
1233
  # @private
832
- # @param [Parser::AST::Node, nil] body the body node
833
- # @param [VisibilityCtx] ctx current visibility context
834
- # @return [void]
835
- def process_body(body, ctx)
836
- return unless body
1234
+ # @param [Parser::AST::Node, nil] node an AST node
1235
+ # @return [Boolean]
1236
+ def sorbet_sig_node?(node)
1237
+ return false unless node.is_a?(Parser::AST::Node)
837
1238
 
838
- nodes = body.type == :begin ? body.children : [body]
839
- pending_sig_nodes = []
1239
+ sig_send_node?(node) || sig_block_node?(node)
1240
+ end
840
1241
 
841
- nodes.each do |child|
842
- if sorbet_sig_node?(child)
843
- pending_sig_nodes << child
844
- next
845
- end
1242
+ # Check if a node is a Sorbet `sig { ... }` block.
1243
+ #
1244
+ # @private
1245
+ # @param [Parser::AST::Node] node an AST node
1246
+ # @return [Boolean]
1247
+ def sig_block_node?(node)
1248
+ return false unless node.type == :block
846
1249
 
847
- process_stmt(child, ctx, pending_sig_anchor: pending_sig_nodes.first)
848
- pending_sig_nodes.clear
849
- end
1250
+ send_node, *_rest = *node
1251
+ sig_send_node?(send_node)
850
1252
  end
851
1253
 
852
- # Check if a node is a Sorbet `sig` declaration (bare `sig` send or `sig { ... }` block).
1254
+ # Check if a node is a bare Sorbet `sig` send (without block).
853
1255
  #
854
1256
  # @private
855
- # @param [Parser::AST::Node, nil] node an AST node
1257
+ # @param [Parser::AST::Node] node an AST node
856
1258
  # @return [Boolean]
857
- def sorbet_sig_node?(node)
858
- return false unless node.is_a?(Parser::AST::Node)
1259
+ def sig_send_node?(node)
1260
+ return false unless node.type == :send
859
1261
 
860
- case node.type
861
- when :send
862
- recv, meth, *_args = *node
863
- recv.nil? && meth == :sig
864
- when :block
865
- send_node, *_rest = *node
866
- return false unless send_node&.type == :send
867
-
868
- recv, meth, *_args = *send_node
869
- recv.nil? && meth == :sig
870
- else
871
- false
872
- end
1262
+ recv, meth, *_args = *node
1263
+ recv.nil? && meth == :sig
873
1264
  end
874
1265
 
875
1266
  # Promote instance methods to class methods for a container under `extend self`.