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
@@ -6,6 +6,15 @@ module Docscribe
6
6
  module Returns
7
7
  module_function
8
8
 
9
+ LAST_EXPR_TYPE_HANDLERS = {
10
+ begin: :handle_begin_node,
11
+ if: :handle_if_node,
12
+ case: :handle_case_node,
13
+ return: :handle_return_node,
14
+ block: :handle_block_node,
15
+ send: :handle_send_node
16
+ }.freeze
17
+
9
18
  # Infer a return type from a full method definition source string.
10
19
  #
11
20
  # The source must parse to a `:def` or `:defs` node. If parsing fails or inference
@@ -18,36 +27,40 @@ module Docscribe
18
27
  def infer_return_type(method_source)
19
28
  return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty?
20
29
 
21
- buffer = Parser::Source::Buffer.new('(method)')
22
- buffer.source = method_source
23
- root = Docscribe::Parsing.parse_buffer(buffer)
30
+ root = parse_method_source(method_source)
24
31
  return FALLBACK_TYPE unless root && %i[def defs].include?(root.type)
25
32
 
26
33
  body = root.children.last
27
34
  local_var_types = build_local_variable_types(body)
28
- last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
29
- local_var_types: local_var_types) || FALLBACK_TYPE
35
+ run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
36
+ local_var_types: local_var_types) || FALLBACK_TYPE
30
37
  rescue Parser::SyntaxError
31
38
  FALLBACK_TYPE
32
39
  end
33
40
 
41
+ # Parse a Ruby source string into an AST using the Parser gem.
42
+ #
43
+ # @note module_function: when included, also defines #parse_method_source (instance visibility: private)
44
+ # @param [String] method_source the method definition source string to parse
45
+ # @return [Parser::AST::Node, nil]
46
+ def parse_method_source(method_source)
47
+ buffer = Parser::Source::Buffer.new('(method)')
48
+ buffer.source = method_source
49
+ Docscribe::Parsing.parse_buffer(buffer)
50
+ end
51
+
34
52
  # Infer a method's normal return type from an already parsed def/defs node.
35
53
  #
36
54
  # @note module_function: when included, also defines #infer_return_type_from_node (instance visibility: private)
37
55
  # @param [Parser::AST::Node] node `:def` or `:defs` node
38
56
  # @return [String]
39
57
  def infer_return_type_from_node(node)
40
- body =
41
- case node.type
42
- when :def then node.children[2]
43
- when :defs then node.children[3]
44
- end
45
-
58
+ body = extract_def_body(node)
46
59
  return FALLBACK_TYPE unless body
47
60
 
48
61
  local_var_types = build_local_variable_types(body)
49
- last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
50
- local_var_types: local_var_types) || FALLBACK_TYPE
62
+ run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
63
+ local_var_types: local_var_types) || FALLBACK_TYPE
51
64
  end
52
65
 
53
66
  # Return a structured return-type spec for a method node.
@@ -60,82 +73,313 @@ module Docscribe
60
73
  # @param [Parser::AST::Node] node `:def` or `:defs` node
61
74
  # @param [String] fallback_type type used when inference is uncertain
62
75
  # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
63
- # @param [nil] core_rbs_provider Param documentation.
64
- # @param [nil] param_types Param documentation.
76
+ # @param [nil] core_rbs_provider core RBS type lookup provider
77
+ # @param [nil] param_types parameter name -> type map
65
78
  # @return [Hash]
66
79
  def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
67
80
  param_types: nil)
68
- body =
69
- case node.type
70
- when :def then node.children[2]
71
- when :defs then node.children[3]
72
- end
73
-
74
- spec = { normal: FALLBACK_TYPE, rescues: [] }
81
+ body = extract_def_body(node)
82
+ spec = { normal: FALLBACK_TYPE, rescues: [] } #: Hash[Symbol, untyped]
75
83
  return spec unless body
76
84
 
77
85
  local_var_types = build_local_variable_types(body)
78
86
 
