docscribe 1.6.1 → 1.6.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -193
  3. data/exe/docscribe-client +26 -7
  4. data/lib/docscribe/cli/config_builder.rb +37 -2
  5. data/lib/docscribe/cli/coverage.rb +5 -5
  6. data/lib/docscribe/cli/formatters/json.rb +74 -29
  7. data/lib/docscribe/cli/formatters/sarif.rb +20 -3
  8. data/lib/docscribe/cli/options.rb +17 -2
  9. data/lib/docscribe/cli/rbs_gen.rb +4 -4
  10. data/lib/docscribe/cli/run.rb +107 -24
  11. data/lib/docscribe/cli/update_types.rb +61 -17
  12. data/lib/docscribe/cli.rb +19 -13
  13. data/lib/docscribe/config/defaults.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +22 -1
  15. data/lib/docscribe/config/template.rb +3 -0
  16. data/lib/docscribe/config/validation.rb +19 -0
  17. data/lib/docscribe/config.rb +1 -0
  18. data/lib/docscribe/infer/behavior.rb +13 -13
  19. data/lib/docscribe/infer/params.rb +2 -2
  20. data/lib/docscribe/infer/raises.rb +5 -6
  21. data/lib/docscribe/infer/returns.rb +1612 -151
  22. data/lib/docscribe/infer.rb +7 -7
  23. data/lib/docscribe/inline_rewriter/doc_builder.rb +485 -102
  24. data/lib/docscribe/inline_rewriter.rb +263 -97
  25. data/lib/docscribe/plugin/registry.rb +1 -0
  26. data/lib/docscribe/server/base.rb +46 -15
  27. data/lib/docscribe/server/client.rb +20 -11
  28. data/lib/docscribe/server/daemon.rb +200 -23
  29. data/lib/docscribe/server/protocol.rb +4 -4
  30. data/lib/docscribe/types/primitive.rb +160 -0
  31. data/lib/docscribe/types/sorbet/base_provider.rb +33 -1
  32. data/lib/docscribe/types/yard/formatter.rb +35 -6
  33. data/lib/docscribe/types/yard/parser.rb +25 -20
  34. data/lib/docscribe/types/yard/validator.rb +131 -0
  35. data/lib/docscribe/validator/generic_compatibility.rb +698 -0
  36. data/lib/docscribe/validator/type_mismatch_validator.rb +287 -0
  37. data/lib/docscribe/version.rb +1 -1
  38. metadata +8 -3
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../types/primitive'
4
+
3
5
  module Docscribe
4
6
  module Infer
5
7
  # Return type inference and rescue-conditional return extraction.
@@ -15,7 +17,7 @@ module Docscribe
15
17
  # @param [String?] method_source full method definition source
16
18
  # @raise [Parser::SyntaxError]
17
19
  # @return [String]
18
- # @return [FALLBACK_TYPE] if Parser::SyntaxError
20
+ # @return [String] if Parser::SyntaxError
19
21
  def infer_return_type(method_source)
20
22
  return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty?
21
23
 
@@ -65,46 +67,58 @@ module Docscribe
65
67
  # @param [Parser::AST::Node] node `:def` or `:defs` node
66
68
  # @param [String] fallback_type type used when inference is uncertain
67
69
  # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
68
- # @param [Object?] core_rbs_provider core RBS type lookup provider
69
- # @param [Hash<String, String>?] param_types parameter name -> type map
70
- # @param [String?] container
71
- # @param [Docscribe::Types::ProviderChain?] signature_provider
72
- # @return [Object]
73
- def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, # rubocop:disable Metrics/ParameterLists
74
- param_types: nil, container: nil, signature_provider: nil)
70
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider core RBS type lookup provider
71
+ # @param [Hash] opts
72
+ # @return [Hash<Symbol, Object>]
73
+ def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
74
+ core_rbs_provider: nil, **opts)
75
75
  body = extract_def_body(node)
76
76
  spec = { normal: FALLBACK_TYPE, rescues: [] } #: Hash[Symbol, untyped]
77
77
  return spec unless body
78
78
 
79
- types = build_local_variable_types(body, core_rbs_provider: core_rbs_provider, param_types: param_types)
80
- populate_returns_spec(spec, body, types, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
81
- core_rbs_provider: core_rbs_provider, param_types: param_types,
82
- container: container, signature_provider: signature_provider)
79
+ populate_spec_with_types(spec, body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
80
+ core_rbs_provider: core_rbs_provider, **opts)
83
81
  spec
84
82
  end
85
83
 
84
+ # @note module_function: defines #populate_spec_with_types (visibility: private)
85
+ # @param [Hash<Symbol, Object>] spec
86
+ # @param [Parser::AST::Node] body
87
+ # @param [String?] fallback_type
88
+ # @param [Boolean] nil_as_optional
89
+ # @param [Hash] opts
90
+ # @return [void]
91
+ def populate_spec_with_types(spec, body, fallback_type: FALLBACK_TYPE, nil_as_optional: true, **opts)
92
+ core_rbs_provider = opts[:core_rbs_provider]
93
+ types = build_local_variable_types(body, core_rbs_provider: core_rbs_provider,
94
+ param_types: opts[:param_types])
95
+ populate_returns_spec(spec, body, types, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
96
+ core_rbs_provider: core_rbs_provider, param_types: opts[:param_types],
97
+ container: opts[:container], signature_provider: opts[:signature_provider])
98
+ end
99
+
86
100
  # Extract the body child node from a `:def` or `:defs` AST node.
87
101
  #
88
102
  # @note module_function: defines #extract_def_body (visibility: private)
89
103
  # @param [Parser::AST::Node] node a `:def` or `:defs` AST node
90
104
  # @return [Parser::AST::Node, nil]
91
105
  def extract_def_body(node)
92
- case node.type
93
- when :def then node.children[2]
94
- when :defs then node.children[3]
106
+ case node&.type
107
+ when :def then node&.children&.[](2)
108
+ when :defs then node&.children&.[](3)
95
109
  end
96
110
  end
97
111
 
98
112
  # Populate the spec hash with normal and/or rescue return types from the body.
99
113
  #
100
114
  # @note module_function: defines #populate_returns_spec (visibility: private)
101
- # @param [Object] spec the return spec hash to populate
115
+ # @param [Hash<Symbol, Object>] spec the return spec hash to populate
102
116
  # @param [Parser::AST::Node] body the method body AST node
103
- # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
104
- # @param [Object] opts additional keyword options forwarded to type inference
105
- # @return [Object]
117
+ # @param [Hash<String, String>?] local_var_types inferred local variable type map
118
+ # @param [Hash] opts additional keyword options forwarded to type inference
119
+ # @return [void]
106
120
  def populate_returns_spec(spec, body, local_var_types, **opts)
107
- if body.type == :rescue
121
+ if body&.type == :rescue
108
122
  process_rescue_body(spec, body, **opts)
109
123
  else
110
124
  spec[:normal] = infer_normal_return_type(body, **opts, local_var_types: local_var_types)
@@ -115,7 +129,7 @@ module Docscribe
115
129
  #
116
130
  # @note module_function: defines #infer_normal_return_type (visibility: private)
117
131
  # @param [Parser::AST::Node] body the method body AST node
118
- # @param [Object] opts additional keyword options forwarded to type inference
132
+ # @param [Hash] opts additional keyword options forwarded to type inference
119
133
  # @return [String]
120
134
  def infer_normal_return_type(body, **opts)
121
135
  run_last_expr_type(body, **opts) || FALLBACK_TYPE
@@ -124,10 +138,10 @@ module Docscribe
124
138
  # Process a :rescue body node and populate spec with normal + rescue return types.
125
139
  #
126
140
  # @note module_function: defines #process_rescue_body (visibility: private)
127
- # @param [Object] spec the return spec hash to populate
141
+ # @param [Hash<Symbol, Object>] spec the return spec hash to populate
128
142
  # @param [Parser::AST::Node] body the :rescue AST node
129
- # @param [Object] opts additional keyword options forwarded to type inference
130
- # @return [Object]
143
+ # @param [Hash] opts additional keyword options forwarded to type inference
144
+ # @return [void]
131
145
  def process_rescue_body(spec, body, **opts)
132
146
  main_body = body.children[0]
