docscribe 1.4.2 → 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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +465 -130
  3. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  4. data/lib/docscribe/cli/config_builder.rb +107 -53
  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 +45 -45
  10. data/lib/docscribe/cli/init.rb +14 -6
  11. data/lib/docscribe/cli/options.rb +190 -88
  12. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  13. data/lib/docscribe/cli/run.rb +210 -152
  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 +21 -13
  17. data/lib/docscribe/config/defaults.rb +5 -1
  18. data/lib/docscribe/config/emit.rb +17 -0
  19. data/lib/docscribe/config/filtering.rb +18 -25
  20. data/lib/docscribe/config/loader.rb +15 -11
  21. data/lib/docscribe/config/plugin.rb +1 -1
  22. data/lib/docscribe/config/rbs.rb +41 -9
  23. data/lib/docscribe/config/sorbet.rb +9 -12
  24. data/lib/docscribe/config/sorting.rb +1 -1
  25. data/lib/docscribe/config/template.rb +9 -1
  26. data/lib/docscribe/config/utils.rb +11 -9
  27. data/lib/docscribe/config.rb +2 -4
  28. data/lib/docscribe/infer/ast_walk.rb +1 -1
  29. data/lib/docscribe/infer/literals.rb +6 -11
  30. data/lib/docscribe/infer/names.rb +2 -3
  31. data/lib/docscribe/infer/params.rb +15 -17
  32. data/lib/docscribe/infer/raises.rb +3 -5
  33. data/lib/docscribe/infer/returns.rb +542 -140
  34. data/lib/docscribe/infer.rb +22 -23
  35. data/lib/docscribe/inline_rewriter/collector.rb +159 -164
  36. data/lib/docscribe/inline_rewriter/doc_block.rb +145 -115
  37. data/lib/docscribe/inline_rewriter/doc_builder.rb +1026 -723
  38. data/lib/docscribe/inline_rewriter/source_helpers.rb +49 -49
  39. data/lib/docscribe/inline_rewriter/tag_sorter.rb +82 -85
  40. data/lib/docscribe/inline_rewriter.rb +495 -492
  41. data/lib/docscribe/parsing.rb +29 -10
  42. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -1
  43. data/lib/docscribe/plugin/base/tag_plugin.rb +0 -1
  44. data/lib/docscribe/plugin/context.rb +28 -18
  45. data/lib/docscribe/plugin/registry.rb +26 -27
  46. data/lib/docscribe/plugin/tag.rb +9 -14
  47. data/lib/docscribe/plugin.rb +17 -16
  48. data/lib/docscribe/types/provider_chain.rb +4 -2
  49. data/lib/docscribe/types/rbs/collection_loader.rb +2 -2
  50. data/lib/docscribe/types/rbs/provider.rb +60 -44
  51. data/lib/docscribe/types/rbs/type_formatter.rb +224 -83
  52. data/lib/docscribe/types/signature.rb +22 -42
  53. data/lib/docscribe/types/sorbet/base_provider.rb +24 -19
  54. data/lib/docscribe/types/sorbet/rbi_provider.rb +3 -3
  55. data/lib/docscribe/types/sorbet/source_provider.rb +3 -2
  56. data/lib/docscribe/types/yard/formatter.rb +100 -0
  57. data/lib/docscribe/types/yard/parser.rb +240 -0
  58. data/lib/docscribe/types/yard/types.rb +52 -0
  59. data/lib/docscribe/version.rb +1 -1
  60. metadata +33 -1
@@ -6,24 +6,16 @@ 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
-
18
9
  # Infer a return type from a full method definition source string.
19
10
  #
20
11
  # The source must parse to a `:def` or `:defs` node. If parsing fails or inference
21
12
  # is uncertain, the fallback type is returned.
22
13
  #
23
- # @note module_function: when included, also defines #infer_return_type (instance visibility: private)
24
- # @param [String, nil] method_source full method definition source
14
+ # @note module_function: defines #infer_return_type (visibility: private)
15
+ # @param [String?] method_source full method definition source
25
16
  # @raise [Parser::SyntaxError]
26
- # @return [String]
17
+ # @return [String] if Parser::SyntaxError
18
+ # @return [FALLBACK_TYPE] if Parser::SyntaxError
27
19
  def infer_return_type(method_source)
28
20
  return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty?
29
21
 
@@ -34,13 +26,13 @@ module Docscribe
34
26
  local_var_types = build_local_variable_types(body)
35
27
  run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
36
28
  local_var_types: local_var_types) || FALLBACK_TYPE
37
- rescue Parser::SyntaxError
29
+ rescue Parser::SyntaxError # steep:ignore
38
30
  FALLBACK_TYPE
39
31
  end
40
32
 
41
33
  # Parse a Ruby source string into an AST using the Parser gem.
42
34
  #
43
- # @note module_function: when included, also defines #parse_method_source (instance visibility: private)
35
+ # @note module_function: defines #parse_method_source (visibility: private)
44
36
  # @param [String] method_source the method definition source string to parse
45
37
  # @return [Parser::AST::Node, nil]
46
38
  def parse_method_source(method_source)
@@ -51,7 +43,7 @@ module Docscribe
51
43
 
52
44
  # Infer a method's normal return type from an already parsed def/defs node.
53
45
  #
54
- # @note module_function: when included, also defines #infer_return_type_from_node (instance visibility: private)
46
+ # @note module_function: defines #infer_return_type_from_node (visibility: private)
55
47
  # @param [Parser::AST::Node] node `:def` or `:defs` node
56
48
  # @return [String]
57
49
  def infer_return_type_from_node(node)
@@ -69,33 +61,28 @@ module Docscribe
69
61
  # - `:normal` => normal/happy-path return type