87
+ populate_returns_spec(spec, body, local_var_types,
88
+ fallback_type: fallback_type,
89
+ nil_as_optional: nil_as_optional,
90
+ core_rbs_provider: core_rbs_provider,
91
+ param_types: param_types)
92
+
93
+ spec
94
+ end
95
+
96
+ # Extract the body child node from a `:def` or `:defs` AST node.
97
+ #
98
+ # @note module_function: when included, also defines #extract_def_body (instance visibility: private)
99
+ # @param [Parser::AST::Node] node a `:def` or `:defs` AST node
100
+ # @return [Parser::AST::Node, nil]
101
+ def extract_def_body(node)
102
+ case node.type
103
+ when :def then node.children[2]
104
+ when :defs then node.children[3]
105
+ end
106
+ end
107
+
108
+ # Populate the spec hash with normal and/or rescue return types from the body.
109
+ #
110
+ # @note module_function: when included, also defines #populate_returns_spec (instance visibility: private)
111
+ # @param [Hash] spec the return spec hash to populate
112
+ # @param [Parser::AST::Node] body the method body AST node
113
+ # @param [Hash, nil] local_var_types inferred local variable type map
114
+ # @param [Hash] opts additional keyword options forwarded to type inference
115
+ # @return [Hash]
116
+ def populate_returns_spec(spec, body, local_var_types, **opts)
79
117
  if body.type == :rescue
80
- main_body = body.children[0]
81
- rescue_local_var_types = build_local_variable_types(body)
82
- all_local_var_types = rescue_local_var_types || local_var_types
83
- spec[:normal] =
84
- last_expr_type(main_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
85
- core_rbs_provider: core_rbs_provider, param_types: param_types,
86
- local_var_types: all_local_var_types) || FALLBACK_TYPE
87
-
88
- body.children.each do |ch|
89
- next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
90
-
91
- exc_list, _asgn, rescue_body = *ch
92
- exc_names = Raises.exception_names_from_rescue_list(exc_list)
93
- rtype =
94
- last_expr_type(rescue_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
95
- core_rbs_provider: core_rbs_provider, param_types: param_types,
96
- local_var_types: all_local_var_types) ||
97
- fallback_type
98
- spec[:rescues] << [exc_names, rtype]
99
- end
118
+ process_rescue_body(spec, body, **opts)
100
119
  else
101
- spec[:normal] =
102
- last_expr_type(body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
103
- core_rbs_provider: core_rbs_provider, param_types: param_types,
104
- local_var_types: local_var_types) || FALLBACK_TYPE
120
+ spec[:normal] = infer_normal_return_type(body, **opts, local_var_types: local_var_types)
105
121
  end
122
+ end
106
123
 
107
- spec
124
+ # Infer the normal (non-rescue) return type from a method body node.
125
+ #
126
+ # @note module_function: when included, also defines #infer_normal_return_type (instance visibility: private)
127
+ # @param [Parser::AST::Node] body the method body AST node
128
+ # @param [Hash] opts additional keyword options forwarded to type inference
129
+ # @return [String]
130
+ def infer_normal_return_type(body, **opts)
131
+ run_last_expr_type(body, **opts) || FALLBACK_TYPE
108
132
  end
109
133
 
110
- # Resolve a return type from core RBS for a method call.
134
+ # Process a :rescue body node and populate spec with normal + rescue return types.
111
135
  #