133
147
  local_var_types = build_local_variable_types(body,
@@ -141,26 +155,46 @@ module Docscribe
141
155
  # Extract return types from each :resbody child and append to spec[:rescues].
142
156
  #
143
157
  # @note module_function: defines #process_rescue_branches (visibility: private)
144
- # @param [Object] spec the return spec hash to populate
158
+ # @param [Hash<Symbol, Object>] spec the return spec hash to populate
145
159
  # @param [Parser::AST::Node] body the :rescue AST node
146
- # @param [Object] opts additional keyword options forwarded to type inference
147
- # @return [Array<Object>] the list of rescue type entries
160
+ # @param [Hash] opts additional keyword options forwarded to type inference
161
+ # @return [void]
148
162
  def process_rescue_branches(spec, body, **opts)
149
163
  body.children.each do |ch|
150
164
  next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
151
165
 
152
166
  exc_list, _asgn, rescue_body = *ch
153
167
  exc_names = Raises.exception_names_from_rescue_list(exc_list)
154
- rtype = run_last_expr_type(rescue_body, **opts) || opts[:fallback_type]
168
+ rtype = rescue_branch_type(rescue_body, **opts) || opts[:fallback_type]
155
169
  spec[:rescues] << [exc_names, rtype]
156
170
  end
157
171
  end
158
172
 
173
+ # Infer a rescue branch type, resolving data constants precisely.
174
+ #
175
+ # Rescue branches form the documented contract (`@return [X] if Error`),
176
+ # so a bare data constant (e.g., FALLBACK_TYPE, whose runtime value is
177
+ # the String 'Object') infers as its value type instead of the bare
178
+ # fallback. Other nodes use the standard path unchanged.
179
+ #
180
+ # @note module_function: defines #rescue_branch_type (visibility: private)
181
+ # @param [Parser::AST::Node, nil] rescue_body rescue branch body node
182
+ # @param [Hash] opts additional keyword options forwarded to inference
183
+ # @return [String, nil] inferred branch type or nil
184
+ def rescue_branch_type(rescue_body, **opts)
185
+ if rescue_body.is_a?(Parser::AST::Node) && rescue_body.type == :const
186
+ resolve_const_value_type(rescue_body, opts[:container]) ||
187
+ run_last_expr_type(rescue_body, **opts)
188
+ else
189
+ run_last_expr_type(rescue_body, **opts)
190
+ end
191
+ end
192
+
159
193
  # Build a map of local/global/ivar/constant assignments to inferred types.
160
194
  #
161
195
  # @note module_function: defines #build_local_variable_types (visibility: private)
162
196
  # @param [Parser::AST::Node] node AST node to walk
163
- # @param [Object] opts additional keyword options forwarded to inference
197
+ # @param [Hash] opts additional keyword options forwarded to inference
164
198
  # @return [Hash<String, String>, nil]
165
199
  def build_local_variable_types(node, **opts)
166
200
  types = {} #: Hash[String, String]
@@ -179,36 +213,56 @@ module Docscribe
179
213
  # @note module_function: defines #collect_assignment_type (visibility: private)
180
214
  # @param [Parser::AST::Node] node an assignment AST node
181
215
  # @param [Hash<String, String>] types the accumulated local variable type map
182
- # @param [Object] opts additional keyword options forwarded to inference
216
+ # @param [Hash] opts additional keyword options forwarded to inference
183
217
  # @return [void]
184
218
  def collect_assignment_type(node, types, **opts)
185
219
  name, value = assignment_name_and_value(node)
186
220
  return unless name && value
187
221
 
188
- inferred = if opts[:core_rbs_provider]
189
- run_last_expr_type(value, **opts, fallback_type: FALLBACK_TYPE,
190
- nil_as_optional: false, local_var_types: types)
222
+ inferred = if %i[op_asgn or_asgn].include?(node.type)
223
+ assignment_op_asgn_type(node, types, **opts)
191
224
  else
192
- Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
225
+ assignment_inferred_type(value, types, **opts)
193
226
  end
194
227
  types[name] = inferred if inferred && inferred != FALLBACK_TYPE
195
228
  end
196
229
 
230
+ # @note module_function: defines #assignment_inferred_type (visibility: private)
231
+ # @param [Parser::AST::Node] value
232
+ # @param [Hash<String, String>] types
233
+ # @param [Hash] opts
234
+ # @return [String, nil]
235
+ def assignment_inferred_type(value, types, **opts)
236
+ run_last_expr_type(value, fallback_type: FALLBACK_TYPE, nil_as_optional: false, local_var_types: types,
237
+ core_rbs_provider: opts[:core_rbs_provider], param_types: opts[:param_types],
238
+ signature_provider: opts[:signature_provider], container: opts[:container])
239
+ end
240
+
241
+ # @note module_function: defines #assignment_op_asgn_type (visibility: private)
242
+ # @param [Parser::AST::Node] node
243
+ # @param [Hash<String, String>] types
244
+ # @param [Hash] opts
245
+ # @return [String, nil]
246
+ def assignment_op_asgn_type(node, types, **opts)
247
+ run_last_expr_type(node, fallback_type: FALLBACK_TYPE, nil_as_optional: false, local_var_types: types,
248
+ core_rbs_provider: opts[:core_rbs_provider], param_types: opts[:param_types],
249
+ signature_provider: opts[:signature_provider], container: opts[:container])
250
+ end
251
+
197
252
  # Extract the variable name and value expression from an assignment node.
198
253
  #
199
254
  # @note module_function: defines #assignment_name_and_value (visibility: private)
200
- # @param [Parser::AST::Node] node an assignment AST node (:lvasgn, :gvasgn, :ivasgn, :casgn, :op_asgn)
255
+ # @param [Parser::AST::Node] node an assignment AST node (:lvasgn, :gvasgn, :ivasgn, :casgn, :op_asgn, :or_asgn)
201
256
  # @return [(String, nil, Parser::AST::Node, nil)]
202
257
  def assignment_name_and_value(node)
258
+ return [nil, nil] unless node.is_a?(Parser::AST::Node)
259
+
203
260
  case node.type
204
- when :lvasgn, :gvasgn, :ivasgn, :cvasgn
205
- [node.children[0].to_s, node.children[1]]
206
- when :casgn
207
- constant_name_and_value(node)
208
- when :op_asgn
209
- compound_name_and_value(node)
210
- else
211
- [nil, nil]
261
+ when :lvasgn, :gvasgn, :ivasgn, :cvasgn then [node.children[0].to_s, node.children[1]]
262
+ when :casgn then constant_name_and_value(node)
263
+ when :op_asgn then compound_name_and_value(node)
264
+ when :or_asgn then or_asgn_name_and_value(node)
265
+ else [nil, nil]
212
266
  end
213
267
  end
214
268
 
@@ -230,11 +284,26 @@ module Docscribe
230
284
  [node.children[0].children.first.to_s, node.children[2]]
231
285
  end
232
286
 
287
+ # Extract the name and value from an `:or_asgn` (`||=`) node.
288
+ #
289
+ # Unlike `:op_asgn` (three children: target, operator, value), `:or_asgn`
290
+ # carries only target and value.
291
+ #
292
+ # @note module_function: defines #or_asgn_name_and_value (visibility: private)
293
+ # @param [Parser::AST::Node] node the `:or_asgn` AST node
294
+ # @return [(String, nil, Parser::AST::Node, nil)]
295
+ def or_asgn_name_and_value(node)
296
+ target = node.children[0]
297
+ return [nil, nil] unless target.is_a?(Parser::AST::Node)
298
+
299
+ [target.children.first.to_s, node.children[1]]
300
+ end
301
+
233
302
  # Handle `:lvar` node for last_expr_type — look up the variable in local_var_types.
234
303
  #
235
304
  # @note module_function: defines #handle_lvar_node (visibility: private)
236
305
  # @param [Parser::AST::Node] node the `:lvar` AST node
237
- # @param [Object] opts additional keyword options forwarded to type inference
306
+ # @param [Hash] opts additional keyword options forwarded to type inference
238
307
  # @return [String, nil]
239
308
  def handle_lvar_node(node, **opts)
240
309
  name = node.children[0].to_s
@@ -245,7 +314,7 @@ module Docscribe
245
314
  #
246
315
  # @note module_function: defines #handle_ivar_node (visibility: private)
247
316
  # @param [Parser::AST::Node] node the `:ivar` AST node
248
- # @param [Object] opts additional keyword options forwarded to type inference
317
+ # @param [Hash] opts additional keyword options forwarded to type inference
249
318
  # @return [String, nil]
250
319
  def handle_ivar_node(node, **opts)
251
320
  name = node.children[0].to_s
@@ -256,7 +325,7 @@ module Docscribe
256
325
  #
257
326
  # @note module_function: defines #handle_gvar_node (visibility: private)
258
327
  # @param [Parser::AST::Node] node the `:gvar` AST node
259
- # @param [Object] opts additional keyword options forwarded to type inference
328
+ # @param [Hash] opts additional keyword options forwarded to type inference
260
329
  # @return [String, nil]
261
330
  def handle_gvar_node(node, **opts)
262
331
  name = node.children[0].to_s
@@ -267,7 +336,7 @@ module Docscribe
267
336
  #
268
337
  # @note module_function: defines #handle_cvar_node (visibility: private)
269
338
  # @param [Parser::AST::Node] node the `:cvar` AST node
270
- # @param [Object] opts additional keyword options forwarded to type inference
339
+ # @param [Hash] opts additional keyword options forwarded to type inference
271
340
  # @return [String, nil]
272
341
  def handle_cvar_node(node, **opts)
273
342
  name = node.children[0].to_s
@@ -278,7 +347,7 @@ module Docscribe
278
347
  #
279
348
  # @note module_function: defines #handle_lvasgn_node (visibility: private)
280
349
  # @param [Parser::AST::Node] node the `:lvasgn` AST node
281
- # @param [Object] opts additional keyword options forwarded to type inference
350
+ # @param [Hash] opts additional keyword options forwarded to type inference
282
351
  # @return [String, nil]
283
352
  def handle_lvasgn_node(node, **opts)
284
353
  name = node.children[0].to_s
@@ -291,7 +360,7 @@ module Docscribe
291
360
  #
292
361
  # @note module_function: defines #handle_ivasgn_node (visibility: private)
293
362
  # @param [Parser::AST::Node] node the `:ivasgn` AST node
294
- # @param [Object] opts additional keyword options forwarded to type inference
363
+ # @param [Hash] opts additional keyword options forwarded to type inference
295
364
  # @return [String, nil]
296
365
  def handle_ivasgn_node(node, **opts)
297
366
  name = node.children[0].to_s
@@ -304,7 +373,7 @@ module Docscribe
304
373
  #
305
374
  # @note module_function: defines #handle_gvasgn_node (visibility: private)
306
375
  # @param [Parser::AST::Node] node the `:gvasgn` AST node
307
- # @param [Object] opts additional keyword options forwarded to type inference
376
+ # @param [Hash] opts additional keyword options forwarded to type inference
308
377
  # @return [String, nil]
309
378
  def handle_gvasgn_node(node, **opts)
310
379
  name = node.children[0].to_s
@@ -317,7 +386,7 @@ module Docscribe
317
386
  #
318
387
  # @note module_function: defines #handle_cvasgn_node (visibility: private)
319
388
  # @param [Parser::AST::Node] node the `:cvasgn` AST node
320
- # @param [Object] opts additional keyword options forwarded to type inference
389
+ # @param [Hash] opts additional keyword options forwarded to type inference
321
390
  # @return [String, nil]
322
391
  def handle_cvasgn_node(node, **opts)
323
392
  name = node.children[0].to_s
@@ -328,31 +397,142 @@ module Docscribe
328
397
 
329
398
  # Handle `:op_asgn` node (compound assignment: `x += 1`, `@var -= 2`, etc.).
330
399
  #
331
- # Infers the result type from the operator and the right operand's type.
332
- # Uses RBS to resolve when available (e.g., `Integer#+` -> `Integer`).
400
+ # RBS -> Infer: try RBS for meth on receiver type, else unify left/right
401
+ # keeping String? via nil_as_optional:true. No hardcoded operator list.
333
402
  #
334
403
  # @note module_function: defines #handle_op_asgn_node (visibility: private)
335
404
  # @param [Parser::AST::Node] node the `:op_asgn` AST node
336
- # @param [Object] opts additional keyword options forwarded to type inference
405
+ # @param [Hash] opts additional keyword options forwarded to type inference
337
406
  # @return [String, nil]
338
407
  def handle_op_asgn_node(node, **opts)
339
408
  meth = node.children[1]
340
- return nil unless %i[+ - * / % ** << | & ^].include?(meth)
341
- return nil unless opts[:core_rbs_provider]
409
+ lhs = node.children[0]
410
+ rhs = node.children[2]
411
+ left = op_asgn_left_type(lhs, **opts)
412
+ right = op_asgn_right_type(rhs, **opts)
413
+ rbs = op_asgn_rbs_type(lhs, left, meth, **opts)
414
+ return rbs if rbs
415
+
416
+ op_asgn_fallback_type(left, right, meth, **opts)
417
+ end
342
418
 
343
- arg = node.children[2]
344
- arg_type = type_from_literal_safe(arg)
345
- return nil unless arg_type
419
+ # @note module_function: defines #op_asgn_left_type (visibility: private)
420
+ # @param [Parser::AST::Node, nil] lhs the lhs target node
421
+ # @param [Hash] opts
422
+ # @return [String, nil]
423
+ def op_asgn_left_type(lhs, **opts)
424
+ name = op_asgn_var_name(lhs)
425
+ if name
426
+ found = op_asgn_lookup_type(lhs, name, **opts)
427
+ return found if found
428
+ end
429
+ run_last_expr_type(lhs, **op_asgn_expr_opts(**opts))
430
+ end
431
+
432
+ # @note module_function: defines #op_asgn_var_name (visibility: private)
433
+ # @param [Parser::AST::Node, nil] lhs
434
+ # @return [String, nil]
435
+ def op_asgn_var_name(lhs)
436
+ return nil unless lhs.is_a?(Parser::AST::Node)
437
+
438
+ case lhs&.type
439
+ when :lvasgn, :ivasgn, :gvasgn, :cvasgn then lhs&.children&.[](0).to_s
440
+ when :casgn then lhs&.children&.[](1).to_s
441
+ end
442
+ end
443
+
444
+ # @note module_function: defines #op_asgn_lookup_type (visibility: private)
445
+ # @param [Parser::AST::Node] lhs
446
+ # @param [String] name
447
+ # @param [Hash] opts
448
+ # @return [String, nil]
449
+ def op_asgn_lookup_type(lhs, name, **opts)
450
+ return nil unless lhs.is_a?(Parser::AST::Node)
451
+
452
+ case lhs&.type
453
+ when :lvasgn
454
+ lookup_lvar_type(name, opts[:local_var_types], opts[:param_types])
455
+ when :ivasgn, :gvasgn, :cvasgn, :casgn
456
+ opts[:local_var_types]&.fetch(name, nil)
457
+ end
458
+ end
459
+
460
+ # @note module_function: defines #op_asgn_right_type (visibility: private)
461
+ # @param [Parser::AST::Node, nil] rhs
462
+ # @param [Hash] opts
463
+ # @return [String, nil]
464
+ def op_asgn_right_type(rhs, **opts)
465
+ return nil unless rhs
466
+
467
+ run_last_expr_type(rhs, **op_asgn_expr_opts(**opts))
468
+ end
469
+
470
+ # @note module_function: defines #op_asgn_expr_opts (visibility: private)
471
+ # @param [Hash] opts
472
+ # @return [Hash<Symbol, Object>]
473
+ def op_asgn_expr_opts(**opts)
474
+ {
475
+ fallback_type: opts[:fallback_type] || FALLBACK_TYPE,
476
+ nil_as_optional: true,
477
+ local_var_types: opts[:local_var_types],
478
+ param_types: opts[:param_types],
479
+ core_rbs_provider: opts[:core_rbs_provider],
480
+ signature_provider: opts[:signature_provider],
481
+ container: opts[:container]
482
+ }
483
+ end
484
+
485
+ # @note module_function: defines #op_asgn_rbs_type (visibility: private)
486
+ # @param [Parser::AST::Node, nil] lhs
487
+ # @param [String, nil] left
488
+ # @param [Symbol] meth
489
+ # @param [Hash] opts
490
+ # @return [String, nil]
491
+ def op_asgn_rbs_type(lhs, left, meth, **opts)
492
+ recv = cleaned_recv_type(left) ||
493
+ receiver_rbs_type_name(lhs, opts[:core_rbs_provider],
494
+ opts[:local_var_types], opts[:param_types])
495
+ return nil unless recv && meth
496
+
497
+ resolve_op_asgn_rbs(recv, meth, **opts)
498
+ end
499
+
500
+ # @note module_function: defines #resolve_op_asgn_rbs (visibility: private)
501
+ # @param [String] recv_type
502
+ # @param [Symbol] meth
503
+ # @param [Hash] opts
504
+ # @return [String, nil]
505
+ def resolve_op_asgn_rbs(recv_type, meth, **opts)
506
+ if opts[:core_rbs_provider]
507
+ rbs = resolve_rbs_return_type(recv_type, meth, opts[:core_rbs_provider])
508
+ return substitute_rbs_type(rbs, recv_type) unless rbs == FALLBACK_TYPE
509
+ end
510
+ if opts[:signature_provider]
511
+ sig = opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)
512
+ return substitute_rbs_type(sig.return_type, recv_type) if sig
513
+ end
514
+ nil
515
+ end
346
516
 
347
- rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
348
- rbs unless rbs == FALLBACK_TYPE
517
+ # @note module_function: defines #op_asgn_fallback_type (visibility: private)
518
+ # @param [String, nil] left
519
+ # @param [String, nil] right
520
+ # @param [Symbol] meth
521
+ # @param [Hash] opts
522
+ # @return [String, nil]
523
+ def op_asgn_fallback_type(left, right, meth, **opts)
524
+ fallback = (opts[:fallback_type] || FALLBACK_TYPE).to_s
525
+ return synthesize_shovel_type(left, right, fallback: fallback) if shovel_method?(left, meth, opts[:core_rbs_provider])
526
+
527
+ fallback_concrete_type(left, right, fallback) ||
528
+ unify_types(left, right, fallback_type: fallback, nil_as_optional: true)
349
529
  end
350
530
 
351
531
  # Handle `:begin` node for last_expr_type.
352
532
  #
353
533
  # @note module_function: defines #handle_begin_node (visibility: private)
354
534
  # @param [Parser::AST::Node] node the `:return` AST node
355
- # @param [Object] opts additional keyword options forwarded to type inference
535
+ # @param [Hash] opts additional keyword options forwarded to type inference
356
536
  # @return [String, nil]
357
537
  def handle_begin_node(node, **opts)
358
538
  run_last_expr_type(node.children.last, **opts)
@@ -362,7 +542,7 @@ module Docscribe
362
542
  #
363
543
  # @note module_function: defines #handle_if_node (visibility: private)
364
544
  # @param [Parser::AST::Node] node the `:return` AST node
365
- # @param [Object] opts additional keyword options forwarded to type inference
545
+ # @param [Hash] opts additional keyword options forwarded to type inference
366
546
  # @return [String, nil]
367
547
  def handle_if_node(node, **opts)
368
548
  t = run_last_expr_type(node.children[1], **opts)
@@ -379,7 +559,7 @@ module Docscribe
379
559
  #
380
560
  # @note module_function: defines #handle_case_node (visibility: private)
381
561
  # @param [Parser::AST::Node] node the `:return` AST node
382
- # @param [Object] opts additional keyword options forwarded to type inference
562
+ # @param [Hash] opts additional keyword options forwarded to type inference
383
563
  # @return [String, nil]
384
564
  def handle_case_node(node, **opts)
385
565
  branches = process_case_branches(node, **opts)
@@ -400,15 +580,57 @@ module Docscribe
400
580
  #
401
581
  # @note module_function: defines #handle_or_node (visibility: private)
402
582
  # @param [Parser::AST::Node] node the `:or` AST node