70
62
  # - `:rescues` => array of `[exception_names, return_type]` pairs for rescue branches
71
63
  #
72
- # @note module_function: when included, also defines #returns_spec_from_node (instance visibility: private)
64
+ # @note module_function: defines #returns_spec_from_node (visibility: private)
73
65
  # @param [Parser::AST::Node] node `:def` or `:defs` node
74
66
  # @param [String] fallback_type type used when inference is uncertain
75
67
  # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
76
- # @param [nil] core_rbs_provider core RBS type lookup provider
77
- # @param [nil] param_types parameter name -> type map
78
- # @return [Hash]
68
+ # @param [Object?] core_rbs_provider core RBS type lookup provider
69
+ # @param [Hash<String, String>?] param_types parameter name -> type map
70
+ # @return [Object]
79
71
  def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
80
72
  param_types: nil)
81
73
  body = extract_def_body(node)
82
74
  spec = { normal: FALLBACK_TYPE, rescues: [] } #: Hash[Symbol, untyped]
83
75
  return spec unless body
84
76
 
85
- local_var_types = build_local_variable_types(body)
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
-
77
+ types = build_local_variable_types(body, core_rbs_provider: core_rbs_provider, param_types: param_types)
78
+ populate_returns_spec(spec, body, types, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
79
+ core_rbs_provider: core_rbs_provider, param_types: param_types)
93
80
  spec
94
81
  end
95
82
 
96
83
  # Extract the body child node from a `:def` or `:defs` AST node.
97
84
  #
98
- # @note module_function: when included, also defines #extract_def_body (instance visibility: private)
85
+ # @note module_function: defines #extract_def_body (visibility: private)
99
86
  # @param [Parser::AST::Node] node a `:def` or `:defs` AST node
100
87
  # @return [Parser::AST::Node, nil]
101
88
  def extract_def_body(node)
@@ -107,12 +94,12 @@ module Docscribe
107
94
 
108
95
  # Populate the spec hash with normal and/or rescue return types from the body.
109
96
  #
110
- # @note module_function: when included, also defines #populate_returns_spec (instance visibility: private)
111
- # @param [Hash] spec the return spec hash to populate
97
+ # @note module_function: defines #populate_returns_spec (visibility: private)
98
+ # @param [Object] spec the return spec hash to populate
112
99
  # @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]
100
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
101
+ # @param [Object] opts additional keyword options forwarded to type inference
102
+ # @return [Object]
116
103
  def populate_returns_spec(spec, body, local_var_types, **opts)
117
104
  if body.type == :rescue
118
105
  process_rescue_body(spec, body, **opts)
@@ -123,9 +110,9 @@ module Docscribe
123
110
 
124
111
  # Infer the normal (non-rescue) return type from a method body node.
125
112
  #
126
- # @note module_function: when included, also defines #infer_normal_return_type (instance visibility: private)
113
+ # @note module_function: defines #infer_normal_return_type (visibility: private)
127
114
  # @param [Parser::AST::Node] body the method body AST node
128
- # @param [Hash] opts additional keyword options forwarded to type inference
115
+ # @param [Object] opts additional keyword options forwarded to type inference
129
116
  # @return [String]
130
117
  def infer_normal_return_type(body, **opts)
131
118
  run_last_expr_type(body, **opts) || FALLBACK_TYPE
@@ -133,18 +120,16 @@ module Docscribe
133
120
 
134
121
  # Process a :rescue body node and populate spec with normal + rescue return types.
135
122
  #
136
- # @note module_function: when included, also defines #process_rescue_body (instance visibility: private)
137
- # @param [Hash] spec the return spec hash to populate
123
+ # @note module_function: defines #process_rescue_body (visibility: private)
124
+ # @param [Object] spec the return spec hash to populate
138
125
  # @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]
126
+ # @param [Object] opts additional keyword options forwarded to type inference
127
+ # @return [Object]
145
128
  def process_rescue_body(spec, body, **opts)
146
129
  main_body = body.children[0]
147
- local_var_types = build_local_variable_types(body)
130
+ local_var_types = build_local_variable_types(body,
131
+ core_rbs_provider: opts[:core_rbs_provider],
132
+ param_types: opts[:param_types])
148
133
  rescue_opts = opts.merge(local_var_types: local_var_types)
149
134
  spec[:normal] = run_last_expr_type(main_body, **rescue_opts) || FALLBACK_TYPE
150
135
  process_rescue_branches(spec, body, **rescue_opts)
@@ -152,11 +137,11 @@ module Docscribe
152
137
 
153
138
  # Extract return types from each :resbody child and append to spec[:rescues].
154
139
  #
155
- # @note module_function: when included, also defines #process_rescue_branches (instance visibility: private)
156
- # @param [Hash] spec the return spec hash to populate
140
+ # @note module_function: defines #process_rescue_branches (visibility: private)
141
+ # @param [Object] spec the return spec hash to populate
157
142
  # @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
143
+ # @param [Object] opts additional keyword options forwarded to type inference
144
+ # @return [Array<Object>] the list of rescue type entries
160
145
  def process_rescue_branches(spec, body, **opts)
161
146
  body.children.each do |ch|
162
147
  next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
@@ -170,76 +155,229 @@ module Docscribe
170
155
 
171
156
  # Build a map of local/global/ivar/constant assignments to inferred types.
172
157
  #
173
- # @note module_function: when included, also defines #build_local_variable_types (instance visibility: private)
158
+ # @note module_function: defines #build_local_variable_types (visibility: private)
174
159
  # @param [Parser::AST::Node] node AST node to walk
175
- # @return [Hash, nil]
176
- def build_local_variable_types(node)
160
+ # @param [Object] opts additional keyword options forwarded to inference
161
+ # @return [Hash<String, String>, nil]
162
+ def build_local_variable_types(node, **opts)
177
163
  types = {} #: Hash[String, String]