112
- # @note module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
113
- # @private
114
- # @param [Object] node Param documentation.
115
- # @return [String] FALLBACK_TYPE if lookup fails
136
+ # @note module_function: when included, also defines #process_rescue_body (instance visibility: private)
137
+ # @param [Hash] spec the return spec hash to populate
138
+ # @param [Parser::AST::Node] body the :rescue AST node
139
+ # @param [String] fallback_type type used when inference is uncertain
140
+ # @param [Boolean] nil_as_optional whether nil unions render as optional types
141
+ # @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
142
+ # @param [Hash, nil] param_types parameter name to type map
143
+ # @param [Hash] opts additional keyword options forwarded to type inference
144
+ # @return [Hash]
145
+ def process_rescue_body(spec, body, **opts)
146
+ main_body = body.children[0]
147
+ local_var_types = build_local_variable_types(body)
148
+ rescue_opts = opts.merge(local_var_types: local_var_types)
149
+ spec[:normal] = run_last_expr_type(main_body, **rescue_opts) || FALLBACK_TYPE
150
+ process_rescue_branches(spec, body, **rescue_opts)
151
+ end
152
+
153
+ # Extract return types from each :resbody child and append to spec[:rescues].
154
+ #
155
+ # @note module_function: when included, also defines #process_rescue_branches (instance visibility: private)
156
+ # @param [Hash] spec the return spec hash to populate
157
+ # @param [Parser::AST::Node] body the :rescue AST node
158
+ # @param [Hash] opts additional keyword options forwarded to type inference
159
+ # @return [Array] the list of rescue type entries
160
+ def process_rescue_branches(spec, body, **opts)
161
+ body.children.each do |ch|
162
+ next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
163
+
164
+ exc_list, _asgn, rescue_body = *ch
165
+ exc_names = Raises.exception_names_from_rescue_list(exc_list)
166
+ rtype = run_last_expr_type(rescue_body, **opts) || opts[:fallback_type]
167
+ spec[:rescues] << [exc_names, rtype]
168
+ end
169
+ end
170
+
171
+ # Build a map of local/global/ivar/constant assignments to inferred types.
172
+ #
173
+ # @note module_function: when included, also defines #build_local_variable_types (instance visibility: private)
174
+ # @param [Parser::AST::Node] node AST node to walk
175
+ # @return [Hash, nil]
116
176
  def build_local_variable_types(node)
117
- types = {}
177
+ types = {} #: Hash[String, String]
118
178
  ASTWalk.walk(node) do |n|
119
- case n.type
120
- when :lvasgn, :gvasgn, :ivasgn
121
- name = n.children[0].to_s
122
- value = n.children[1]
123
- if value
124
- inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
125
- types[name] = inferred if inferred && inferred != FALLBACK_TYPE
126
- end
127
- when :casgn
128
- name = n.children[0].to_s
129
- value = n.children[2]
130
- if value
131
- inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
132
- types[name] = inferred if inferred && inferred != FALLBACK_TYPE
133
- end
134
- end
179
+ collect_assignment_type(n, types)
135
180
  end
136
181
  types.empty? ? nil : types
137
182
  end
138
183
 