403
- # @param [Object] opts additional keyword options forwarded to type inference
583
+ # @param [Hash] opts additional keyword options forwarded to type inference
404
584
  # @return [String, nil]
405
585
  def handle_or_node(node, **opts)
406
586
  t = run_last_expr_type(node.children[0], **opts)
407
587
  e = run_last_expr_type(node.children[1], **opts)
408
- unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
588
+ fallback = opts[:fallback_type] || 'untyped'
589
+ # If one side is the fallback alias (FALLBACK_TYPE / fallback_type) and the other is concrete, prefer the concrete
590
+ # This prevents `sig&.return_type || FALLBACK_TYPE` from becoming `String, Object` when String is known
591
+ preferred = or_prefer_concrete(t, e, fallback)
592
+ return preferred if preferred
593
+
594
+ unify_types(t, e, fallback_type: fallback,
595
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
596
+ end
597
+
598
+ # Handle `:or_asgn` node (`x ||= y`) for last_expr_type.
599
+ #
600
+ # Same type semantics as `||`: the assignment target counts as the left
601
+ # side, so an unknown receiver with a concrete literal right-hand side
602
+ # (e.g. `@h ||= Hash.new`) infers the literal type.
603
+ #
604
+ # @note module_function: defines #handle_or_asgn_node (visibility: private)
605
+ # @param [Parser::AST::Node] node the `:or_asgn` AST node
606
+ # @param [Hash] opts additional keyword options forwarded to type inference
607
+ # @return [String, nil]
608
+ def handle_or_asgn_node(node, **opts)
609
+ t = run_last_expr_type(node.children[0], **opts)
610
+ e = run_last_expr_type(node.children[1], **opts)
611
+ fallback = opts[:fallback_type] || 'untyped'
612
+ preferred = or_prefer_concrete(t, e, fallback)
613
+ return preferred if preferred
614
+
615
+ unify_types(t, e, fallback_type: fallback,
409
616
  nil_as_optional: opts.fetch(:nil_as_optional, true))
410
617
  end
411
618
 
619
+ # Prefer the concrete side when the other is a fallback alias.
620
+ #
621
+ # @note module_function: defines #or_prefer_concrete (visibility: private)
622
+ # @param [String, nil] left_type left side inferred type
623
+ # @param [String, nil] right_type right side inferred type
624
+ # @param [String] fallback fallback type name
625
+ # @return [String, nil] preferred side or nil when neither applies
626
+ def or_prefer_concrete(left_type, right_type, fallback)
627
+ if fallback_alias?(left_type, fallback) && !fallback_alias?(right_type, fallback)
628
+ right_type
629
+ elsif fallback_alias?(right_type, fallback) && !fallback_alias?(left_type, fallback)
630
+ left_type
631
+ end
632
+ end
633
+
412
634
  # Handle `:and` node (`a && b`) for last_expr_type.
413
635
  #
414
636
  # The result type is the union of both sides, since either may be returned
@@ -416,7 +638,7 @@ module Docscribe
416
638
  #
417
639
  # @note module_function: defines #handle_and_node (visibility: private)
418
640
  # @param [Parser::AST::Node] node the `:and` AST node
419
- # @param [Object] opts additional keyword options forwarded to type inference
641
+ # @param [Hash] opts additional keyword options forwarded to type inference
420
642
  # @return [String, nil]
421
643
  def handle_and_node(node, **opts)
422
644
  t = run_last_expr_type(node.children[0], **opts)
@@ -432,7 +654,7 @@ module Docscribe
432
654
  #
433
655
  # @note module_function: defines #handle_kwbegin_node (visibility: private)
434
656
  # @param [Parser::AST::Node] node the `:kwbegin` AST node
435
- # @param [Object] opts additional keyword options forwarded to type inference
657
+ # @param [Hash] opts additional keyword options forwarded to type inference
436
658
  # @return [String, nil]
437
659
  def handle_kwbegin_node(node, **opts)
438
660
  run_last_expr_type(node.children.first, **opts)
@@ -445,7 +667,7 @@ module Docscribe
445
667
  #
446
668
  # @note module_function: defines #handle_rescue_node (visibility: private)
447
669
  # @param [Parser::AST::Node] node the `:rescue` AST node
448
- # @param [Object] opts additional keyword options forwarded to type inference
670
+ # @param [Hash] opts additional keyword options forwarded to type inference
449
671
  # @return [String, nil]
450
672
  def handle_rescue_node(node, **opts)
451
673
  branches = collect_rescue_branches(node, **opts)
@@ -462,7 +684,7 @@ module Docscribe
462
684
  #
463
685
  # @note module_function: defines #collect_rescue_branches (visibility: private)
464
686
  # @param [Parser::AST::Node] node the `:rescue` AST node
465
- # @param [Object] opts additional keyword options forwarded to type inference
687
+ # @param [Hash] opts additional keyword options forwarded to type inference
466
688
  # @return [Array<String, nil>]
467
689
  def collect_rescue_branches(node, **opts)
468
690
  branches = [run_last_expr_type(node.children[0], **opts)]
@@ -483,7 +705,7 @@ module Docscribe
483
705
  #
484
706
  # @note module_function: defines #handle_ensure_node (visibility: private)
485
707
  # @param [Parser::AST::Node] node the `:ensure` AST node
486
- # @param [Object] opts additional keyword options forwarded to type inference
708
+ # @param [Hash] opts additional keyword options forwarded to type inference
487
709
  # @return [String, nil]
488
710
  def handle_ensure_node(node, **opts)
489
711
  run_last_expr_type(node.children[0], **opts)
@@ -496,7 +718,7 @@ module Docscribe
496
718
  #
497
719
  # @note module_function: defines #handle_defined_node (visibility: private)
498
720
  # @param [Parser::AST::Node] _node the `:defined?` AST node
499
- # @param [Object] opts additional keyword options forwarded to type inference
721
+ # @param [Hash] opts additional keyword options forwarded to type inference
500
722
  # @return [String, nil]
501
723
  def handle_defined_node(_node, **opts)
502
724
  nil_as_optional = opts.fetch(:nil_as_optional, true)
@@ -510,7 +732,7 @@ module Docscribe
510
732
  #
511
733
  # @note module_function: defines #handle_zsuper_node (visibility: private)
512
734
  # @param [Parser::AST::Node] _node the `:zsuper` AST node
513
- # @param [Object] opts additional keyword options forwarded to type inference
735
+ # @param [Hash] opts additional keyword options forwarded to type inference
514
736
  # @return [String, nil]
515
737
  def handle_zsuper_node(_node, **opts)
516
738
  opts[:fallback_type]
@@ -523,7 +745,7 @@ module Docscribe
523
745
  #
524
746
  # @note module_function: defines #handle_super_node (visibility: private)
525
747
  # @param [Parser::AST::Node] _node the `:super` AST node
526
- # @param [Object] opts additional keyword options forwarded to type inference
748
+ # @param [Hash] opts additional keyword options forwarded to type inference
527
749
  # @return [String, nil]
528
750
  def handle_super_node(_node, **opts)
529
751
  opts[:fallback_type]
@@ -536,7 +758,7 @@ module Docscribe
536
758
  #
537
759
  # @note module_function: defines #handle_yield_node (visibility: private)
538
760
  # @param [Parser::AST::Node] _node the `:yield` AST node
539
- # @param [Object] opts additional keyword options forwarded to type inference
761
+ # @param [Hash] opts additional keyword options forwarded to type inference
540
762
  # @return [String, nil]
541
763
  def handle_yield_node(_node, **opts)
542
764
  opts[:fallback_type]
@@ -548,7 +770,7 @@ module Docscribe
548
770
  #
549
771
  # @note module_function: defines #handle_case_match_node (visibility: private)
550
772
  # @param [Parser::AST::Node] node the `:case_match` AST node
551
- # @param [Object] opts additional keyword options forwarded to type inference
773
+ # @param [Hash] opts additional keyword options forwarded to type inference
552
774
  # @return [String, nil]
553
775
  def handle_case_match_node(node, **opts)
554
776
  branches = process_pattern_branches(node, **opts)
@@ -568,7 +790,7 @@ module Docscribe
568
790
  #
569
791
  # @note module_function: defines #handle_in_pattern_node (visibility: private)
570
792
  # @param [Parser::AST::Node] node the `:in_pattern` AST node
571
- # @param [Object] opts additional keyword options forwarded to type inference
793
+ # @param [Hash] opts additional keyword options forwarded to type inference
572
794
  # @return [String, nil]
573
795
  def handle_in_pattern_node(node, **opts)
574
796
  run_last_expr_type(node.children[2], **opts)
@@ -578,7 +800,7 @@ module Docscribe
578
800
  #
579
801
  # @note module_function: defines #process_pattern_branches (visibility: private)
580
802
  # @param [Parser::AST::Node] node the :case_match AST node
581
- # @param [Object] opts additional keyword options forwarded to type inference
803
+ # @param [Hash] opts additional keyword options forwarded to type inference
582
804
  # @return [Array<String>] list of inferred types from each branch
583
805
  def process_pattern_branches(node, **opts)
584
806
  (node.children[1..] || []).compact.filter_map do |child|
@@ -590,51 +812,524 @@ module Docscribe
590
812
  #
591
813
  # @note module_function: defines #process_case_branches (visibility: private)
592
814
  # @param [Parser::AST::Node] node the :case AST node
593
- # @param [Object] opts additional keyword options forwarded to type inference
815
+ # @param [Hash] opts additional keyword options forwarded to type inference
594
816
  # @return [Array<String>] list of inferred types from each branch
595
817
  def process_case_branches(node, **opts)
596
- (node.children[1..] || []).compact.flat_map do |child|
597
- if child.type == :when
598
- run_last_expr_type(child.children.last, **opts)
599
- else
600
- run_last_expr_type(child, **opts)
601
- end
818
+ children = (node.children[1..] || []).compact
819
+ branches = children.flat_map do |child|
820
+ child.type == :when ? run_last_expr_type(child.children.last, **opts) : run_last_expr_type(child, **opts)
602
821
  end.compact
822
+ branches << 'nil' unless children.last && children.last.type != :when
823
+ branches
603
824
  end
604
825
 
605
826
  # Handle `:block` node for last_expr_type.
606
827
  #
607
828
  # @note module_function: defines #handle_block_node (visibility: private)
608
829
  # @param [Parser::AST::Node] node the `:return` AST node
609
- # @param [Object] opts additional keyword options forwarded to type inference
830
+ # @param [Hash] opts additional keyword options forwarded to type inference
610
831
  # @return [String, nil]
611
832
  def handle_block_node(node, **opts)
612
833
  send_node = node.children[0]
613
- if send_node&.type == :send
614
- type = send_rbs_type(send_node.children[0], send_node.children[1], **opts)
615
- return type if type
616
- end
834
+ return run_last_expr_type(node.children[2], **opts) unless send_node&.type == :send
835
+
836
+ meth = send_node.children[1]
837
+ handle_then_block(node, meth, **opts) ||
838
+ handle_to_h_block(node, meth, **opts) ||
839
+ block_send_rbs_type(node, send_node, **opts) ||
840
+ handle_map_block(node, meth, **opts) ||
841
+ run_last_expr_type(node.children[2], **opts)
842
+ end
843
+
844
+ # @note module_function: defines #handle_then_block (visibility: private)
845
+ # @param [Parser::AST::Node] node
846
+ # @param [Symbol] meth
847
+ # @param [Hash] opts
848
+ # @return [String, nil]
849
+ def handle_then_block(node, meth, **opts)
850
+ return nil unless %i[then yield_self].include?(meth)
617
851
 
618
852
  run_last_expr_type(node.children[2], **opts)
619
853
  end
620
854
 
855
+ # @note module_function: defines #handle_to_h_block (visibility: private)
856
+ # @param [Parser::AST::Node] node
857
+ # @param [Symbol] meth
858
+ # @param [Hash] opts
859
+ # @return [String, nil]
860
+ def handle_to_h_block(node, meth, **opts)
861
+ return nil unless meth == :to_h
862
+
863
+ recv = to_h_each_recv(node)
864
+ return nil unless recv
865
+
866
+ key, value = to_h_key_value(node, recv, **opts)
867
+ "Hash<#{key}, #{value}>"
868
+ end
869
+
870
+ # Receiver of `to_h` when it chains off `each_with_index`.
871
+ #
872
+ # @note module_function: defines #to_h_each_recv (visibility: private)
873
+ # @param [Parser::AST::Node] node block node
874
+ # @return [Parser::AST::Node, nil]
875
+ def to_h_each_recv(node)
876
+ send_node = node.children[0]
877
+ recv = send_node.children[0]
878
+ recv if recv&.type == :send && recv.children[1] == :each_with_index
879
+ end
880
+
881
+ # Resolve key/value types for a `to_h` block.
882
+ #
883
+ # @note module_function: defines #to_h_key_value (visibility: private)
884
+ # @param [Parser::AST::Node] node block node
885
+ # @param [Parser::AST::Node] recv `each_with_index` send node
886
+ # @param [Hash] opts additional keyword options forwarded to type inference
887
+ # @return [(String, String)]
888
+ def to_h_key_value(node, recv, **opts)
889
+ body = node.children[2]
890
+ key, value = to_h_block_pair_types(body, block_arg_names(node), **opts)
891
+ key ||= hash_elem_from_enumerator(recv.children[0], **opts)
892
+ key = 'Object' if unknown_type?(key)
893
+ value ||= pair_literal?(body) ? 'Object' : 'Integer'
894
+ [key, value]
895
+ end
896
+
897
+ # Names of the block parameters, if any.
898
+ #
899
+ # @note module_function: defines #block_arg_names (visibility: private)
900
+ # @param [Parser::AST::Node] node block node
901
+ # @return [Array<String>]
902
+ def block_arg_names(node)
903
+ args = node.children[1]
904
+ return [] unless args&.type == :args
905
+
906
+ args.children.filter_map { |a| a.children[0]&.to_s }
907
+ end
908
+
909
+ # Infer key/value types from a `[k, v]` pair literal block body.
910
+ #
911
+ # A bare `lvar` matching an `each_with_index` block parameter resolves
912
+ # structurally: first parameter is the element (resolved from the receiver
913
+ # by the caller), second parameter is always the Integer index.
914
+ #
915
+ # @note module_function: defines #to_h_block_pair_types (visibility: private)
916
+ # @param [Parser::AST::Node?] body block body node
917
+ # @param [Array<String>] arg_names block parameter names
918
+ # @param [Hash] opts additional keyword options forwarded to type inference
919
+ # @return [(String, nil, String, nil)] inferred key and value types, nil when unavailable
920
+ def to_h_block_pair_types(body, arg_names, **opts)
921
+ return [nil, nil] unless pair_literal?(body)
922
+
923
+ pair = body.type == :begin ? body.children.last : body
924
+ [pair_elem_type(pair.children[0], arg_names, 0, **opts),
925
+ pair_elem_type(pair.children[1], arg_names, 1, **opts)]
926
+ end
927
+
928
+ # Whether the block body is a `[k, v]` pair literal.
929
+ #
930
+ # @note module_function: defines #pair_literal? (visibility: private)
931
+ # @param [Parser::AST::Node?] body block body node
932
+ # @return [Boolean]
933
+ def pair_literal?(body)
934
+ body = body.children.last if body&.type == :begin
935
+ body&.type == :array && body.children.size == 2
936
+ end
937
+
938
+ # Infer one pair element, honoring block parameter positions.
939
+ #
940
+ # @note module_function: defines #pair_elem_type (visibility: private)
941
+ # @param [Parser::AST::Node?] node element node
942
+ # @param [Array<String>] arg_names block parameter names
943
+ # @param [Integer] position 0 for key, 1 for value
944
+ # @param [Hash] opts additional keyword options forwarded to type inference
945
+ # @return [String, nil]
946
+ def pair_elem_type(node, arg_names, position, **opts)
947
+ if node&.type == :lvar && node.children[0].to_s == arg_names[position]
948
+ return position == 1 ? 'Integer' : nil
949
+ end
950
+
951
+ infer_pair_elem(node, **opts)
952
+ end
953
+
954
+ # Infer one pair element type, normalizing unknown to nil.
955
+ #
956
+ # @note module_function: defines #infer_pair_elem (visibility: private)
957
+ # @param [Parser::AST::Node?] node element node
958
+ # @param [Hash] opts additional keyword options forwarded to type inference
959
+ # @return [String, nil]
960
+ def infer_pair_elem(node, **opts)
961
+ type = run_last_expr_type(node, **opts)
962
+ type unless unknown_type?(type)
963
+ end
964
+
965
+ # Whether a type string means "could not determine".
966
+ #
967
+ # @note module_function: defines #unknown_type? (visibility: private)
968
+ # @param [String, nil] type inferred type string
969
+ # @return [Boolean]
970
+ def unknown_type?(type)
971
+ type.nil? || %w[Object untyped nil].include?(type)
972
+ end
973
+
974
+ # @note module_function: defines #handle_map_block (visibility: private)
975
+ # @param [Parser::AST::Node] node
976
+ # @param [Symbol] meth
977
+ # @param [Hash] opts
978
+ # @return [String, nil]
979
+ def handle_map_block(node, meth, **opts)
980
+ return nil unless %i[map collect].include?(meth)
981
+
982
+ inner = run_last_expr_type(node.children[2], **opts)
983
+ return nil unless inner && inner != 'Object' && inner != 'untyped'
984
+
985
+ "Array<#{inner}>"
986
+ end
987
+
988
+ # @note module_function: defines #block_send_rbs_type (visibility: private)
989
+ # @param [Parser::AST::Node] node
990
+ # @param [Parser::AST::Node] send_node
991
+ # @param [Hash] opts
992
+ # @return [String, nil]
993
+ def block_send_rbs_type(node, send_node, **opts)
994
+ rbs_type = send_rbs_type(send_node.children[0], send_node.children[1], **opts)
995
+ return nil unless rbs_type
996
+
997
+ block_rbs_with_inner(rbs_type, node.children[2], **opts) || rbs_type
998
+ end
999
+
1000
+ # @note module_function: defines #block_rbs_with_inner (visibility: private)
1001
+ # @param [String, nil] rbs_type
1002
+ # @param [Parser::AST::Node] block_body
1003
+ # @param [Hash] opts
1004
+ # @return [String, nil]
1005
+ def block_rbs_with_inner(rbs_type, block_body, **opts)
1006
+ inner = run_last_expr_type(block_body, **opts)
1007
+ return nil unless inner
1008
+
1009
+ substituted = block_generic_substitution(rbs_type, inner)
1010
+ return substituted if substituted
1011
+
1012
+ bare_container_type(rbs_type, inner)
1013
+ end
1014
+
1015
+ # @note module_function: defines #block_generic_substitution (visibility: private)
1016
+ # @param [String] rbs_type
1017
+ # @param [String, nil] inner
1018
+ # @return [String, nil]
1019
+ def block_generic_substitution(rbs_type, inner)
1020
+ return nil unless generic_placeholder?(rbs_type)
1021
+
1022
+ inner_generic = extract_generic_inner(rbs_type)
1023
+ return nil unless inner_generic
1024
+
1025
+ placeholders = placeholder_tokens(inner_generic)
1026
+ result = substitute_placeholders(rbs_type, placeholders, inner)
1027
+ return result unless result == rbs_type
1028
+
1029
+ fallback_generic_substitution(rbs_type, inner)
1030
+ end
1031
+
1032
+ # @note module_function: defines #placeholder_tokens (visibility: private)
1033
+ # @param [String] inner_generic
1034
+ # @return [Array<String>]
1035
+ def placeholder_tokens(inner_generic)
1036
+ split_generic_args(inner_generic).select do |arg|
1037
+ placeholder_token?(arg.strip.delete_suffix('?').strip)
1038
+ end
1039
+ end
1040
+
1041
+ # @note module_function: defines #substitute_placeholders (visibility: private)
1042
+ # @param [String] rbs_type
1043
+ # @param [Array<String>] placeholders
1044
+ # @param [String] inner
1045
+ # @return [String]
1046
+ def substitute_placeholders(rbs_type, placeholders, inner)
1047
+ result = rbs_type.dup
1048
+ placeholders.each do |ph|
1049
+ token = ph.strip.delete_suffix('?').strip
1050
+ result = result.gsub(token, inner)
1051
+ end
1052
+ result
1053
+ end
1054
+
1055
+ # @note module_function: defines #fallback_generic_substitution (visibility: private)
1056
+ # @param [String] rbs_type
1057
+ # @param [String] inner
1058
+ # @return [String]
1059
+ def fallback_generic_substitution(rbs_type, inner)
1060
+ rbs_type.gsub(/\bU\b/, inner).gsub(/\bElem\b/, inner).gsub(/\buntyped\b/, inner)
1061
+ .gsub(/\bV\b/, inner).gsub(/\bT\b/, inner).gsub(/\bE\b/, inner).gsub(/\bK\b/, inner)
1062
+ end
1063
+
1064
+ # @note module_function: defines #bare_container_type (visibility: private)
1065
+ # @param [String, nil] rbs_type
1066
+ # @param [String] inner
1067
+ # @return [String, nil]
1068
+ def bare_container_type(rbs_type, inner)
1069
+ return nil unless rbs_type.is_a?(String)
1070
+
1071
+ base = rbs_type&.split(/[<\[ ]/)&.first
1072
+ return nil unless %w[Array Set Enumerable Enumerator].include?(base)
1073
+ return nil if rbs_type.include?('<') || rbs_type.include?('[')
1074
+
1075
+ "#{base}<#{inner}>"
1076
+ end
1077
+
1078
+ # @note module_function: defines #generic_placeholder? (visibility: private)
1079
+ # @param [String, nil] rbs_type
1080
+ # @return [Boolean]
1081
+ def generic_placeholder?(rbs_type)
1082
+ return false unless rbs_type =~ /[<\[]/
1083
+
1084
+ inner = extract_generic_inner(rbs_type)
1085
+ return false unless inner
1086
+
1087
+ split_generic_args(inner).any? do |arg|
1088
+ placeholder_token?(arg.strip.delete_suffix('?').strip)
1089
+ end
1090
+ end
1091
+
1092
+ # @note module_function: defines #placeholder_token? (visibility: private)
1093
+ # @param [String] token
1094
+ # @return [Boolean]
1095
+ def placeholder_token?(token)
1096
+ !!(token == 'untyped' || token.include?('::') || token =~ /\A[a-z]/ ||
1097
+ (token =~ /\A[A-Z][A-Za-z0-9_]*\z/ && !Docscribe::Types::Primitive.primitive?(token)))
1098
+ end
1099
+
621
1100
  # Handle `:send` node for last_expr_type.
622
1101
  #
623
1102
  # @note module_function: defines #handle_send_node (visibility: private)
624
1103
  # @param [Parser::AST::Node] node the `:return` AST node
625
- # @param [Object] opts additional keyword options forwarded to type inference
1104
+ # @param [Hash] opts additional keyword options forwarded to type inference
626
1105
  # @return [String, nil]
627
1106
  def handle_send_node(node, **opts)
628
1107
  recv = node.children[0]
629
1108
  meth = node.children[1]
1109
+ try_synthetic(node, meth, recv, **opts) ||
1110
+ try_rbs(meth, recv, **opts) ||
1111
+ try_compound(node, **opts) ||
1112
+ string_send_type(meth, recv) ||
1113
+ rbs_fallback(meth, recv, **opts) ||
1114
+ Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
1115
+ end
630
1116
 
631
- rbs_type = send_rbs_type(recv, meth, **opts) if opts[:core_rbs_provider]
632
- return rbs_type if rbs_type
1117
+ # @note module_function: defines #try_synthetic (visibility: private)
1118
+ # @param [Parser::AST::Node] node
1119
+ # @param [Symbol] meth
1120
+ # @param [Parser::AST::Node, nil] recv
1121
+ # @param [Hash] opts
1122
+ # @return [String, nil]
1123
+ def try_synthetic(node, meth, recv, **opts)
1124
+ synthetic_enumerator_type(node, meth, recv, **opts) ||
1125
+ synthetic_hash_type(node, meth, recv, **opts)
1126
+ end
1127
+
1128
+ # @note module_function: defines #try_rbs (visibility: private)
1129
+ # @param [Symbol] meth
1130
+ # @param [Parser::AST::Node, nil] recv
1131
+ # @param [Hash] opts
1132
+ # @return [String, nil]
1133
+ def try_rbs(meth, recv, **opts)
1134
+ return nil unless opts[:core_rbs_provider]
1135
+
1136
+ rbs = send_rbs_type(recv, meth, **opts)
1137
+ return rbs if rbs && rbs != FALLBACK_TYPE && !rbs.include?('Object')
1138
+
1139
+ nil
1140
+ end
1141
+
1142
+ # @note module_function: defines #try_compound (visibility: private)
1143
+ # @param [Parser::AST::Node] node
1144
+ # @param [Hash] opts
1145
+ # @return [String, nil]
1146
+ def try_compound(node, **opts)
1147
+ infer_from_compound_assign(node, **opts)
1148
+ end
1149
+
1150
+ # @note module_function: defines #rbs_fallback (visibility: private)
1151
+ # @param [Symbol] meth
1152
+ # @param [Parser::AST::Node, nil] recv
1153
+ # @param [Hash] opts
1154
+ # @return [String, nil]
1155
+ def rbs_fallback(meth, recv, **opts)
1156
+ return nil unless opts[:core_rbs_provider]
1157
+
1158
+ send_rbs_type(recv, meth, **opts)
1159
+ end
1160
+
1161
+ # @note module_function: defines #synthetic_enumerator_type (visibility: private)
1162
+ # @param [Parser::AST::Node] node
1163
+ # @param [Symbol] meth
1164
+ # @param [Parser::AST::Node, nil] recv
1165
+ # @param [Hash] opts
1166
+ # @return [String, nil]
1167
+ def synthetic_enumerator_type(node, meth, recv, **opts)
1168
+ return unless meth == :each_with_index && node.children.size == 2
1169
+
1170
+ elem = enumerator_elem_from_recv(recv, **opts)
1171
+ return "Enumerator<#{elem}, Integer>" if elem
1172
+
1173
+ 'Enumerator<Object, Integer>'
1174
+ end
1175
+
1176
+ # @note module_function: defines #enumerator_elem_from_recv (visibility: private)
1177
+ # @param [Parser::AST::Node, nil] recv
1178
+ # @param [Hash] opts
1179
+ # @return [String, nil]
1180
+ def enumerator_elem_from_recv(recv, **opts)
1181
+ recv_type = enumerator_recv_type(recv, **opts)
1182
+ return nil unless recv_type
1183
+
1184
+ extract_array_elem_strict(recv_type) || enumerator_object_elem(recv_type)
1185
+ end
1186
+
1187
+ # @note module_function: defines #enumerator_recv_type (visibility: private)
1188
+ # @param [Parser::AST::Node, nil] recv
1189
+ # @param [Hash] opts
1190
+ # @return [String, nil]
1191
+ def enumerator_recv_type(recv, **opts)
1192
+ receiver_rbs_type_name(recv, opts[:core_rbs_provider], opts[:local_var_types],
1193
+ opts[:param_types]) ||
1194
+ run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
1195
+ local_var_types: opts[:local_var_types],
1196
+ param_types: opts[:param_types],
1197
+ core_rbs_provider: opts[:core_rbs_provider],
1198
+ signature_provider: opts[:signature_provider],
1199
+ container: opts[:container])
1200
+ end
633
1201
 
634
- compound_type = infer_from_compound_assign(node, **opts)
635
- return compound_type if compound_type
1202
+ # @note module_function: defines #extract_array_elem_strict (visibility: private)
1203
+ # @param [String] type_str
1204
+ # @return [String, nil]
1205
+ def extract_array_elem_strict(type_str)
1206
+ return nil unless type_str =~ /\AArray<(.+)>\z/
636
1207
 
637
- Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
1208
+ Regexp.last_match(1).strip
1209
+ end
1210
+
1211
+ # @note module_function: defines #enumerator_object_elem (visibility: private)
1212
+ # @param [String] recv_type
1213
+ # @return [String, nil]
1214
+ def enumerator_object_elem(recv_type)
1215
+ return 'Object' if recv_type == 'Array'
1216
+
1217
+ nil
1218
+ end
1219
+
1220
+ # @note module_function: defines #synthetic_hash_type (visibility: private)
1221
+ # @param [Parser::AST::Node] _node
1222
+ # @param [Symbol] meth
1223
+ # @param [Parser::AST::Node, nil] recv
1224
+ # @param [Hash] opts
1225
+ # @return [String, nil]
1226
+ def synthetic_hash_type(_node, meth, recv, **opts)
1227
+ return unless meth == :to_h && recv && recv.type == :send && recv.children[1] == :each_with_index
1228
+
1229
+ inner_recv = recv.children[0]
1230
+ elem = hash_elem_from_enumerator(inner_recv, **opts) || 'Object'
1231
+ "Hash<#{elem}, Integer>"
1232
+ end
1233
+
1234
+ # @note module_function: defines #hash_elem_from_enumerator (visibility: private)
1235
+ # @param [Parser::AST::Node, nil] inner_recv
1236
+ # @param [Hash] opts
1237
+ # @return [String, nil]
1238
+ def hash_elem_from_enumerator(inner_recv, **opts)
1239
+ inner_type = hash_inner_type(inner_recv, **opts)
1240
+ return nil unless inner_type
1241
+
1242
+ extract_array_elem(inner_type) || extract_enumerator_elem(inner_type)
1243
+ end
1244
+
1245
+ # @note module_function: defines #hash_inner_type (visibility: private)
1246
+ # @param [Parser::AST::Node, nil] inner_recv
1247
+ # @param [Hash] opts
1248
+ # @return [String, nil]
1249
+ def hash_inner_type(inner_recv, **opts)
1250
+ receiver_rbs_type_name(inner_recv, opts[:core_rbs_provider], opts[:local_var_types],
1251
+ opts[:param_types]) ||
1252
+ run_last_expr_type(inner_recv, fallback_type: nil, nil_as_optional: false,
1253
+ local_var_types: opts[:local_var_types],
1254
+ param_types: opts[:param_types],
1255
+ core_rbs_provider: opts[:core_rbs_provider],
1256
+ signature_provider: opts[:signature_provider],
1257
+ container: opts[:container])
1258
+ end
1259
+
1260
+ # @note module_function: defines #extract_array_elem (visibility: private)
1261
+ # @param [String] type_str
1262
+ # @return [String, nil]
1263
+ def extract_array_elem(type_str)
1264
+ return nil unless type_str =~ /\AArray<(.+)>\z/
1265
+
1266
+ cand = Regexp.last_match(1).strip
1267
+ return nil if cand.empty? || %w[Object untyped].include?(cand)
1268
+
1269
+ cand
1270
+ end
1271
+
1272
+ # @note module_function: defines #extract_enumerator_elem (visibility: private)
1273
+ # @param [String] type_str
1274
+ # @return [String, nil]
1275
+ def extract_enumerator_elem(type_str)
1276
+ return nil unless type_str =~ /\AEnumerator<(.+),\s*Integer>\z/
1277
+
1278
+ cand = Regexp.last_match(1).strip
1279
+ return nil if cand.empty?
1280
+
1281
+ cand
1282
+ end
1283
+
1284
+ # @note module_function: defines #string_send_type (visibility: private)
1285
+ # @param [Symbol] meth
1286
+ # @param [Parser::AST::Node, nil] recv
1287
+ # @return [String, nil]
1288
+ def string_send_type(meth, recv)
1289
+ return 'String' if string_like_method?(meth)
1290
+ return 'String' if file_join_method?(meth, recv)
1291
+ return 'String' if sub_string_method?(meth, recv)
1292
+
1293
+ nil
1294
+ end
1295
+
1296
+ # @note module_function: defines #string_like_method? (visibility: private)
1297
+ # @param [Symbol] meth
1298
+ # @return [Boolean]
1299
+ def string_like_method?(meth)
1300
+ %i[to_s to_str inspect].include?(meth)
1301
+ end
1302
+
1303
+ # @note module_function: defines #file_join_method? (visibility: private)
1304
+ # @param [Symbol] meth
1305
+ # @param [Parser::AST::Node, nil] recv
1306
+ # @return [Boolean]
1307
+ def file_join_method?(meth, recv)
1308
+ meth == :join && recv&.type == :const && recv&.children&.[](1) == :File
1309
+ end
1310
+
1311
+ # @note module_function: defines #sub_string_method? (visibility: private)
1312
+ # @param [Symbol] meth
1313
+ # @param [Parser::AST::Node, nil] recv
1314
+ # @return [Boolean]
1315
+ def sub_string_method?(meth, recv)
1316
+ meth == :sub && recv && recv&.type != :const
1317
+ end
1318
+
1319
+ # @note module_function: defines #handle_csend_node (visibility: private)
1320
+ # @param [Parser::AST::Node] node the `:csend` AST node (safe navigation)
1321
+ # @param [Hash] opts additional keyword options forwarded to type inference
1322
+ # @return [String, nil]
1323
+ def handle_csend_node(node, **opts)
1324
+ recv = node.children[0]
1325
+ meth = node.children[1]
1326
+ rbs_type = send_rbs_type(recv, meth, **opts) if opts[:core_rbs_provider] || opts[:signature_provider]
1327
+ if rbs_type
1328
+ unify_types(rbs_type, 'nil', fallback_type: opts[:fallback_type] || FALLBACK_TYPE,
1329
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
1330
+ else
1331
+ opts[:fallback_type] || FALLBACK_TYPE
1332
+ end
638
1333
  end
639
1334
 
640
1335
  # Resolve RBS return type for a send node, trying explicit receiver first,
@@ -643,7 +1338,7 @@ module Docscribe
643
1338
  # @note module_function: defines #send_rbs_type (visibility: private)
644
1339
  # @param [Parser::AST::Node, nil] recv the receiver node
645
1340
  # @param [Symbol] meth the method name
646
- # @param [Object] opts additional keyword options
1341
+ # @param [Hash] opts additional keyword options
647
1342
  # @return [String, nil]
648
1343
  def send_rbs_type(recv, meth, **opts)
649
1344
  rbs_type = resolve_rbs_for_send(recv, meth, opts[:core_rbs_provider], opts[:local_var_types],
@@ -664,9 +1359,9 @@ module Docscribe
664
1359
  # @note module_function: defines #resolve_rbs_for_send (visibility: private)
665
1360
  # @param [Parser::AST::Node, nil] recv the receiver node of the send
666
1361
  # @param [Symbol] meth the method name being called
667
- # @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
668
- # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
669
- # @param [Hash<String, String>, nil] param_types parameter name to type map
1362
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider optional RBS provider for core type lookup
1363
+ # @param [Hash<String, String>?] local_var_types inferred local variable type map
1364
+ # @param [Hash<String, String>?] param_types parameter name to type map
670
1365
  # @return [String, nil] resolved type or nil if unresolvable
671
1366
  def resolve_rbs_for_send(recv, meth, core_rbs_provider, local_var_types, param_types)
672
1367
  recv_type = receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
@@ -674,7 +1369,7 @@ module Docscribe
674
1369
 
675
1370
  if core_rbs_provider
676
1371
  rbs = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
677
- return rbs unless rbs == FALLBACK_TYPE
1372
+ return substitute_rbs_type(rbs, recv_type) unless rbs == FALLBACK_TYPE
678
1373
  end
679
1374
 
680
1375
  nil
@@ -687,7 +1382,7 @@ module Docscribe
687
1382
  # @note module_function: defines #resolve_rbs_for_send_with_signature_provider (visibility: private)
688
1383
  # @param [Parser::AST::Node, nil] recv the receiver node
689
1384
  # @param [Symbol] meth the method name
690
- # @param [Object] opts additional keyword options
1385
+ # @param [Hash] opts additional keyword options
691
1386
  # @return [String, nil]
692
1387
  def resolve_rbs_for_send_with_signature_provider(recv, meth, **opts)
693
1388
  return nil unless opts[:signature_provider]
@@ -696,7 +1391,8 @@ module Docscribe
696
1391
  opts[:param_types])
697
1392
  return nil unless recv_type
698
1393
 
699
- opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)&.return_type
1394
+ rbs = opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)&.return_type
1395
+ rbs ? substitute_rbs_type(rbs, recv_type) : nil
700
1396
  end
701
1397
 
702
1398
  # Resolve return type from the current method's container via RBS.
@@ -706,19 +1402,19 @@ module Docscribe
706
1402
  #
707
1403
  # @note module_function: defines #container_rbs_return_type (visibility: private)
708
1404
  # @param [Symbol] meth the method name being called
709
- # @param [Object] opts additional keyword options (must include :container and :core_rbs_provider)
1405
+ # @param [Hash] opts additional keyword options (must include :container and :core_rbs_provider)
710
1406
  # @return [String, nil] resolved type or nil if unresolvable
711
1407
  def container_rbs_return_type(meth, **opts)
712
1408
  return unless opts[:container]
713
1409
 
714
1410
  if opts[:core_rbs_provider]
715
1411
  rbs = resolve_rbs_return_type(opts[:container], meth, opts[:core_rbs_provider])
716
- return rbs unless rbs == FALLBACK_TYPE
1412
+ return substitute_rbs_type(rbs, opts[:container]) unless rbs == FALLBACK_TYPE
717
1413
  end
718
1414
 
719
1415
  if opts[:signature_provider]
720
1416
  sig = opts[:signature_provider].signature_for(container: opts[:container], scope: :instance, name: meth)
721
- return sig.return_type if sig
1417
+ return substitute_rbs_type(sig.return_type, opts[:container]) if sig
722
1418
  end
723
1419
 
724
1420
  nil
@@ -740,51 +1436,454 @@ module Docscribe
740
1436
  nil: 'NilClass'
741
1437
  }.freeze