178
164
  ASTWalk.walk(node) do |n|
179
- collect_assignment_type(n, types)
165
+ collect_assignment_type(n, types, **opts)
180
166
  end
181
167
  types.empty? ? nil : types
182
168
  end
183
169
 
184
170
  # Infer the type of a single assignment node and store it in the types hash.
185
171
  #
186
- # @note module_function: when included, also defines #collect_assignment_type (instance visibility: private)
172
+ # Uses `run_last_expr_type` when `core_rbs_provider` is available to
173
+ # resolve send expressions (e.g., `x = 123 + 1` -> `Integer`).
174
+ # Falls back to `Literals.type_from_literal` for plain literals.
175
+ #
176
+ # @note module_function: defines #collect_assignment_type (visibility: private)
187
177
  # @param [Parser::AST::Node] node an assignment AST node
188
- # @param [Hash] types the accumulated local variable type map
178
+ # @param [Hash<String, String>] types the accumulated local variable type map
179
+ # @param [Object] opts additional keyword options forwarded to inference
189
180
  # @return [void]
190
- def collect_assignment_type(node, types)
181
+ def collect_assignment_type(node, types, **opts)
191
182
  name, value = assignment_name_and_value(node)
192
183
  return unless name && value
193
184
 
194
- inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
185
+ inferred = if opts[:core_rbs_provider]
186
+ run_last_expr_type(value, **opts, fallback_type: FALLBACK_TYPE,
187
+ nil_as_optional: false, local_var_types: types)
188
+ else
189
+ Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
190
+ end
195
191
  types[name] = inferred if inferred && inferred != FALLBACK_TYPE
196
192
  end
197
193
 
198
194
  # Extract the variable name and value expression from an assignment node.
199
195
  #
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
196
+ # @note module_function: defines #assignment_name_and_value (visibility: private)
197
+ # @param [Parser::AST::Node] node an assignment AST node (:lvasgn, :gvasgn, :ivasgn, :casgn, :op_asgn)
198
+ # @return [(String, nil, Parser::AST::Node, nil)]
203
199
  def assignment_name_and_value(node)
204
200
  case node.type
205
- when :lvasgn, :gvasgn, :ivasgn
201
+ when :lvasgn, :gvasgn, :ivasgn, :cvasgn
206
202
  [node.children[0].to_s, node.children[1]]
207
203
  when :casgn
208
- [node.children[0].to_s, node.children[2]]
204
+ constant_name_and_value(node)
205
+ when :op_asgn
206
+ compound_name_and_value(node)
209
207
  else
210
208
  [nil, nil]
211
209
  end
212
210
  end
213
211
 
