docscribe 1.4.1 → 1.5.0

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