742
1438
 
1439
+ # Infer return type from a compound-assignment-like `:send`.
1440
+ #
1441
+ # RBS -> Infer: no hardcoded operator list, always try left/right via Infer,
1442
+ # then RBS for meth on receiver, else unify with String? handling.
1443
+ #
1444
+ # @note module_function: defines #infer_from_compound_assign (visibility: private)
1445
+ # @param [Parser::AST::Node] node the `:send` AST node
1446
+ # @param [Hash] opts additional keyword options forwarded to type inference
1447
+ # @return [String, nil]
1448
+ def infer_from_compound_assign(node, **opts)
1449
+ meth = node.children[1]
1450
+ recv = node.children[0]
1451
+ arg = node.children[2]
1452
+ left = compound_left_type(recv, **opts)
1453
+ right = compound_right_type(arg, **opts)
1454
+ rbs = compound_rbs_type(recv, left, meth, **opts)
1455
+ return rbs if rbs
1456
+
1457
+ compound_fallback_type(left, right, meth, **opts)
1458
+ end
1459
+
1460
+ # @note module_function: defines #compound_left_type (visibility: private)
1461
+ # @param [Parser::AST::Node, nil] recv
1462
+ # @param [Hash] opts
1463
+ # @return [String, nil]
1464
+ def compound_left_type(recv, **opts)
1465
+ return nil unless recv
1466
+
1467
+ found = compound_var_lookup(recv, **opts)
1468
+ return found if found
1469
+
1470
+ run_last_expr_type(recv, **compound_expr_opts(**opts))
1471
+ end
1472
+
1473
+ # @note module_function: defines #compound_var_lookup (visibility: private)
1474
+ # @param [Parser::AST::Node] recv
1475
+ # @param [Hash] opts
1476
+ # @return [String, nil]
1477
+ def compound_var_lookup(recv, **opts)
1478
+ return nil unless recv.is_a?(Parser::AST::Node)
1479
+ return nil unless %i[lvar ivar gvar cvar].include?(recv&.type)
1480
+
1481
+ name = recv&.children&.[](0).to_s
1482
+ if recv&.type == :lvar
1483
+ lookup_lvar_type(name, opts[:local_var_types], opts[:param_types])
1484
+ else
1485
+ opts[:local_var_types]&.fetch(name, nil)
1486
+ end
1487
+ end
1488
+
1489
+ # @note module_function: defines #compound_right_type (visibility: private)
1490
+ # @param [Parser::AST::Node, nil] arg
1491
+ # @param [Hash] opts
1492
+ # @return [String, nil]
1493
+ def compound_right_type(arg, **opts)
1494
+ return nil unless arg
1495
+
1496
+ run_last_expr_type(arg, **compound_expr_opts(**opts))
1497
+ end
1498
+
1499
+ # @note module_function: defines #compound_expr_opts (visibility: private)
1500
+ # @param [Hash] opts
1501
+ # @return [Hash<Symbol, Object>]
1502
+ def compound_expr_opts(**opts)
1503
+ {
1504
+ fallback_type: opts[:fallback_type] || FALLBACK_TYPE,
1505
+ nil_as_optional: true,
1506
+ local_var_types: opts[:local_var_types],
1507
+ param_types: opts[:param_types],
1508
+ core_rbs_provider: opts[:core_rbs_provider],
1509
+ signature_provider: opts[:signature_provider],
1510
+ container: opts[:container]
1511
+ }
1512
+ end
1513
+
1514
+ # @note module_function: defines #compound_rbs_type (visibility: private)
1515
+ # @param [Parser::AST::Node, nil] recv
1516
+ # @param [String, nil] left
1517
+ # @param [Symbol] meth
1518
+ # @param [Hash] opts
1519
+ # @return [String, nil]
1520
+ def compound_rbs_type(recv, left, meth, **opts)
1521
+ recv_type = cleaned_recv_type(left) ||
1522
+ receiver_rbs_type_name(recv, opts[:core_rbs_provider],
1523
+ opts[:local_var_types], opts[:param_types])
1524
+ return nil unless recv_type && meth
1525
+
1526
+ resolve_compound_rbs(recv_type, meth, **opts)
1527
+ end
1528
+
1529
+ # @note module_function: defines #resolve_compound_rbs (visibility: private)
1530
+ # @param [String] recv_type
1531
+ # @param [Symbol] meth
1532
+ # @param [Hash] opts
1533
+ # @return [String, nil]
1534
+ def resolve_compound_rbs(recv_type, meth, **opts)
1535
+ if opts[:core_rbs_provider]
1536
+ rbs = resolve_rbs_return_type(recv_type, meth, opts[:core_rbs_provider])
1537
+ return substitute_rbs_type(rbs, recv_type) unless rbs == FALLBACK_TYPE
1538
+ end
1539
+ if opts[:signature_provider]
1540
+ sig = opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)
1541
+ return substitute_rbs_type(sig.return_type, recv_type) if sig
1542
+ end
1543
+ nil
1544
+ end
1545
+
1546
+ # @note module_function: defines #compound_fallback_type (visibility: private)
1547
+ # @param [String, nil] left
1548
+ # @param [String, nil] right
1549
+ # @param [Symbol] meth
1550
+ # @param [Hash] opts
1551
+ # @return [String, nil]
1552
+ def compound_fallback_type(left, right, meth, **opts)
1553
+ fallback = (opts[:fallback_type] || FALLBACK_TYPE).to_s
1554
+ return synthesize_shovel_type(left, right, fallback: fallback) if shovel_method?(left, meth, opts[:core_rbs_provider])
1555
+ return nil unless %i[+ - * / % ** | & ^].include?(meth)
1556
+
1557
+ fallback_concrete_type(left, right, fallback) ||
1558
+ unify_types(left, right, fallback_type: fallback, nil_as_optional: true)
1559
+ end
1560
+
1561
+ # @note module_function: defines #fallback_concrete_type (visibility: private)
1562
+ # @param [String, nil] left
1563
+ # @param [String, nil] right
1564
+ # @param [String] fallback
1565
+ # @return [String]
1566
+ def fallback_concrete_type(left, right, fallback)
1567
+ left_is_fallback = fallback_type?(left, fallback)
1568
+ right_is_fallback = fallback_type?(right, fallback)
1569
+ preferred = fallback_preferred_side(left, right, left_is_fallback, right_is_fallback)
1570
+ return preferred if preferred
1571
+ return fallback if left_is_fallback && right_is_fallback
1572
+
1573
+ nil
1574
+ end
1575
+
1576
+ # @note module_function: defines #fallback_preferred_side (visibility: private)
1577
+ # @param [String, nil] left
1578
+ # @param [String, nil] right
1579
+ # @param [Boolean] left_is_fallback
1580
+ # @param [Boolean] right_is_fallback
1581
+ # @return [String, nil]
1582
+ def fallback_preferred_side(left, right, left_is_fallback, right_is_fallback)
1583
+ return right.to_s if left_is_fallback && !right_is_fallback && right
1584
+ return left.to_s if right_is_fallback && !left_is_fallback && left
1585
+
1586
+ nil
1587
+ end
1588
+
1589
+ # @note module_function: defines #fallback_type? (visibility: private)
1590
+ # @param [String, nil] type
1591
+ # @param [String] fallback
1592
+ # @return [Boolean]
1593
+ def fallback_type?(type, fallback)
1594
+ type.nil? || fallback_alias?(type, fallback)
1595
+ end
1596
+
1597
+ # @note module_function: defines #cleaned_recv_type (visibility: private)
1598
+ # @param [String, nil] raw
1599
+ # @return [String, nil]
1600
+ def cleaned_recv_type(raw) # rubocop:disable SortedMethodsByCall/Waterfall
1601
+ return nil unless raw && raw != FALLBACK_TYPE
1602
+
1603
+ str = raw.to_s.strip
1604
+ return nil if str.empty?
1605
+
1606
+ str = stripped_union_type(str) || str if str.include?(',')
1607
+ cleaned = str.delete_suffix('?').strip
1608
+ cleaned.empty? ? nil : cleaned
1609
+ end
1610
+
1611
+ # @note module_function: defines #synthesize_shovel_type (visibility: private)
1612
+ # @param [String, nil] left
1613
+ # @param [String, nil] right
1614
+ # @param [String] fallback
1615
+ # @return [String]
1616
+ def synthesize_shovel_type(left, right, fallback:)
1617
+ l = left || fallback
1618
+ r = right || fallback
1619
+ base = l.split(/[<\[ ]/).first.to_s.strip.delete_suffix('?')
1620
+ return shovel_array_type(l, r, base, fallback) if %w[Array Set Enumerable Enumerator].include?(base)
1621
+
1622
+ l
1623
+ end
1624
+
1625
+ # Whether left#meth is shovel (returns self) via RBS dynamically, not hardcoding :<<.
1626
+ #
1627
+ # @note module_function: defines #shovel_method? (visibility: private)
1628
+ # @param [String, nil] left left type
1629
+ # @param [Symbol] meth method name
1630
+ # @param [Docscribe::Types::RBS::Provider?] provider optional RBS provider
1631
+ # @return [Boolean] true if shovel
1632
+ def shovel_method?(left, meth, provider)
1633
+ return false unless left && meth
1634
+
1635
+ base = left.split(/[<\[ ]/).first.to_s.strip.delete_suffix('?')
1636
+ return false if base.empty?
1637
+ return true if resolve_self_via_rbs?(base, meth, provider)
1638
+
1639
+ resolve_self_via_rbs?(base, meth, core_rbs_provider)
1640
+ end
1641
+
1642
+ # @note module_function: defines #resolve_self_via_rbs? (visibility: private)
1643
+ # @param [String] base
1644
+ # @param [Symbol] meth
1645
+ # @param [Docscribe::Types::RBS::Provider?] provider
1646
+ # @return [Boolean]
1647
+ def resolve_self_via_rbs?(base, meth, provider)
1648
+ return false unless provider
1649
+
1650
+ resolve_rbs_return_type(base, meth, provider) == 'self'
1651
+ end
1652
+
1653
+ # Core RBS provider singleton for shovel/primitive checks (dynamic, not hardcoding).
1654
+ #
1655
+ # @note module_function: defines #core_rbs_provider (visibility: private)
1656
+ # @raise [LoadError]
1657
+ # @raise [StandardError]
1658
+ # @return [Docscribe::Types::RBS::Provider, nil]
1659
+ def core_rbs_provider
1660
+ @core_rbs_provider ||= begin
1661
+ require_relative '../types/rbs/provider'
1662
+ Docscribe::Types::RBS::Provider.new(sig_dirs: ['sig'], collection_dirs: [])
1663
+ rescue LoadError, StandardError
1664
+ nil
1665
+ end
1666
+ end
1667
+
1668
+ # @note module_function: defines #shovel_array_type (visibility: private)
1669
+ # @param [String] left_str
1670
+ # @param [String] right_str
1671
+ # @param [String] base
1672
+ # @param [String] fallback
1673
+ # @return [String]
1674
+ def shovel_array_type(left_str, right_str, base, fallback)
1675
+ return left_str if shovel_left_generic?(left_str)
1676
+ return left_str if shovel_right_invalid?(right_str, fallback)
1677
+
1678
+ cleaned = right_str.to_s.strip.delete_suffix('?').strip
1679
+ return left_str if cleaned == 'nil' || cleaned.empty? || cleaned == FALLBACK_TYPE
1680
+
1681
+ "#{base}<#{cleaned}>"
1682
+ end
1683
+
1684
+ # @note module_function: defines #shovel_left_generic? (visibility: private)
1685
+ # @param [String] str
1686
+ # @return [Boolean]
1687
+ def shovel_left_generic?(str)
1688
+ str.include?('<') || str.include?('[')
1689
+ end
1690
+
1691
+ # @note module_function: defines #shovel_right_invalid? (visibility: private)
1692
+ # @param [String] str
1693
+ # @param [String] fallback
1694
+ # @return [Boolean]
1695
+ def shovel_right_invalid?(str, fallback)
1696
+ str == fallback || %w[Object untyped].include?(str)
1697
+ end
1698
+
743
1699
  # Map receiver AST node to RBS type name.
744
1700
  #
745
1701
  # @note module_function: defines #receiver_rbs_type_name (visibility: private)
746
1702
  # @param [Parser::AST::Node, nil] recv the receiver AST node
747
- # @param [Object, nil] core_rbs_provider core RBS type provider
748
- # @param [Hash<Object, Object>, nil] local_var_types inferred local variable types
749
- # @param [Hash<String, String>, nil] param_types parameter name-to-type map
1703
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider core RBS type provider
1704
+ # @param [Hash<String, String>?] local_var_types inferred local variable types
1705
+ # @param [Hash<String, String>?] param_types parameter name-to-type map
750
1706
  # @return [String, nil]
751
1707
  def receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
752
1708
  return unless recv
753
- return LITERAL_RBS_TYPES[recv.type] if LITERAL_RBS_TYPES.key?(recv.type)
754
- return lookup_lvar_type(recv.children.first, local_var_types, param_types) if %i[lvar ivar gvar
755
- cvar].include?(recv.type)
756
- return unless recv.type == :send
757
1709
 
1710
+ literal = receiver_literal_type(recv)
1711
+ return literal if literal
1712
+ return receiver_var_type(recv, local_var_types, param_types) if var_receiver?(recv)
1713
+
1714
+ receiver_dispatch_type(recv, core_rbs_provider, local_var_types, param_types)
1715
+ end
1716
+
1717
+ # @note module_function: defines #receiver_dispatch_type (visibility: private)
1718
+ # @param [Parser::AST::Node, nil] recv
1719
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider
1720
+ # @param [Hash<String, String>?] local_var_types
1721
+ # @param [Hash<String, String>?] param_types
1722
+ # @return [String, nil]
1723
+ def receiver_dispatch_type(recv, core_rbs_provider, local_var_types, param_types)
1724
+ return nil unless recv.is_a?(Parser::AST::Node)
1725
+
1726
+ case recv.type
1727
+ when :send, :csend then receiver_send_type(recv, core_rbs_provider, local_var_types, param_types)
1728
+ when :block then block_receiver_type(recv, core_rbs_provider, local_var_types, param_types)
1729
+ when :or, :and then receiver_or_and_type(recv, core_rbs_provider, local_var_types, param_types)
1730
+ when :begin then receiver_begin_type(recv, core_rbs_provider, local_var_types, param_types)
1731
+ end
1732
+ end
1733
+
1734
+ # @note module_function: defines #block_receiver_type (visibility: private)
1735
+ # @param [Parser::AST::Node] recv
1736
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider
1737
+ # @param [Hash<String, String>?] local_var_types
1738
+ # @param [Hash<String, String>?] param_types
1739
+ # @return [String, nil]
1740
+ def block_receiver_type(recv, core_rbs_provider, local_var_types, param_types)
758
1741
  run_last_expr_type(recv, fallback_type: FALLBACK_TYPE, nil_as_optional: false,
759
- core_rbs_provider: core_rbs_provider,
760
- param_types: param_types,
761
- local_var_types: local_var_types)
1742
+ core_rbs_provider: core_rbs_provider, local_var_types: local_var_types,
1743
+ param_types: param_types) ||
1744
+ receiver_send_type(recv.children[0], core_rbs_provider, local_var_types, param_types)
762
1745
  end
763
1746
 
764
- # Infer return type from a compound-assignment-like `:send` by reading the
765
- # first literal argument's type — only fires when `core_rbs_provider` is
766
- # present and the argument's RBS return type can be resolved.
767
- #
768
- # Enables `@var += 123` -> `Integer` (via `Integer#+`) and similar patterns.
769
- #
770
- # @note module_function: defines #infer_from_compound_assign (visibility: private)
771
- # @param [Parser::AST::Node] node the `:send` AST node
772
- # @param [Object] opts additional keyword options forwarded to type inference
1747
+ # @note module_function: defines #receiver_begin_type (visibility: private)
1748
+ # @param [Parser::AST::Node, nil] recv
1749
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider
1750
+ # @param [Hash<String, String>?] local_var_types
1751
+ # @param [Hash<String, String>?] param_types
773
1752
  # @return [String, nil]
774
- def infer_from_compound_assign(node, **opts)
775
- return nil unless opts[:core_rbs_provider]
1753
+ def receiver_begin_type(recv, core_rbs_provider, local_var_types, param_types)
1754
+ return nil unless recv.is_a?(Parser::AST::Node)
776
1755
 
777
- meth = node.children[1]
778
- return nil unless %i[+ - * / % ** << | & ^].include?(meth)
1756
+ inner = recv&.children&.[](0)
1757
+ return unless inner && recv&.children&.size == 1
1758
+
1759
+ receiver_rbs_type_name(inner, core_rbs_provider, local_var_types, param_types)
1760
+ end
1761
+
1762
+ # @note module_function: defines #receiver_or_and_type (visibility: private)
1763
+ # @param [Parser::AST::Node, nil] recv
1764
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider
1765
+ # @param [Hash<String, String>?] local_var_types
1766
+ # @param [Hash<String, String>?] param_types
1767
+ # @return [String, nil]
1768
+ def receiver_or_and_type(recv, core_rbs_provider, local_var_types, param_types)
1769
+ return nil unless recv.is_a?(Parser::AST::Node)
1770
+
1771
+ left = receiver_rbs_type_name(recv&.children&.[](0), core_rbs_provider, local_var_types, param_types)
1772
+ right = receiver_rbs_type_name(recv&.children&.[](1), core_rbs_provider, local_var_types, param_types)
1773
+ left_clean = resolve_cleaned_type(left)
1774
+ right_clean = resolve_cleaned_type(right)
1775
+ receiver_or_and_preference(left_clean, right_clean, left, right)
1776
+ end
1777
+
1778
+ # @note module_function: defines #resolve_cleaned_type (visibility: private)
1779
+ # @param [String, nil] type
1780
+ # @return [String, nil]
1781
+ def resolve_cleaned_type(type)
1782
+ return nil unless type
779
1783
 
780
- first_arg = node.children[2]
781
- return nil unless first_arg
1784
+ cleaned_recv_type(type) || type
1785
+ end
782
1786
 
783
- arg_type = type_from_literal_safe(first_arg)
784
- return nil unless arg_type
1787
+ # @note module_function: defines #receiver_or_and_preference (visibility: private)
1788
+ # @param [String, nil] left_clean
1789
+ # @param [String, nil] right_clean
1790
+ # @param [String, nil] left
1791
+ # @param [String, nil] right
1792
+ # @return [String, nil]
1793
+ def receiver_or_and_preference(left_clean, right_clean, left, right)
1794
+ preferred = single_clean_preference(left_clean, right_clean)
1795
+ return preferred if preferred
1796
+ return left_clean if both_clean_equal?(left_clean, right_clean)
785
1797
 
786
- rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
787
- rbs unless rbs == FALLBACK_TYPE
1798
+ left_clean || right_clean || left || right
1799
+ end
1800
+
1801
+ # @note module_function: defines #single_clean_preference (visibility: private)
1802
+ # @param [String, nil] left_clean
1803
+ # @param [String, nil] right_clean
1804
+ # @return [String, nil]
1805
+ def single_clean_preference(left_clean, right_clean)
1806
+ return left_clean if left_clean && !right_clean
1807
+ return right_clean if right_clean && !left_clean
1808
+
1809
+ nil
1810
+ end
1811
+
1812
+ # @note module_function: defines #both_clean_equal? (visibility: private)
1813
+ # @param [String, nil] left_clean
1814
+ # @param [String, nil] right_clean
1815
+ # @return [Boolean]
1816
+ def both_clean_equal?(left_clean, right_clean)
1817
+ left_clean && right_clean && left_clean == right_clean
1818
+ end
1819
+
1820
+ # @note module_function: defines #receiver_literal_type (visibility: private)
1821
+ # @param [Parser::AST::Node, nil] recv
1822
+ # @return [String, nil]
1823
+ def receiver_literal_type(recv)
1824
+ return nil unless recv.is_a?(Parser::AST::Node)
1825
+
1826
+ LITERAL_RBS_TYPES[recv&.type] if LITERAL_RBS_TYPES.key?(recv&.type)
1827
+ end
1828
+
1829
+ # @note module_function: defines #var_receiver? (visibility: private)
1830
+ # @param [Parser::AST::Node, nil] recv
1831
+ # @return [Boolean]
1832
+ def var_receiver?(recv)
1833
+ return false unless recv.is_a?(Parser::AST::Node)
1834
+
1835
+ %i[lvar ivar gvar cvar].include?(recv&.type)
1836
+ end
1837
+
1838
+ # @note module_function: defines #receiver_var_type (visibility: private)
1839
+ # @param [Parser::AST::Node, nil] recv
1840
+ # @param [Hash<String, String>?] local_var_types
1841
+ # @param [Hash<String, String>?] param_types
1842
+ # @return [String, nil]
1843
+ def receiver_var_type(recv, local_var_types, param_types)
1844
+ raw = lookup_lvar_type(recv.children.first, local_var_types, param_types)
1845
+ return nil unless raw
1846
+
1847
+ cleaned = raw.include?(',') ? stripped_union_type(raw) : raw
1848
+ cleaned = cleaned.to_s.strip.delete_suffix('?').strip
1849
+ return nil if cleaned.empty? || cleaned == 'FALLBACK_TYPE'
1850
+
1851
+ cleaned
1852
+ end
1853
+
1854
+ # @note module_function: defines #stripped_union_type (visibility: private)
1855
+ # @param [String, nil] raw
1856
+ # @return [String, nil]
1857
+ def stripped_union_type(raw)
1858
+ parts = split_top_level_commas(raw).map { |p| p.strip.delete_suffix('?').strip }
1859
+ non_nil = parts.reject { |p| %w[nil FALLBACK_TYPE].include?(p) }
1860
+ (non_nil.first || parts.first).to_s.strip.delete_suffix('?').strip
1861
+ end
1862
+
1863
+ # Split a type string by top-level commas (outside any < > [ ] ( ) nesting).
1864
+ #
1865
+ # Used to distinguish union types (`String, nil`) from generic commas (`Hash<Integer, String>`).
1866
+ #
1867
+ # @note module_function: defines #split_top_level_commas (visibility: private)
1868
+ # @param [String] str the type string to split
1869
+ # @return [Array<String>]
1870
+ def split_top_level_commas(str)
1871
+ state = { parts: [], cur: +'', da: 0, db: 0, dp: 0 } #: Hash[Symbol, untyped]
1872
+ str.each_char { |chr| split_process_char(chr, state, strip: false) }
1873
+ state[:parts] << state[:cur] unless state[:cur].empty?
1874
+ state[:parts]
1875
+ end
1876
+
1877
+ # @note module_function: defines #receiver_send_type (visibility: private)
1878
+ # @param [Parser::AST::Node, nil] recv
1879
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider
1880
+ # @param [Hash<String, String>?] local_var_types
1881
+ # @param [Hash<String, String>?] param_types
1882
+ # @return [String, nil]
1883
+ def receiver_send_type(recv, core_rbs_provider, local_var_types, param_types)
1884
+ run_last_expr_type(recv, fallback_type: FALLBACK_TYPE, nil_as_optional: false,
1885
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
1886
+ local_var_types: local_var_types)
788
1887
  end
789
1888
 
790
1889
  # Safely get a type string from a literal node, returning nil if the node
@@ -805,9 +1904,9 @@ module Docscribe
805
1904
  # @note module_function: defines #resolve_lvar_rbs (visibility: private)
806
1905
  # @param [Parser::AST::Node?] recv the receiver node of the send
807
1906
  # @param [Symbol] meth the method name being called
808
- # @param [Object, nil] core_rbs_provider core RBS type lookup provider
809
- # @param [Hash<Object, Object>, nil] local_var_types pre-built local variable types map
810
- # @param [Hash<String, String>, nil] param_types parameter name -> type map for lvar resolution
1907
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider core RBS type lookup provider
1908
+ # @param [Hash<String, String>?] local_var_types pre-built local variable types map
1909
+ # @param [Hash<String, String>?] param_types parameter name -> type map for lvar resolution
811
1910
  # @return [String, nil]
812
1911
  def resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
813
1912
  lvar_name = recv&.children&.first
@@ -821,12 +1920,17 @@ module Docscribe
821
1920
  # Look up a local variable's inferred type from local or parameter type maps.
822
1921
  #
823
1922
  # @note module_function: defines #lookup_lvar_type (visibility: private)
824
- # @param [Object] lvar_name the local variable name
825
- # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
826
- # @param [Hash<String, String>, nil] param_types parameter name to type map
1923
+ # @param [String, Symbol, nil] lvar_name the local variable name
1924
+ # @param [Hash<String, String>?] local_var_types inferred local variable type map
1925
+ # @param [Hash<String, String>?] param_types parameter name to type map
827
1926
  # @return [String, nil]
828
1927
  def lookup_lvar_type(lvar_name, local_var_types, param_types)
829
- return local_var_types[lvar_name.to_s] if local_var_types&.key?(lvar_name.to_s)
1928
+ if local_var_types&.key?(lvar_name.to_s)
1929
+ val = local_var_types[lvar_name.to_s]
1930
+ return nil if val == FALLBACK_TYPE
1931
+
1932
+ return val
1933
+ end
830
1934
  return param_types[lvar_name.to_s] if param_types&.key?(lvar_name.to_s)
831
1935
 
832
1936
  nil
@@ -837,9 +1941,9 @@ module Docscribe
837
1941
  # @note module_function: defines #resolve_chained_send_rbs (visibility: private)
838
1942
  # @param [Parser::AST::Node?] recv the receiver node of the send
839
1943
  # @param [Symbol] meth the method name being called
840
- # @param [Object, nil] core_rbs_provider core RBS type lookup provider
841
- # @param [Hash<Object, Object>, nil] local_var_types pre-built local variable types map
842
- # @param [Hash<String, String>, nil] param_types parameter name -> type map for lvar resolution
1944
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider core RBS type lookup provider
1945
+ # @param [Hash<String, String>?] local_var_types pre-built local variable types map
1946
+ # @param [Hash<String, String>?] param_types parameter name -> type map for lvar resolution
843
1947
  # @return [String, nil]
844
1948
  def resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
845
1949
  inner_type = run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
@@ -863,7 +1967,7 @@ module Docscribe
863
1967
  #
864
1968
  # @note module_function: defines #last_expr_type (visibility: private)
865
1969
  # @param [Parser::AST::Node, nil] node expression node
866
- # @param [Object] opts additional keyword options forwarded to type inference
1970
+ # @param [Hash] opts additional keyword options forwarded to type inference
867
1971
  # @return [String, nil]
868
1972
  def last_expr_type(node, **opts)
869
1973
  run_last_expr_type(node, **opts)
@@ -873,7 +1977,7 @@ module Docscribe
873
1977
  #
874
1978
  # @note module_function: defines #run_last_expr_type (visibility: private)
875
1979
  # @param [Parser::AST::Node, nil] node the `:return` AST node
876
- # @param [Object] opts options passed through as keyword args
1980
+ # @param [Hash] opts options passed through as keyword args
877
1981
  # @return [String, nil]
878
1982
  def run_last_expr_type(node, **opts)
879
1983
  return unless node
@@ -891,18 +1995,181 @@ module Docscribe
891
1995
  #
892
1996
  # @note module_function: defines #handle_return_node (visibility: private)
893
1997
  # @param [Parser::AST::Node] node the `:return` AST node
894
- # @param [Object] opts additional keyword options forwarded to type inference
1998
+ # @param [Hash] opts additional keyword options forwarded to type inference
895
1999
  # @return [String, nil]
896
2000
  def handle_return_node(node, **opts)
897
2001
  Literals.type_from_literal(node.children.first, fallback_type: opts[:fallback_type])
898
2002
  end
899
2003
 
2004
+ # @note module_function: defines #handle_const_node (visibility: private)
2005
+ # @param [Parser::AST::Node] node the `:const` AST node
2006
+ # @param [Hash] opts additional keyword options forwarded to type inference
2007
+ # @return [String, nil]
2008
+ def handle_const_node(node, **opts)
2009
+ fallback = (opts[:fallback_type] || FALLBACK_TYPE).to_s #: String
2010
+ const_name = node.children.last.to_s #: String
2011
+ resolved = Literals.type_from_literal(node, fallback_type: fallback)
2012
+ return fallback if fallback_alias?(resolved, fallback)
2013
+ return fallback if fallback_alias?(const_name, fallback)
2014
+
2015
+ resolved
2016
+ end
2017
+
2018
+ # Resolve a const node to the YARD type of its runtime value.
2019
+ #
2020
+ # Looks the constant up in this process through the analyzed lexical
2021
+ # scope (innermost container outward, then top level) and maps its
2022
+ # value's class to a type name. Only constants actually present here
2023
+ # resolve — user code is never loaded, so unknown names safely fall
2024
+ # through to the fallback path. Classes and modules are skipped so
2025
+ # references like `String` keep name-based inference.
2026
+ #
2027
+ # @note module_function: defines #resolve_const_value_type (visibility: private)
2028
+ # @param [Parser::AST::Node] node the `:const` node
2029
+ # @param [String, nil] container lexical container (e.g. "Foo::Bar")
2030
+ # @return [String, nil] YARD type of the value, or nil when unresolvable
2031
+ def resolve_const_value_type(node, container)
2032
+ segments, absolute = const_path_segments(node)
2033
+ return nil if segments.empty?
2034
+
2035
+ *scope_parts, const_name = segments
2036
+ const_owner_paths(container, scope_parts, absolute).each do |owner_path|
2037
+ found, value = runtime_const_lookup(owner_path, const_name)
2038
+ next unless found
2039
+
2040
+ type = yard_type_for_const_value(value)
2041
+ return type if type
2042
+ end
2043
+ nil
2044
+ end
2045
+
2046
+ # Split a const node into static path segments and absoluteness.
2047
+ #
2048
+ # @note module_function: defines #const_path_segments (visibility: private)
2049
+ # @param [Parser::AST::Node] node the `:const` node
2050
+ # @return [(Array<String>, Boolean), nil] segments with last element
2051
+ # being the constant name plus absolute flag, or nil when dynamic
2052
+ def const_path_segments(node)
2053
+ parts = [] #: Array[String]
2054
+ current = node
2055
+ while current.is_a?(Parser::AST::Node) && current.type == :const
2056
+ parts.unshift(current.children[1].to_s)
2057
+ current = current.children[0]
2058
+ end
2059
+ return nil if current && !(current.is_a?(Parser::AST::Node) && current.type == :cbase)
2060
+
2061
+ absolute = !current.nil?
2062
+ [parts, absolute]
2063
+ end
2064
+
2065
+ # Candidate owner paths for a constant lookup, innermost first.
2066
+ #
2067
+ # Flat strings keep generic inference (and RubyMine) happy — nested
2068
+ # arrays lose a level (`first(n)` degrades to Elem).
2069
+ #
2070
+ # @note module_function: defines #const_owner_paths (visibility: private)
2071
+ # @param [String, nil] container lexical container (e.g. "Foo::Bar")
2072
+ # @param [Array<String>] scope_parts static scope segments (may be empty)
2073
+ # @param [Boolean] absolute whether the reference starts with `::`
2074
+ # @return [Array<String>] owner paths, "" means top level
2075
+ def const_owner_paths(container, scope_parts, absolute)
2076
+ return [scope_parts.join('::')] if absolute
2077
+
2078
+ segments = container.to_s.split('::').grep(/\A[A-Z]\w*\z/)
2079
+ # NOTE: `take` (not `first`) keeps RBS generic inference precise —
2080
+ # `first(n)` loses one nesting level.
2081
+ paths = segments.length.downto(1).map { |n| (segments.take(n) + scope_parts).join('::') }
2082
+ paths << scope_parts.join('::')
2083
+ paths
2084
+ end
2085
+
2086
+ # Look up a constant in this process without side effects.
2087
+ #
2088
+ # @note module_function: defines #runtime_const_lookup (visibility: private)
2089
+ # @param [String] owner_path owner namespace path ("" is top level)
2090
+ # @param [String] const_name constant name to look up
2091
+ # @raise [StandardError]
2092
+ # @return [(Boolean, Object)] found flag with value (nil value is valid)
2093
+ # @return [Array] if StandardError
2094
+ def runtime_const_lookup(owner_path, const_name)
2095
+ owner = owner_path.empty? ? Object : safe_const_path(owner_path.split('::'))
2096
+ return [false, nil] unless owner.is_a?(Module)
2097
+ return [false, nil] unless owner.const_defined?(const_name, false)
2098
+ return [false, nil] if owner.autoload?(const_name)
2099
+
2100
+ [true, owner.const_get(const_name, false)]
2101
+ rescue StandardError
2102
+ [false, nil]
2103
+ end
2104
+
2105
+ # Resolve a namespace path in this process without side effects.
2106
+ #
2107
+ # @note module_function: defines #safe_const_path (visibility: private)
2108
+ # @param [Array<String>] parts namespace segments
2109
+ # @return [Module, nil] the namespace or nil when unresolvable
2110
+ def safe_const_path(parts)
2111
+ mod = Object #: Module
2112
+ parts.each do |name|
2113
+ fetched = safe_const_step(mod, name)
2114
+ return nil unless fetched
2115
+
2116
+ mod = fetched
2117
+ end
2118
+ mod
2119
+ end
2120
+
2121
+ # Resolve one namespace step without side effects.
2122
+ #
2123
+ # @note module_function: defines #safe_const_step (visibility: private)
2124
+ # @param [Module] mod current namespace
2125
+ # @param [String] name nested constant name
2126
+ # @raise [StandardError]
2127
+ # @return [Module, nil] nested namespace or nil when unresolvable
2128
+ # @return [nil] if StandardError
2129
+ def safe_const_step(mod, name)
2130
+ return nil unless mod.const_defined?(name, false) && !mod.autoload?(name)
2131
+
2132
+ fetched = mod.const_get(name, false)
2133
+ fetched if fetched.is_a?(Module)
2134
+ rescue StandardError
2135
+ nil
2136
+ end
2137
+
2138
+ # Map a constant's runtime value to a YARD type name.
2139
+ #
2140
+ # @note module_function: defines #yard_type_for_const_value (visibility: private)
2141
+ # @param [Object] value the constant's runtime value
2142
+ # @return [String, nil] YARD type or nil when not mappable
2143
+ def yard_type_for_const_value(value)
2144
+ return 'nil' if value.nil?
2145
+ return 'Boolean' if value.is_a?(TrueClass) || value.is_a?(FalseClass)
2146
+ return nil if value.is_a?(Module)
2147
+
2148
+ name = value.class.name
2149
+ return nil unless name.is_a?(String) && name.match?(/\A[A-Z][A-Za-z0-9_:]*\z/)
2150
+
2151
+ name
2152
+ end
2153
+
2154
+ # Whether a type string is the fallback alias (FALLBACK_TYPE or the configured fallback type).
2155
+ #
2156
+ # @note module_function: defines #fallback_alias? (visibility: private)
2157
+ # @param [String, nil] type_str the type string to check
2158
+ # @param [String] fallback_type the configured fallback type
2159
+ # @return [Boolean]
2160
+ def fallback_alias?(type_str, fallback_type)
2161
+ return false if type_str.nil?
2162
+
2163
+ s = type_str.to_s.strip.delete_suffix('?').strip
2164
+ s == fallback_type || s == 'FALLBACK_TYPE' || (fallback_type == 'Object' && s == 'untyped')
2165
+ end
2166
+
900
2167
  # Resolve an RBS return type for a method call.
901
2168
  #
902
2169
  # @note module_function: defines #resolve_rbs_return_type (visibility: private)
903
2170
  # @param [String] container_type class or module name
904
2171
  # @param [String, Symbol] method_name method name
905
- # @param [Object, nil] core_rbs_provider core RBS type lookup provider
2172
+ # @param [Docscribe::Types::RBS::Provider?] core_rbs_provider core RBS type lookup provider
906
2173
  # @return [String] inferred return type
907
2174
  def resolve_rbs_return_type(container_type, method_name, core_rbs_provider)
908
2175
  return FALLBACK_TYPE unless core_rbs_provider
@@ -916,6 +2183,187 @@ module Docscribe
916
2183
  sig&.return_type || FALLBACK_TYPE
917
2184
  end
918
2185
 
2186
+ # Substitute `self` and generic type variables in an RBS return type with the concrete receiver type.
2187
+ #
2188
+ # Handles `Array#<<` (`self` -> `Array<Elem>`) and `Hash#[]` (`V` -> value type).
2189
+ # For `self` returns the concrete receiver type; for `K`/`V`/`Elem` substitutes from generic args.
2190
+ #
2191
+ # @note module_function: defines #substitute_rbs_type (visibility: private)
2192
+ # @param [String] rbs the raw RBS return type string
2193
+ # @param [String] recv_type the concrete receiver type string
2194
+ # @return [String]
2195
+ def substitute_rbs_type(rbs, recv_type)
2196
+ self_sub = substitute_self_type(rbs, recv_type)
2197
+ return self_sub if self_sub
2198
+
2199
+ inner = extract_generic_inner(recv_type)
2200
+ return rbs unless inner
2201
+
2202
+ args = split_generic_args(inner)
2203
+ return rbs if args.empty?
2204
+
2205
+ substitute_with_mapping(rbs, recv_type, args)
2206
+ end
2207
+
2208
+ # @note module_function: defines #substitute_self_type (visibility: private)
2209
+ # @param [String] rbs
2210
+ # @param [String] recv_type
2211
+ # @return [String, nil]
2212
+ def substitute_self_type(rbs, recv_type)
2213
+ return recv_type if rbs == 'self'
2214
+ return "#{recv_type}?" if rbs == 'self?'
2215
+
2216
+ nil
2217
+ end
2218
+
2219
+ # @note module_function: defines #substitute_with_mapping (visibility: private)
2220
+ # @param [String] rbs
2221
+ # @param [String] recv_type
2222
+ # @param [Array<String>] args
2223
+ # @return [String]
2224
+ def substitute_with_mapping(rbs, recv_type, args)
2225
+ mapping = build_generic_mapping(recv_type, args)
2226
+ return rbs if mapping.empty?
2227
+
2228
+ apply_generic_mapping(rbs, mapping, recv_type)
2229
+ end
2230
+
2231
+ # @note module_function: defines #build_generic_mapping (visibility: private)
2232
+ # @param [String] recv_type
2233
+ # @param [Array<String>] args
2234
+ # @return [Hash<String, String>]
2235
+ def build_generic_mapping(recv_type, args)
2236
+ base = recv_type.split(/[<\[ ]/).first.to_s.strip
2237
+ mapping = {} #: Hash[String, String]
2238
+ fill_mapping_for_base(mapping, base, args)
2239
+ mapping
2240
+ end
2241
+
2242
+ # @note module_function: defines #fill_mapping_for_base (visibility: private)
2243
+ # @param [Hash<String, String>] mapping
2244
+ # @param [String] base
2245
+ # @param [Array<String>] args
2246
+ # @return [void]
2247
+ def fill_mapping_for_base(mapping, base, args)
2248
+ case base
2249
+ when 'Hash' then fill_hash_mapping(mapping, args)
2250
+ when 'Array', 'Set', 'Enumerable', 'Enumerator' then fill_array_mapping(mapping, args)
2251
+ else fill_other_mapping(mapping, args)
2252
+ end
2253
+ end
2254
+
2255
+ # @note module_function: defines #fill_hash_mapping (visibility: private)
2256
+ # @param [Hash<String, String>] mapping
2257
+ # @param [Array<String>] args
2258
+ # @return [void]
2259
+ def fill_hash_mapping(mapping, args)
2260
+ mapping['K'] = args[0] if args[0]
2261
+ mapping['V'] = args[1] if args[1]
2262
+ end
2263
+
2264
+ # @note module_function: defines #fill_array_mapping (visibility: private)
2265
+ # @param [Hash<String, String>] mapping
2266
+ # @param [Array<String>] args
2267
+ # @return [void]
2268
+ def fill_array_mapping(mapping, args)
2269
+ %w[Elem T U E].each { |key| mapping[key] = args[0] if args[0] }
2270
+ end
2271
+
2272
+ # @note module_function: defines #fill_other_mapping (visibility: private)
2273
+ # @param [Hash<String, String>] mapping
2274
+ # @param [Array<String>] args
2275
+ # @return [void]
2276
+ def fill_other_mapping(mapping, args)
2277
+ %w[Elem T U].each { |key| mapping[key] = args[0] if args[0] }
2278
+ end
2279
+
2280
+ # @note module_function: defines #apply_generic_mapping (visibility: private)
2281
+ # @param [String] rbs
2282
+ # @param [Hash<String, String>] mapping
2283
+ # @param [String] recv_type
2284
+ # @return [String]
2285
+ def apply_generic_mapping(rbs, mapping, recv_type)
2286
+ stripped = rbs.delete_suffix('?').strip
2287
+ optional = rbs.end_with?('?')
2288
+ return optional ? "#{mapping[stripped]}?" : mapping[stripped] if mapping.key?(stripped)
2289
+
2290
+ new_rbs = rbs.dup
2291
+ mapping.each { |var, val| new_rbs = new_rbs.gsub(/\b#{Regexp.escape(var)}\b/, val) }
2292
+ new_rbs.include?('self') ? new_rbs.gsub(/\bself\b/, recv_type) : new_rbs
2293
+ end
2294
+
2295
+ # Extract the inner generic args string from a receiver type like `Array<String>` or `Hash<Integer, String>`.
2296
+ #
2297
+ # @note module_function: defines #extract_generic_inner (visibility: private)
2298
+ # @param [String] type the concrete type string
2299
+ # @return [String, nil]
2300
+ def extract_generic_inner(type)
2301
+ return unless type =~ /\A(?:Array|Hash|Set|Enumerable)[<\[](.*)[>\]]\z/m || type =~ /\A[^<\[\]]+[<\[](.*)[>\]]\z/m
2302
+
2303
+ Regexp.last_match(1)
2304
+ end
2305
+
2306
+ # Split a generic inner string like `Integer, Array<(Integer, String)>` by top-level commas.
2307
+ #
2308
+ # Respects nesting of `< > [ ] ( )` so tuples are not split.
2309
+ #
2310
+ # @note module_function: defines #split_generic_args (visibility: private)
2311
+ # @param [String] inner the raw inner string
2312
+ # @return [Array<String>]
2313
+ def split_generic_args(inner)
2314
+ state = { parts: [], cur: +'', da: 0, db: 0, dp: 0 } #: Hash[Symbol, untyped]
2315
+ inner.each_char { |chr| split_process_char(chr, state, strip: true) }
2316
+ last = state[:cur].strip
2317
+ state[:parts] << last unless last.empty?
2318
+ state[:parts]
2319
+ end
2320
+
2321
+ # @note module_function: defines #split_process_char (visibility: private)
2322
+ # @param [String] chr single character
2323
+ # @param [Hash<Symbol, Object>] state mutable split state
2324
+ # @param [Boolean] strip whether to strip parts on comma
2325
+ # @return [void]
2326
+ def split_process_char(chr, state, strip:)
2327
+ case chr
2328
+ when '<', '>', '[', ']', '(', ')'
2329
+ split_handle_bracket(chr, state)
2330
+ when ','
2331
+ split_handle_comma(state, strip: strip)
2332
+ else
2333
+ state[:cur] << chr
2334
+ end
2335
+ end
2336
+
2337
+ # @note module_function: defines #split_handle_bracket (visibility: private)
2338
+ # @param [String] chr bracket character
2339
+ # @param [Hash<Symbol, Object>] state mutable split state
2340
+ # @return [void]
2341
+ def split_handle_bracket(chr, state)
2342
+ case chr
2343
+ when '<' then state[:da] += 1
2344
+ when '>' then state[:da] -= 1
2345
+ when '[' then state[:db] += 1
2346
+ when ']' then state[:db] -= 1
2347
+ when '(' then state[:dp] += 1
2348
+ when ')' then state[:dp] -= 1
2349
+ end
2350
+ state[:cur] << chr
2351
+ end
2352
+
2353
+ # @note module_function: defines #split_handle_comma (visibility: private)
2354
+ # @param [Hash<Symbol, Object>] state mutable split state
2355
+ # @param [Boolean] strip whether to strip
2356
+ # @return [void]
2357
+ def split_handle_comma(state, strip:)
2358
+ if state[:da].zero? && state[:db].zero? && state[:dp].zero?
2359
+ part = strip ? state[:cur].strip : state[:cur]
2360
+ state[:parts] << part
2361
+ state[:cur] = +''
2362
+ else
2363
+ state[:cur] << ','
2364
+ end
2365
+ end
2366
+
919
2367
  # Unify two inferred types into a single type string.
920
2368
  #
921
2369
  # Rules:
@@ -930,13 +2378,24 @@ module Docscribe
930
2378
  # @param [Boolean] nil_as_optional whether to render nil unions as optional types
931
2379
  # @return [String]
932
2380
  def unify_types(type_a, type_b, fallback_type:, nil_as_optional:)
933
- type_a ||= fallback_type
934
- type_b ||= fallback_type
2381
+ type_a = coalesce_type(type_a, fallback_type)
2382
+ type_b = coalesce_type(type_b, fallback_type)
935
2383
  return type_a if type_a == type_b
936
2384
 
937
2385
  unify_nil_types(type_a, type_b, nil_as_optional: nil_as_optional)
938
2386
  end
939
2387
 
2388
+ # @note module_function: defines #coalesce_type (visibility: private)
2389
+ # @param [String, nil] type
2390
+ # @param [String] fallback_type
2391
+ # @return [String]
2392
+ def coalesce_type(type, fallback_type)
2393
+ normalized = type || fallback_type
2394
+ normalized = fallback_type if normalized == 'FALLBACK_TYPE'
2395
+ normalized = 'Object' if normalized == 'untyped' && fallback_type == 'Object'
2396
+ normalized
2397
+ end
2398
+
940
2399
  # Unify two types where one may be `nil`, producing optional or union type.
941
2400
  #
942
2401
  # @note module_function: defines #unify_nil_types (visibility: private)
@@ -947,6 +2406,8 @@ module Docscribe
947
2406
  def unify_nil_types(type_a, type_b, nil_as_optional:)
948
2407
  if type_a == 'nil' || type_b == 'nil'
949
2408
  non_nil = (type_a == 'nil' ? type_b : type_a)
2409
+ return non_nil if non_nil.end_with?('?')
2410
+
950
2411
  return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil"
951
2412
  end
952
2413