212
+ # Extract the name and value from a `:casgn` (constant assignment) node.
213
+ #
214
+ # @note module_function: defines #constant_name_and_value (visibility: private)
215
+ # @param [Parser::AST::Node] node the `:casgn` AST node
216
+ # @return [(String, nil, Parser::AST::Node, nil)]
217
+ def constant_name_and_value(node)
218
+ [node.children[0].to_s, node.children[2]]
219
+ end
220
+
221
+ # Extract the name and value from an `:op_asgn` (compound assignment) node.
222
+ #
223
+ # @note module_function: defines #compound_name_and_value (visibility: private)
224
+ # @param [Parser::AST::Node] node the `:op_asgn` AST node
225
+ # @return [(String, nil, Parser::AST::Node, nil)]
226
+ def compound_name_and_value(node)
227
+ [node.children[0].children.first.to_s, node.children[2]]
228
+ end
229
+
230
+ # Handle `:lvar` node for last_expr_type — look up the variable in local_var_types.
231
+ #
232
+ # @note module_function: defines #handle_lvar_node (visibility: private)
233
+ # @param [Parser::AST::Node] node the `:lvar` AST node
234
+ # @param [Object] opts additional keyword options forwarded to type inference
235
+ # @return [String, nil]
236
+ def handle_lvar_node(node, **opts)
237
+ name = node.children[0].to_s
238
+ opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
239
+ end
240
+
241
+ # Handle `:ivar` node for last_expr_type — look up instance variable in local_var_types.
242
+ #
243
+ # @note module_function: defines #handle_ivar_node (visibility: private)
244
+ # @param [Parser::AST::Node] node the `:ivar` AST node
245
+ # @param [Object] opts additional keyword options forwarded to type inference
246
+ # @return [String, nil]
247
+ def handle_ivar_node(node, **opts)
248
+ name = node.children[0].to_s
249
+ opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
250
+ end
251
+
252
+ # Handle `:gvar` node for last_expr_type — look up global variable in local_var_types.
253
+ #
254
+ # @note module_function: defines #handle_gvar_node (visibility: private)
255
+ # @param [Parser::AST::Node] node the `:gvar` AST node
256
+ # @param [Object] opts additional keyword options forwarded to type inference
257
+ # @return [String, nil]
258
+ def handle_gvar_node(node, **opts)
259
+ name = node.children[0].to_s
260
+ opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
261
+ end
262
+
263
+ # Handle `:cvar` node for last_expr_type — look up class variable in local_var_types.
264
+ #
265
+ # @note module_function: defines #handle_cvar_node (visibility: private)
266
+ # @param [Parser::AST::Node] node the `:cvar` AST node
267
+ # @param [Object] opts additional keyword options forwarded to type inference
268
+ # @return [String, nil]
269
+ def handle_cvar_node(node, **opts)
270
+ name = node.children[0].to_s
271
+ opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
272
+ end
273
+
274
+ # Handle `:lvasgn` node for last_expr_type — look up local var assignment in local_var_types.
275
+ #
276
+ # @note module_function: defines #handle_lvasgn_node (visibility: private)
277
+ # @param [Parser::AST::Node] node the `:lvasgn` AST node
278
+ # @param [Object] opts additional keyword options forwarded to type inference
279
+ # @return [String, nil]
280
+ def handle_lvasgn_node(node, **opts)
281
+ name = node.children[0].to_s
282
+ opts[:local_var_types]&.fetch(name, nil) ||
283
+ run_last_expr_type(node.children[1], **opts) ||
284
+ opts[:fallback_type]
285
+ end
286
+
287
+ # Handle `:ivasgn` node for last_expr_type — look up ivar assignment in local_var_types.
288
+ #
289
+ # @note module_function: defines #handle_ivasgn_node (visibility: private)
290
+ # @param [Parser::AST::Node] node the `:ivasgn` AST node
291
+ # @param [Object] opts additional keyword options forwarded to type inference
292
+ # @return [String, nil]
293
+ def handle_ivasgn_node(node, **opts)
294
+ name = node.children[0].to_s
295
+ opts[:local_var_types]&.fetch(name, nil) ||
296
+ run_last_expr_type(node.children[1], **opts) ||
297
+ opts[:fallback_type]
298
+ end
299
+
300
+ # Handle `:gvasgn` node for last_expr_type — look up global var assignment in local_var_types.
301
+ #
302
+ # @note module_function: defines #handle_gvasgn_node (visibility: private)
303
+ # @param [Parser::AST::Node] node the `:gvasgn` AST node
304
+ # @param [Object] opts additional keyword options forwarded to type inference
305
+ # @return [String, nil]
306
+ def handle_gvasgn_node(node, **opts)
307
+ name = node.children[0].to_s
308
+ opts[:local_var_types]&.fetch(name, nil) ||
309
+ run_last_expr_type(node.children[1], **opts) ||
310
+ opts[:fallback_type]
311
+ end
312
+
313
+ # Handle `:cvasgn` node for last_expr_type — look up class var assignment in local_var_types.
314
+ #
315
+ # @note module_function: defines #handle_cvasgn_node (visibility: private)
316
+ # @param [Parser::AST::Node] node the `:cvasgn` AST node
317
+ # @param [Object] opts additional keyword options forwarded to type inference
318
+ # @return [String, nil]
319
+ def handle_cvasgn_node(node, **opts)
320
+ name = node.children[0].to_s
321
+ opts[:local_var_types]&.fetch(name, nil) ||
322
+ run_last_expr_type(node.children[1], **opts) ||
323
+ opts[:fallback_type]
324
+ end
325
+
326
+ # Handle `:op_asgn` node (compound assignment: `x += 1`, `@var -= 2`, etc.).
327
+ #
328
+ # Infers the result type from the operator and the right operand's type.
329
+ # Uses RBS to resolve when available (e.g., `Integer#+` -> `Integer`).
330
+ #
331
+ # @note module_function: defines #handle_op_asgn_node (visibility: private)
332
+ # @param [Parser::AST::Node] node the `:op_asgn` AST node
333
+ # @param [Object] opts additional keyword options forwarded to type inference
334
+ # @return [String, nil]
335
+ def handle_op_asgn_node(node, **opts)
336
+ meth = node.children[1]
337
+ return nil unless %i[+ - * / % ** << | & ^].include?(meth)
338
+ return nil unless opts[:core_rbs_provider]
339
+
340
+ arg = node.children[2]
341
+ arg_type = type_from_literal_safe(arg)
342
+ return nil unless arg_type
343
+
344
+ rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
345
+ rbs unless rbs == FALLBACK_TYPE
346
+ end
347
+
214
348
  # Handle `:begin` node for last_expr_type.
215
349
  #
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]
350
+ # @note module_function: defines #handle_begin_node (visibility: private)
351
+ # @param [Parser::AST::Node] node the `:return` AST node
352
+ # @param [Object] opts additional keyword options forwarded to type inference
353
+ # @return [String, nil]
220
354
  def handle_begin_node(node, **opts)
221
355
  run_last_expr_type(node.children.last, **opts)
222
356
  end
223
357
 
224
358
  # Handle `:if` node for last_expr_type.
225
359
  #
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]
360
+ # @note module_function: defines #handle_if_node (visibility: private)
361
+ # @param [Parser::AST::Node] node the `:return` AST node
362
+ # @param [Object] opts additional keyword options forwarded to type inference
363
+ # @return [String, nil]
230
364
  def handle_if_node(node, **opts)
231
365
  t = run_last_expr_type(node.children[1], **opts)
232
- e = run_last_expr_type(node.children[2], **opts)
366
+ e = if node.children[2]
367
+ run_last_expr_type(node.children[2], **opts)
368
+ else
369
+ 'nil'
370
+ end
233
371
  unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
234
372
  nil_as_optional: opts.fetch(:nil_as_optional, true))
235
373
  end
236
374
 
237
375
  # Handle `:case` node for last_expr_type.
238
376
  #
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]
377
+ # @note module_function: defines #handle_case_node (visibility: private)
378
+ # @param [Parser::AST::Node] node the `:return` AST node
379
+ # @param [Object] opts additional keyword options forwarded to type inference
380
+ # @return [String, nil]
243
381
  def handle_case_node(node, **opts)
244
382
  branches = process_case_branches(node, **opts)
245
383
  if branches.empty?
@@ -252,11 +390,204 @@ module Docscribe
252
390
  end
253
391
  end
254
392
 