184
+ # Infer the type of a single assignment node and store it in the types hash.
185
+ #
186
+ # @note module_function: when included, also defines #collect_assignment_type (instance visibility: private)
187
+ # @param [Parser::AST::Node] node an assignment AST node
188
+ # @param [Hash] types the accumulated local variable type map
189
+ # @return [void]
190
+ def collect_assignment_type(node, types)
191
+ name, value = assignment_name_and_value(node)
192
+ return unless name && value
193
+
194
+ inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
195
+ types[name] = inferred if inferred && inferred != FALLBACK_TYPE
196
+ end
197
+
198
+ # Extract the variable name and value expression from an assignment node.
199
+ #
200
+ # @note module_function: when included, also defines #assignment_name_and_value (instance visibility: private)
201
+ # @param [Parser::AST::Node] node an assignment AST node (:lvasgn, :gvasgn, :ivasgn, :casgn)
202
+ # @return [Array<(String, Parser::AST::Node)>] pair of variable name and value node
203
+ def assignment_name_and_value(node)
204
+ case node.type
205
+ when :lvasgn, :gvasgn, :ivasgn
206
+ [node.children[0].to_s, node.children[1]]
207
+ when :casgn
208
+ [node.children[0].to_s, node.children[2]]
209
+ else
210
+ [nil, nil]
211
+ end
212
+ end
213
+
214
+ # Handle `:begin` node for last_expr_type.
215
+ #
216
+ # @note module_function: when included, also defines #handle_begin_node (instance visibility: private)
217
+ # @param [Object] node
218
+ # @param [Hash] opts
219
+ # @return [Object]
220
+ def handle_begin_node(node, **opts)
221
+ run_last_expr_type(node.children.last, **opts)
222
+ end
223
+
224
+ # Handle `:if` node for last_expr_type.
225
+ #
226
+ # @note module_function: when included, also defines #handle_if_node (instance visibility: private)
227
+ # @param [Object] node
228
+ # @param [Hash] opts
229
+ # @return [Object]
230
+ def handle_if_node(node, **opts)
231
+ t = run_last_expr_type(node.children[1], **opts)
232
+ e = run_last_expr_type(node.children[2], **opts)
233
+ unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
234
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
235
+ end
236
+
237
+ # Handle `:case` node for last_expr_type.
238
+ #
239
+ # @note module_function: when included, also defines #handle_case_node (instance visibility: private)
240
+ # @param [Object] node
241
+ # @param [Hash] opts
242
+ # @return [Object]
243
+ def handle_case_node(node, **opts)
244
+ branches = process_case_branches(node, **opts)
245
+ if branches.empty?
246
+ opts[:fallback_type]
247
+ else
248
+ branches.reduce do |a, b|
249
+ unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
250
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
251
+ end
252
+ end
253
+ end
254
+
255
+ # Extract inferred return types from all branches of a :case expression.
256
+ #
257
+ # @note module_function: when included, also defines #process_case_branches (instance visibility: private)
258
+ # @param [Parser::AST::Node] node the :case AST node
259
+ # @param [Hash] opts additional keyword options forwarded to type inference
260
+ # @return [Array<String>] list of inferred types from each branch
261
+ def process_case_branches(node, **opts)
262
+ (node.children[1..] || []).compact.flat_map do |child|
263
+ if child.type == :when
264
+ run_last_expr_type(child.children.last, **opts)
265
+ else
266
+ run_last_expr_type(child, **opts)
267
+ end
268
+ end.compact
269
+ end
270
+
271
+ # Handle `:block` node for last_expr_type.
272
+ #
273
+ # @note module_function: when included, also defines #handle_block_node (instance visibility: private)
274
+ # @param [Object] node
275
+ # @param [Hash] opts
276
+ # @return [Object]
277
+ def handle_block_node(node, **opts)
278
+ send_node = node.children[0]
279
+ if send_node&.type == :send
280
+ recv = send_node.children[0]
281
+ meth = send_node.children[1]
282
+ rbs_type = resolve_rbs_for_send(recv, meth, opts[:core_rbs_provider], opts[:local_var_types],
283
+ opts[:param_types])
284
+ return rbs_type if rbs_type
285
+ end
286
+
287
+ run_last_expr_type(node.children[2], **opts)
288
+ end
289
+
290
+ # Handle `:send` node for last_expr_type.
291
+ #
292
+ # @note module_function: when included, also defines #handle_send_node (instance visibility: private)
293
+ # @param [Object] node
294
+ # @param [Hash] opts
295
+ # @return [Object]
296
+ def handle_send_node(node, **opts)
297
+ recv = node.children[0]
298
+ meth = node.children[1]
299
+
300
+ if opts[:core_rbs_provider]
301
+ rbs_type = resolve_rbs_for_send(recv, meth, opts[:core_rbs_provider], opts[:local_var_types],
302
+ opts[:param_types])
303
+ return rbs_type if rbs_type
304
+ end
305
+
306
+ Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
307
+ end
308
+
309
+ # Resolve RBS return type for a send node's receiver, if possible.
310
+ #
311
+ # Handles `:lvar` and chained `:send` receivers.
312
+ #
313
+ # @note module_function: when included, also defines #resolve_rbs_for_send (instance visibility: private)
314
+ # @param [Parser::AST::Node, nil] recv the receiver node of the send
315
+ # @param [Symbol] meth the method name being called
316
+ # @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
317
+ # @param [Hash, nil] local_var_types inferred local variable type map
318
+ # @param [Hash, nil] param_types parameter name to type map
319
+ # @return [String, nil] resolved type or nil if unresolvable
320
+ def resolve_rbs_for_send(recv, meth, core_rbs_provider, local_var_types, param_types)
321
+ return nil unless core_rbs_provider
322
+
323
+ if recv&.type == :lvar
324
+ resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
325
+ elsif recv&.type == :send
326
+ resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
327
+ end
328
+ end
329
+
330
+ # Resolve RBS return type for an `:lvar` receiver.
331
+ #
332
+ # @note module_function: when included, also defines # (instance visibility: private)
333
+ # @private
334
+ # @param [Object] recv
335
+ # @param [Object] meth
336
+ # @param [Object] core_rbs_provider
337
+ # @param [Object] local_var_types
338
+ # @param [Object] param_types
339
+ # @return [String, nil]
340
+ def resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
341
+ lvar_name = recv&.children&.first
342
+ recv_type = lookup_lvar_type(lvar_name, local_var_types, param_types)
343
+ return nil unless recv_type
344
+
345
+ rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
346
+ rbs_type unless rbs_type == FALLBACK_TYPE
347
+ end
348
+
349
+ # Look up a local variable's inferred type from local or parameter type maps.
350
+ #
351
+ # @note module_function: when included, also defines #lookup_lvar_type (instance visibility: private)
352
+ # @param [Symbol] lvar_name the local variable name
353
+ # @param [Hash, nil] local_var_types inferred local variable type map
354
+ # @param [Hash, nil] param_types parameter name to type map
355
+ # @return [String, nil]
356
+ def lookup_lvar_type(lvar_name, local_var_types, param_types)
357
+ return local_var_types[lvar_name.to_s] if local_var_types&.key?(lvar_name.to_s)
358
+ return param_types[lvar_name.to_s] if param_types&.key?(lvar_name.to_s)
359
+
360
+ nil
361
+ end
362
+
363
+ # Resolve RBS return type for a chained `:send` receiver.
364
+ #
365
+ # @note module_function: when included, also defines # (instance visibility: private)
366
+ # @private
367
+ # @param [Object] recv
368
+ # @param [Object] meth
369
+ # @param [Object] core_rbs_provider
370
+ # @param [Object] local_var_types
371
+ # @param [Object] param_types
372
+ # @return [String, nil]
373
+ def resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
374
+ inner_type = run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
375
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
376
+ local_var_types: local_var_types)
377
+ return nil unless inner_type
378
+
379
+ rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
380
+ rbs_type unless rbs_type == FALLBACK_TYPE
381
+ end
382
+
139
383
  # Infer the type of the last expression in a node.