393
+ # Handle `:or` node (`a || b`) for last_expr_type.
394
+ #
395
+ # The result type is the union of both sides, since either may be returned
396
+ # depending on the truthiness of the left operand.
397
+ #
398
+ # @note module_function: defines #handle_or_node (visibility: private)
399
+ # @param [Parser::AST::Node] node the `:or` AST node
400
+ # @param [Object] opts additional keyword options forwarded to type inference
401
+ # @return [String, nil]
402
+ def handle_or_node(node, **opts)
403
+ t = run_last_expr_type(node.children[0], **opts)
404
+ e = run_last_expr_type(node.children[1], **opts)
405
+ unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
406
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
407
+ end
408
+
409
+ # Handle `:and` node (`a && b`) for last_expr_type.
410
+ #
411
+ # The result type is the union of both sides, since either may be returned
412
+ # depending on the truthiness of the left operand.
413
+ #
414
+ # @note module_function: defines #handle_and_node (visibility: private)
415
+ # @param [Parser::AST::Node] node the `:and` AST node
416
+ # @param [Object] opts additional keyword options forwarded to type inference
417
+ # @return [String, nil]
418
+ def handle_and_node(node, **opts)
419
+ t = run_last_expr_type(node.children[0], **opts)
420
+ e = run_last_expr_type(node.children[1], **opts)
421
+ unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
422
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
423
+ end
424
+
425
+ # Handle `:kwbegin` node (`begin; expr; end`) for last_expr_type.
426
+ #
427
+ # Unwraps the explicit begin node and delegates to the inner expression,
428
+ # which may be a `:rescue` or `:ensure` node.
429
+ #
430
+ # @note module_function: defines #handle_kwbegin_node (visibility: private)
431
+ # @param [Parser::AST::Node] node the `:kwbegin` AST node
432
+ # @param [Object] opts additional keyword options forwarded to type inference
433
+ # @return [String, nil]
434
+ def handle_kwbegin_node(node, **opts)
435
+ run_last_expr_type(node.children.first, **opts)
436
+ end
437
+
438
+ # Handle `:rescue` node for last_expr_type.
439
+ #
440
+ # Supports both inline rescue (`expr rescue default`) and block rescue
441
+ # (`begin; expr; rescue; e; end`).
442
+ #
443
+ # @note module_function: defines #handle_rescue_node (visibility: private)
444
+ # @param [Parser::AST::Node] node the `:rescue` AST node
445
+ # @param [Object] opts additional keyword options forwarded to type inference
446
+ # @return [String, nil]
447
+ def handle_rescue_node(node, **opts)
448
+ branches = collect_rescue_branches(node, **opts)
449
+ branches.reduce do |a, b|
450
+ unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
451
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
452
+ end
453
+ end
454
+
455
+ # Handle `:rescue` node for last_expr_type.
456
+ #
457
+ # Unifies the body type with all rescue handler types and the optional else clause.
458
+ # Collect all rescue branch return types from a `:rescue` AST node.
459
+ #
460
+ # @note module_function: defines #collect_rescue_branches (visibility: private)
461
+ # @param [Parser::AST::Node] node the `:rescue` AST node
462
+ # @param [Object] opts additional keyword options forwarded to type inference
463
+ # @return [Array<String, nil>]
464
+ def collect_rescue_branches(node, **opts)
465
+ branches = [run_last_expr_type(node.children[0], **opts)]
466
+ (node.children[1..] || []).each do |child|
467
+ if child.is_a?(Parser::AST::Node) && child.type == :resbody
468
+ handler = child.children[2]
469
+ branches << run_last_expr_type(handler, **opts) if handler
470
+ else
471
+ branches << run_last_expr_type(child, **opts)
472
+ end
473
+ end
474
+ branches
475
+ end
476
+
477
+ # Handle `:ensure` node (`begin; expr; ensure; cleanup; end`) for last_expr_type.
478
+ #
479
+ # The ensure clause's result is discarded by Ruby; only the body type is returned.
480
+ #
481
+ # @note module_function: defines #handle_ensure_node (visibility: private)
482
+ # @param [Parser::AST::Node] node the `:ensure` AST node
483
+ # @param [Object] opts additional keyword options forwarded to type inference
484
+ # @return [String, nil]
485
+ def handle_ensure_node(node, **opts)
486
+ run_last_expr_type(node.children[0], **opts)
487
+ end
488
+
489
+ # Handle `:defined?` node (`defined?(expr)`) for last_expr_type.
490
+ #
491
+ # Returns `nil` if the expression is not defined, or a String description
492
+ # if it is defined. The union type is `String?`.
493
+ #
494
+ # @note module_function: defines #handle_defined_node (visibility: private)
495
+ # @param [Parser::AST::Node] _node the `:defined?` AST node
496
+ # @param [Object] opts additional keyword options forwarded to type inference
497
+ # @return [String, nil]
498
+ def handle_defined_node(_node, **opts)
499
+ nil_as_optional = opts.fetch(:nil_as_optional, true)
500
+ nil_as_optional ? 'String?' : 'String, nil'
501
+ end
502
+
503
+ # Handle `:zsuper` node (`super` with no arguments) for last_expr_type.
504
+ #
505
+ # Returns the super method's return type if resolvable via RBS, or the
506
+ # fallback type otherwise.
507
+ #
508
+ # @note module_function: defines #handle_zsuper_node (visibility: private)
509
+ # @param [Parser::AST::Node] _node the `:zsuper` AST node
510
+ # @param [Object] opts additional keyword options forwarded to type inference
511
+ # @return [String, nil]
512
+ def handle_zsuper_node(_node, **opts)
513
+ opts[:fallback_type]
514
+ end
515
+
516
+ # Handle `:super` node (`super(args)`) for last_expr_type.
517
+ #
518
+ # Returns the super method's return type if resolvable via RBS, or the
519
+ # fallback type otherwise.
520
+ #
521
+ # @note module_function: defines #handle_super_node (visibility: private)
522
+ # @param [Parser::AST::Node] _node the `:super` AST node
523
+ # @param [Object] opts additional keyword options forwarded to type inference
524
+ # @return [String, nil]
525
+ def handle_super_node(_node, **opts)
526
+ opts[:fallback_type]
527
+ end
528
+
529
+ # Handle `:yield` node (`yield` / `yield(args)`) for last_expr_type.
530
+ #
531
+ # Returns the block's return type if resolvable via RBS (`Proc#call`),
532
+ # or the fallback type otherwise.
533
+ #
534
+ # @note module_function: defines #handle_yield_node (visibility: private)
535
+ # @param [Parser::AST::Node] _node the `:yield` AST node
536
+ # @param [Object] opts additional keyword options forwarded to type inference
537
+ # @return [String, nil]
538
+ def handle_yield_node(_node, **opts)
539
+ opts[:fallback_type]
540
+ end
541
+
542
+ # Handle `:case_match` node (`case x; in pat; expr; end`) for last_expr_type.
543
+ #
544
+ # Similar to `:case` — unifies all `in_pattern` branch types and the optional else clause.
545
+ #
546
+ # @note module_function: defines #handle_case_match_node (visibility: private)
547
+ # @param [Parser::AST::Node] node the `:case_match` AST node
548
+ # @param [Object] opts additional keyword options forwarded to type inference
549
+ # @return [String, nil]
550
+ def handle_case_match_node(node, **opts)
551
+ branches = process_pattern_branches(node, **opts)
552
+ if branches.empty?
553
+ opts[:fallback_type]
554
+ else
555
+ branches.reduce do |a, b|
556
+ unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
557
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
558
+ end
559
+ end
560
+ end
561
+
562
+ # Handle `:in_pattern` node (pattern inside `case...in`) for last_expr_type.
563
+ #
564
+ # Extracts the body expression from the pattern and recurses.
565
+ #
566
+ # @note module_function: defines #handle_in_pattern_node (visibility: private)
567
+ # @param [Parser::AST::Node] node the `:in_pattern` AST node
568
+ # @param [Object] opts additional keyword options forwarded to type inference
569
+ # @return [String, nil]
570
+ def handle_in_pattern_node(node, **opts)
571
+ run_last_expr_type(node.children[2], **opts)
572
+ end
573
+
574
+ # Extract inferred return types from all in_pattern branches of a :case_match expression.
575
+ #
576
+ # @note module_function: defines #process_pattern_branches (visibility: private)
577
+ # @param [Parser::AST::Node] node the :case_match AST node
578
+ # @param [Object] opts additional keyword options forwarded to type inference
579
+ # @return [Array<String>] list of inferred types from each branch
580
+ def process_pattern_branches(node, **opts)
581
+ (node.children[1..] || []).compact.filter_map do |child|
582
+ run_last_expr_type(child, **opts) if child.is_a?(Parser::AST::Node)
583
+ end
584
+ end
585
+
255
586
  # Extract inferred return types from all branches of a :case expression.