140
384
  #
141
385
  # Supports:
@@ -152,122 +396,47 @@ module Docscribe
152
396
  # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
153
397
  # @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
154
398
  # @param [Hash, nil] param_types parameter name -> type map for lvar resolution
155
- # @param [nil] local_var_types Param documentation.
399
+ # @param [nil] local_var_types pre-built local variable types map
400
+ # @param [Hash] opts additional keyword options forwarded to type inference
156
401
  # @return [String, nil]
157
- def last_expr_type(node, fallback_type:, nil_as_optional:, core_rbs_provider: nil, param_types: nil,
158
- local_var_types: nil)
159
- return nil unless node
160
-
161
- case node.type
162
- when :begin
163
- last_expr_type(node.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
164
- core_rbs_provider: core_rbs_provider, param_types: param_types,
165
- local_var_types: local_var_types)
166
-
167
- when :if
168
- t = last_expr_type(node.children[1], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
169
- core_rbs_provider: core_rbs_provider, param_types: param_types,
170
- local_var_types: local_var_types)
171
- e = last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
172
- core_rbs_provider: core_rbs_provider, param_types: param_types,
173
- local_var_types: local_var_types)
174
- unify_types(t, e, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
175
-
176
- when :case
177
- branches = node.children[1..].compact.flat_map do |child|
178
- if child.type == :when
179
- last_expr_type(child.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
180
- core_rbs_provider: core_rbs_provider, param_types: param_types,
181
- local_var_types: local_var_types)
182
- else
183
- last_expr_type(child, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
184
- core_rbs_provider: core_rbs_provider, param_types: param_types,
185
- local_var_types: local_var_types)
186
- end
187
- end.compact
188
-
189
- if branches.empty?
190
- fallback_type
191
- else
192
- branches.reduce do |a, b|
193
- unify_types(a, b, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
194
- end
195
- end
196
-
197
- when :return
198
- Literals.type_from_literal(node.children.first, fallback_type: fallback_type)
199
-
200
- when :block
201
- send_node = node.children[0]
202
- if send_node&.type == :send
203
- recv = send_node.children[0]
204
- meth = send_node.children[1]
205
-
206
- if core_rbs_provider && recv&.type == :lvar
207
- lvar_name = recv.children.first
208
- recv_type = nil
209
- recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name
210
- recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name
211
- if recv_type
212
- rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
213
- return rbs_type unless rbs_type == FALLBACK_TYPE
214
- end
215
- elsif core_rbs_provider && recv&.type == :send
216
- inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
217
- core_rbs_provider: core_rbs_provider, param_types: param_types,
218
- local_var_types: local_var_types)
219
- if inner_type
220
- rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
221
- return rbs_type unless rbs_type == FALLBACK_TYPE
222
- end
223
- end
224
- end
225
-
226
- last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
227
- core_rbs_provider: core_rbs_provider, param_types: param_types,
228
- local_var_types: local_var_types)
229
-
230
- when :send
231
- recv = node.children[0]
232
- meth = node.children[1]
233
-
234
- # Try to resolve return type from RBS core for method calls
235
- if core_rbs_provider && recv&.type == :send
236
- # Chained call: arg.to_i.positive?
237
- inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
238
- core_rbs_provider: core_rbs_provider, param_types: param_types,
239
- local_var_types: local_var_types)
240
- if inner_type
241
- rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
242
- return rbs_type unless rbs_type == FALLBACK_TYPE
243
- end
244
-
245
- elsif core_rbs_provider && recv&.type == :lvar
246
- # Direct call on local variable: p1.positive? or admins.any?
247
- lvar_name = recv.children.first
248
- recv_type = nil
249
- recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name
250
- recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name
251
- if recv_type
252
- rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
253
- return rbs_type unless rbs_type == FALLBACK_TYPE
254
- end
255
- end
402
+ def last_expr_type(node, **opts)
403
+ run_last_expr_type(node, **opts)
404
+ end
256
405
 
257
- Literals.type_from_literal(node, fallback_type: fallback_type)
406
+ # Dispatch `last_expr_type` based on node type.
407
+ #
408
+ # @note module_function: when included, also defines #run_last_expr_type (instance visibility: private)
409
+ # @param [Parser::AST::Node, nil] node
410
+ # @param [Hash] opts options passed through as keyword args
411
+ # @return [String, nil]
412
+ def run_last_expr_type(node, **opts)
413
+ return unless node
258
414
 
415
+ handler = LAST_EXPR_TYPE_HANDLERS[node.type]
416
+ if handler
417
+ send(handler, node, **opts)
259
418
  else
260
- Literals.type_from_literal(node, fallback_type: fallback_type)
419
+ Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
261
420
  end
262
421
  end
263
422
 
264
- # Method documentation.
423
+ # Extract the return type from an explicit `:return` node.
424
+ #
425
+ # @note module_function: when included, also defines #handle_return_node (instance visibility: private)
426
+ # @param [Parser::AST::Node] node the `:return` AST node
427
+ # @param [Hash] opts additional keyword options forwarded to type inference
428
+ # @return [String, nil]
429
+ def handle_return_node(node, **opts)
430
+ Literals.type_from_literal(node.children.first, fallback_type: opts[:fallback_type])
431
+ end
432
+
433
+ # Resolve an RBS return type for a method call.
265
434
  #
266
435
  # @note module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