256
587
  #
257
- # @note module_function: when included, also defines #process_case_branches (instance visibility: private)
588
+ # @note module_function: defines #process_case_branches (visibility: private)
258
589
  # @param [Parser::AST::Node] node the :case AST node
259
- # @param [Hash] opts additional keyword options forwarded to type inference
590
+ # @param [Object] opts additional keyword options forwarded to type inference
260
591
  # @return [Array<String>] list of inferred types from each branch
261
592
  def process_case_branches(node, **opts)
262
593
  (node.children[1..] || []).compact.flat_map do |child|
@@ -270,10 +601,10 @@ module Docscribe
270
601
 
271
602
  # Handle `:block` node for last_expr_type.
272
603
  #
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]
604
+ # @note module_function: defines #handle_block_node (visibility: private)
605
+ # @param [Parser::AST::Node] node the `:return` AST node
606
+ # @param [Object] opts additional keyword options forwarded to type inference
607
+ # @return [String, nil]
277
608
  def handle_block_node(node, **opts)
278
609
  send_node = node.children[0]
279
610
  if send_node&.type == :send
@@ -289,10 +620,10 @@ module Docscribe
289
620
 
290
621
  # Handle `:send` node for last_expr_type.
291
622
  #
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]
623
+ # @note module_function: defines #handle_send_node (visibility: private)
624
+ # @param [Parser::AST::Node] node the `:return` AST node
625
+ # @param [Object] opts additional keyword options forwarded to type inference
626
+ # @return [String, nil]
296
627
  def handle_send_node(node, **opts)
297
628
  recv = node.children[0]
298
629
  meth = node.children[1]
@@ -303,39 +634,118 @@ module Docscribe
303
634
  return rbs_type if rbs_type
304
635
  end
305
636
 
637
+ compound_type = infer_from_compound_assign(node, **opts)
638
+ return compound_type if compound_type
639
+
306
640
  Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
307
641
  end
308
642
 
309
643
  # Resolve RBS return type for a send node's receiver, if possible.
310
644
  #
311
- # Handles `:lvar` and chained `:send` receivers.
645
+ # Handles `:lvar`, chained `:send`, literal (`:int`, `:str`, etc.),
646
+ # and variable (`:ivar`, `:gvar`, `:cvar`) receivers.
312
647
  #
313
- # @note module_function: when included, also defines #resolve_rbs_for_send (instance visibility: private)
648
+ # @note module_function: defines #resolve_rbs_for_send (visibility: private)
314
649
  # @param [Parser::AST::Node, nil] recv the receiver node of the send
315
650
  # @param [Symbol] meth the method name being called
316
651
  # @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
652
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
653
+ # @param [Hash<String, String>, nil] param_types parameter name to type map
319
654
  # @return [String, nil] resolved type or nil if unresolvable
320
655
  def resolve_rbs_for_send(recv, meth, core_rbs_provider, local_var_types, param_types)
321
656
  return nil unless core_rbs_provider
322
657
 
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
658
+ recv_type = receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
659
+ return nil unless recv_type
660
+
661
+ rbs = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
662
+ rbs unless rbs == FALLBACK_TYPE
663
+ end
664
+
665
+ # Map a receiver AST node to its RBS type name string.
666
+ #
667
+ # Supports local variables, method calls, literals, and instance/global/class variables.
668
+ #
669
+ # @note module_function: when included, also defines #receiver_rbs_type_name (instance visibility: private)
670
+ # @param [Parser::AST::Node, nil] recv the receiver node
671
+ # @param [Object, nil] core_rbs_provider core RBS provider
672
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable types
673
+ # @param [Hash<String, String>, nil] param_types parameter name to type map
674
+ # @return [String, nil]
675
+ LITERAL_RBS_TYPES = {
676
+ int: 'Integer', str: 'String', sym: 'Symbol', true: 'Boolean',
677
+ false: 'Boolean', float: 'Float', array: 'Array', hash: 'Hash',
678
+ nil: 'NilClass'
679
+ }.freeze
680
+
681
+ # Map receiver AST node to RBS type name.
682
+ #
683
+ # @note module_function: defines #receiver_rbs_type_name (visibility: private)
684
+ # @param [Parser::AST::Node, nil] recv the receiver AST node
685
+ # @param [Object, nil] core_rbs_provider core RBS type provider
686
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable types
687
+ # @param [Hash<String, String>, nil] param_types parameter name-to-type map
688
+ # @return [String, nil]
689
+ def receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
690
+ return unless recv
691
+ return LITERAL_RBS_TYPES[recv.type] if LITERAL_RBS_TYPES.key?(recv.type)
692
+ return lookup_lvar_type(recv.children.first, local_var_types, param_types) if %i[lvar ivar gvar
693
+ cvar].include?(recv.type)
694
+ return unless recv.type == :send
695
+
696
+ run_last_expr_type(recv, fallback_type: FALLBACK_TYPE, nil_as_optional: false,
697
+ core_rbs_provider: core_rbs_provider,
698
+ param_types: param_types,
699
+ local_var_types: local_var_types)
700
+ end
701
+
702
+ # Infer return type from a compound-assignment-like `:send` by reading the
703
+ # first literal argument's type — only fires when `core_rbs_provider` is
704
+ # present and the argument's RBS return type can be resolved.
705
+ #
706
+ # Enables `@var += 123` -> `Integer` (via `Integer#+`) and similar patterns.
707
+ #
708
+ # @note module_function: defines #infer_from_compound_assign (visibility: private)
709
+ # @param [Parser::AST::Node] node the `:send` AST node
710
+ # @param [Object] opts additional keyword options forwarded to type inference
711
+ # @return [String, nil]
712
+ def infer_from_compound_assign(node, **opts)
713
+ return nil unless opts[:core_rbs_provider]
714
+
715
+ meth = node.children[1]
716
+ return nil unless %i[+ - * / % ** << | & ^].include?(meth)
717
+
718
+ first_arg = node.children[2]
719
+ return nil unless first_arg
720
+
721
+ arg_type = type_from_literal_safe(first_arg)
722
+ return nil unless arg_type
723
+
724
+ rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
725
+ rbs unless rbs == FALLBACK_TYPE
726
+ end
727
+
728
+ # Safely get a type string from a literal node, returning nil if the node
729
+ # is not a literal or yields no type.
730
+ #
731
+ # @note module_function: defines #type_from_literal_safe (visibility: private)
732
+ # @param [Parser::AST::Node, nil] node literal AST node
733
+ # @return [String, nil]
734
+ def type_from_literal_safe(node)
735
+ return nil unless node
736
+
737
+ t = Literals.type_from_literal(node, fallback_type: FALLBACK_TYPE)
738
+ t unless t == FALLBACK_TYPE
328
739
  end
329
740
 
330
741
  # Resolve RBS return type for an `:lvar` receiver.
331
742
  #
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
743
+ # @note module_function: defines #resolve_lvar_rbs (visibility: private)
744
+ # @param [Parser::AST::Node?] recv the receiver node of the send
745
+ # @param [Symbol] meth the method name being called
746
+ # @param [Object, nil] core_rbs_provider core RBS type lookup provider
747
+ # @param [Hash<Object, Object>, nil] local_var_types pre-built local variable types map
748
+ # @param [Hash<String, String>, nil] param_types parameter name -> type map for lvar resolution
339
749
  # @return [String, nil]
340
750
  def resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
341
751
  lvar_name = recv&.children&.first
@@ -348,10 +758,10 @@ module Docscribe
348
758
 
349
759
  # Look up a local variable's inferred type from local or parameter type maps.
350
760
  #
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
761
+ # @note module_function: defines #lookup_lvar_type (visibility: private)
762
+ # @param [Object] lvar_name the local variable name
763
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
764
+ # @param [Hash<String, String>, nil] param_types parameter name to type map
355
765
  # @return [String, nil]
356
766
  def lookup_lvar_type(lvar_name, local_var_types, param_types)
357
767
  return local_var_types[lvar_name.to_s] if local_var_types&.key?(lvar_name.to_s)
@@ -362,13 +772,12 @@ module Docscribe
362
772
 
363
773
  # Resolve RBS return type for a chained `:send` receiver.