267
- # @param [Object] container_type Param documentation.
268
- # @param [Object] method_name Param documentation.
269
- # @param [Object] core_rbs_provider Param documentation.
270
- # @return [Object]
436
+ # @param [String] container_type class or module name
437
+ # @param [String] method_name method name
438
+ # @param [Object] core_rbs_provider core RBS type lookup provider
439
+ # @return [String] inferred return type
271
440
  def resolve_rbs_return_type(container_type, method_name, core_rbs_provider)
272
441
  return FALLBACK_TYPE unless core_rbs_provider
273
442
 
@@ -292,14 +461,28 @@ module Docscribe
292
461
  # @param [String, nil] b
293
462
  # @param [String] fallback_type
294
463
  # @param [Boolean] nil_as_optional
464
+ # @param [String, nil] type_a first type to unify
465
+ # @param [String, nil] type_b second type to unify
295
466
  # @return [String, nil]
296
- def unify_types(a, b, fallback_type:, nil_as_optional:)
297
- a ||= fallback_type
298
- b ||= fallback_type
299
- return a if a == b
467
+ def unify_types(type_a, type_b, fallback_type:, nil_as_optional:)
468
+ type_a ||= fallback_type
469
+ type_b ||= fallback_type
470
+ return type_a if type_a == type_b
300
471
 
301
- if a == 'nil' || b == 'nil'
302
- non_nil = (a == 'nil' ? b : a)
472
+ unify_nil_types(type_a, type_b, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
473
+ end
474
+
475
+ # Unify two types where one may be `nil`, producing optional or union type.
476
+ #
477
+ # @note module_function: when included, also defines #unify_nil_types (instance visibility: private)
478
+ # @param [String] type_a first type string
479
+ # @param [String] type_b second type string
480
+ # @param [String] fallback_type type used when neither is nil
481
+ # @param [Boolean] nil_as_optional whether to render nil unions as optional types
482
+ # @return [String]
483
+ def unify_nil_types(type_a, type_b, fallback_type:, nil_as_optional:)
484
+ if type_a == 'nil' || type_b == 'nil'
485
+ non_nil = (type_a == 'nil' ? type_b : type_a)
303
486
  return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil"
304
487
  end
305
488
 
@@ -93,8 +93,8 @@ module Docscribe
93
93
  # @param [Parser::AST::Node] node
94
94
  # @param [String] fallback_type
95
95
  # @param [Boolean] nil_as_optional
96
- # @param [nil] core_rbs_provider Param documentation.
97
- # @param [nil] param_types Param documentation.
96
+ # @param [nil] core_rbs_provider core RBS type lookup provider
97
+ # @param [nil] param_types parameter name -> type map
98
98
  # @return [Hash]
99
99
  def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
100
100
  param_types: nil)
@@ -124,9 +124,10 @@ module Docscribe
124
124
  # Convert a constant AST node into its fully qualified name.
125
125
  #
126
126
  # @param [Parser::AST::Node, nil] n
127
+ # @param [Parser::AST::Node, nil] node constant AST node to resolve
127
128
  # @return [String, nil]
128
- def const_full_name(n)
129
- Names.const_full_name(n)
129
+ def const_full_name(node)
130
+ Names.const_full_name(node)
130
131
  end
131
132
 
132
133
  # Infer a YARD-ish type string from a literal AST node.
@@ -140,15 +141,15 @@ module Docscribe
140
141
 
141
142
  # Unify two inferred type strings conservatively.
142
143
  #
143
- # @param [String, nil] a
144
- # @param [String, nil] b
144
+ # @param [String, nil] type_a
145
+ # @param [String, nil] type_b
145
146
  # @param [String] fallback_type
146
147
  # @param [Boolean] nil_as_optional
147
148
  # @return [String]
148
- def unify_types(a, b, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
149
+ def unify_types(type_a, type_b, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
149
150
  Returns.unify_types(
150
- a,
151
- b,
151
+ type_a,
152
+ type_b,
152
153
  fallback_type: fallback_type,
153
154
  nil_as_optional: nil_as_optional
154
155
  )