364
774
  #
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
775
+ # @note module_function: defines #resolve_chained_send_rbs (visibility: private)
776
+ # @param [Parser::AST::Node?] recv the receiver node of the send
777
+ # @param [Symbol] meth the method name being called
778
+ # @param [Object, nil] core_rbs_provider core RBS type lookup provider
779
+ # @param [Hash<Object, Object>, nil] local_var_types pre-built local variable types map
780
+ # @param [Hash<String, String>, nil] param_types parameter name -> type map for lvar resolution
372
781
  # @return [String, nil]
373
782
  def resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
374
783
  inner_type = run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
@@ -390,14 +799,9 @@ module Docscribe
390
799
  # - literal-like expressions via {Literals.type_from_literal}
391
800
  # - method calls with RBS core type lookup
392
801
  #
393
- # @note module_function: when included, also defines #last_expr_type (instance visibility: private)
802
+ # @note module_function: defines #last_expr_type (visibility: private)
394
803
  # @param [Parser::AST::Node, nil] node expression node
395
- # @param [String] fallback_type type used when inference is uncertain
396
- # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
397
- # @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
398
- # @param [Hash, nil] param_types parameter name -> type map for lvar resolution
399
- # @param [nil] local_var_types pre-built local variable types map
400
- # @param [Hash] opts additional keyword options forwarded to type inference
804
+ # @param [Object] opts additional keyword options forwarded to type inference
401
805
  # @return [String, nil]
402
806
  def last_expr_type(node, **opts)
403
807
  run_last_expr_type(node, **opts)
@@ -405,16 +809,17 @@ module Docscribe
405
809
 
406
810
  # Dispatch `last_expr_type` based on node type.
407
811
  #
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
812
+ # @note module_function: defines #run_last_expr_type (visibility: private)
813
+ # @param [Parser::AST::Node, nil] node the `:return` AST node
814
+ # @param [Object] opts options passed through as keyword args
411
815
  # @return [String, nil]
412
816
  def run_last_expr_type(node, **opts)
413
817
  return unless node
414
818
 
415
- handler = LAST_EXPR_TYPE_HANDLERS[node.type]
416
- if handler
417
- send(handler, node, **opts)
819
+ type = node.type == :defined? ? :defined : node.type
820
+ method_name = :"handle_#{type}_node"
821
+ if respond_to?(method_name, true)
822
+ send(method_name, node, **opts)
418
823
  else
419
824
  Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
420
825
  end
@@ -422,9 +827,9 @@ module Docscribe
422
827
 
423
828
  # Extract the return type from an explicit `:return` node.
424
829
  #
425
- # @note module_function: when included, also defines #handle_return_node (instance visibility: private)
830
+ # @note module_function: defines #handle_return_node (visibility: private)
426
831
  # @param [Parser::AST::Node] node the `:return` AST node
427
- # @param [Hash] opts additional keyword options forwarded to type inference
832
+ # @param [Object] opts additional keyword options forwarded to type inference
428
833
  # @return [String, nil]
429
834
  def handle_return_node(node, **opts)
430
835
  Literals.type_from_literal(node.children.first, fallback_type: opts[:fallback_type])
@@ -432,10 +837,10 @@ module Docscribe
432
837
 
433
838
  # Resolve an RBS return type for a method call.
434
839
  #
435
- # @note module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
840
+ # @note module_function: defines #resolve_rbs_return_type (visibility: private)
436
841
  # @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
842
+ # @param [String, Symbol] method_name method name
843
+ # @param [Object, nil] core_rbs_provider core RBS type lookup provider
439
844
  # @return [String] inferred return type
440
845
  def resolve_rbs_return_type(container_type, method_name, core_rbs_provider)
441
846
  return FALLBACK_TYPE unless core_rbs_provider
@@ -456,37 +861,34 @@ module Docscribe
456
861
  # - `nil` unions may become optional types if enabled
457
862
  # - otherwise falls back conservatively to `fallback_type`
458
863
  #
459
- # @note module_function: when included, also defines #unify_types (instance visibility: private)
460
- # @param [String, nil] a
461
- # @param [String, nil] b
462
- # @param [String] fallback_type
463
- # @param [Boolean] nil_as_optional
864
+ # @note module_function: defines #unify_types (visibility: private)
464
865
  # @param [String, nil] type_a first type to unify
465
866
  # @param [String, nil] type_b second type to unify
466
- # @return [String, nil]
867
+ # @param [String] fallback_type type used when neither is nil
868
+ # @param [Boolean] nil_as_optional whether to render nil unions as optional types
869
+ # @return [String]
467
870
  def unify_types(type_a, type_b, fallback_type:, nil_as_optional:)
468
871
  type_a ||= fallback_type
469
872
  type_b ||= fallback_type
470
873
  return type_a if type_a == type_b
471
874
 
472
- unify_nil_types(type_a, type_b, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
875
+ unify_nil_types(type_a, type_b, nil_as_optional: nil_as_optional)
473
876
  end
474
877
 
475
878
  # Unify two types where one may be `nil`, producing optional or union type.
476
879
  #
477
- # @note module_function: when included, also defines #unify_nil_types (instance visibility: private)
880
+ # @note module_function: defines #unify_nil_types (visibility: private)
478
881
  # @param [String] type_a first type string
479
882
  # @param [String] type_b second type string
480
- # @param [String] fallback_type type used when neither is nil
481
883
  # @param [Boolean] nil_as_optional whether to render nil unions as optional types
482
884
  # @return [String]
483
- def unify_nil_types(type_a, type_b, fallback_type:, nil_as_optional:)
885
+ def unify_nil_types(type_a, type_b, nil_as_optional:)
484
886
  if type_a == 'nil' || type_b == 'nil'
485
887
  non_nil = (type_a == 'nil' ? type_b : type_a)
486
888
  return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil"
487
889
  end
488
890
 
489
- fallback_type
891
+ "#{type_a}, #{type_b}"
490
892
  end
491
893
  end
492
894
  end