Module: Docscribe::Infer::Returns

Defined in:
lib/docscribe/infer/returns.rb

Overview

Return type inference and rescue-conditional return extraction.

Constant Summary collapse

LITERAL_RBS_TYPES =
Note:

module_function: when included, also defines #receiver_rbs_type_name (instance visibility: private)

Map a receiver AST node to its RBS type name string.

Supports local variables, method calls, literals, and instance/global/class variables.

Returns:

  • (String, nil)
{
  int: 'Integer', str: 'String', sym: 'Symbol', true: 'Boolean',
  false: 'Boolean', float: 'Float', array: 'Array', hash: 'Hash',
  nil: 'NilClass'
}.freeze

Class Method Summary collapse

Class Method Details

.assignment_name_and_value(node) ⇒ (String, nil, Parser::AST::Node, nil)

Note:

module_function: defines #assignment_name_and_value (visibility: private)

Extract the variable name and value expression from an assignment node.

Parameters:

  • node (Parser::AST::Node)

    an assignment AST node (:lvasgn, :gvasgn, :ivasgn, :casgn, :op_asgn)

Returns:

  • ((String, nil, Parser::AST::Node, nil))


202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/docscribe/infer/returns.rb', line 202

def assignment_name_and_value(node)
  case node.type
  when :lvasgn, :gvasgn, :ivasgn, :cvasgn
    [node.children[0].to_s, node.children[1]]
  when :casgn
    constant_name_and_value(node)
  when :op_asgn
    compound_name_and_value(node)
  else
    [nil, nil]
  end
end

.build_local_variable_types(node, **opts) ⇒ Hash<String, String>?

Note:

module_function: defines #build_local_variable_types (visibility: private)

Build a map of local/global/ivar/constant assignments to inferred types.

Parameters:

  • node (Parser::AST::Node)

    AST node to walk

  • opts (Object)

    additional keyword options forwarded to inference

Returns:

  • (Hash<String, String>, nil)


165
166
167
168
169
170
171
# File 'lib/docscribe/infer/returns.rb', line 165

def build_local_variable_types(node, **opts)
  types = {} #: Hash[String, String]
  ASTWalk.walk(node) do |n|
    collect_assignment_type(n, types, **opts)
  end
  types.empty? ? nil : types
end

.collect_assignment_type(node, types, **opts) ⇒ void

Note:

module_function: defines #collect_assignment_type (visibility: private)

This method returns an undefined value.

Infer the type of a single assignment node and store it in the types hash.

Uses run_last_expr_type when core_rbs_provider is available to resolve send expressions (e.g., x = 123 + 1 -> Integer). Falls back to Literals.type_from_literal for plain literals.

Parameters:

  • node (Parser::AST::Node)

    an assignment AST node

  • types (Hash<String, String>)

    the accumulated local variable type map

  • opts (Object)

    additional keyword options forwarded to inference



184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/docscribe/infer/returns.rb', line 184

def collect_assignment_type(node, types, **opts)
  name, value = assignment_name_and_value(node)
  return unless name && value

  inferred = if opts[:core_rbs_provider]
               run_last_expr_type(value, **opts, fallback_type: FALLBACK_TYPE,
                                                 nil_as_optional: false, local_var_types: types)
             else
               Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
             end
  types[name] = inferred if inferred && inferred != FALLBACK_TYPE
end

.collect_rescue_branches(node, **opts) ⇒ Array<String, nil>

Note:

module_function: defines #collect_rescue_branches (visibility: private)

Handle :rescue node for last_expr_type.

Unifies the body type with all rescue handler types and the optional else clause. Collect all rescue branch return types from a :rescue AST node.

Parameters:

  • node (Parser::AST::Node)

    the :rescue AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (Array<String, nil>)


467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/docscribe/infer/returns.rb', line 467

def collect_rescue_branches(node, **opts)
  branches = [run_last_expr_type(node.children[0], **opts)]
  (node.children[1..] || []).each do |child|
    if child.is_a?(Parser::AST::Node) && child.type == :resbody
      handler = child.children[2]
      branches << run_last_expr_type(handler, **opts) if handler
    else
      branches << run_last_expr_type(child, **opts)
    end
  end
  branches
end

.compound_name_and_value(node) ⇒ (String, nil, Parser::AST::Node, nil)

Note:

module_function: defines #compound_name_and_value (visibility: private)

Extract the name and value from an :op_asgn (compound assignment) node.

Parameters:

  • node (Parser::AST::Node)

    the :op_asgn AST node

Returns:

  • ((String, nil, Parser::AST::Node, nil))


229
230
231
# File 'lib/docscribe/infer/returns.rb', line 229

def compound_name_and_value(node)
  [node.children[0].children.first.to_s, node.children[2]]
end

.constant_name_and_value(node) ⇒ (String, nil, Parser::AST::Node, nil)

Note:

module_function: defines #constant_name_and_value (visibility: private)

Extract the name and value from a :casgn (constant assignment) node.

Parameters:

  • node (Parser::AST::Node)

    the :casgn AST node

Returns:

  • ((String, nil, Parser::AST::Node, nil))


220
221
222
# File 'lib/docscribe/infer/returns.rb', line 220

def constant_name_and_value(node)
  [node.children[0].to_s, node.children[2]]
end

.container_rbs_return_type(meth, **opts) ⇒ String?

Note:

module_function: defines #container_rbs_return_type (visibility: private)

Resolve return type from the current method's container via RBS.

Handles implicit self calls (recv is nil) by looking up the method on the container class.

Parameters:

  • meth (Symbol)

    the method name being called

  • opts (Object)

    additional keyword options (must include :container and :core_rbs_provider)

Returns:

  • (String, nil)

    resolved type or nil if unresolvable



711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
# File 'lib/docscribe/infer/returns.rb', line 711

def container_rbs_return_type(meth, **opts)
  return unless opts[:container]

  if opts[:core_rbs_provider]
    rbs = resolve_rbs_return_type(opts[:container], meth, opts[:core_rbs_provider])
    return rbs unless rbs == FALLBACK_TYPE
  end

  if opts[:signature_provider]
    sig = opts[:signature_provider].signature_for(container: opts[:container], scope: :instance, name: meth)
    return sig.return_type if sig
  end

  nil
end

.extract_def_body(node) ⇒ Parser::AST::Node?

Note:

module_function: defines #extract_def_body (visibility: private)

Extract the body child node from a :def or :defs AST node.

Parameters:

  • node (Parser::AST::Node)

    a :def or :defs AST node

Returns:

  • (Parser::AST::Node, nil)


91
92
93
94
95
96
# File 'lib/docscribe/infer/returns.rb', line 91

def extract_def_body(node)
  case node.type
  when :def then node.children[2]
  when :defs then node.children[3]
  end
end

.handle_and_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_and_node (visibility: private)

Handle :and node (a && b) for last_expr_type.

The result type is the union of both sides, since either may be returned depending on the truthiness of the left operand.

Parameters:

  • node (Parser::AST::Node)

    the :and AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


421
422
423
424
425
426
# File 'lib/docscribe/infer/returns.rb', line 421

def handle_and_node(node, **opts)
  t = run_last_expr_type(node.children[0], **opts)
  e = run_last_expr_type(node.children[1], **opts)
  unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
                    nil_as_optional: opts.fetch(:nil_as_optional, true))
end

.handle_begin_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_begin_node (visibility: private)

Handle :begin node for last_expr_type.

Parameters:

  • node (Parser::AST::Node)

    the :return AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


357
358
359
# File 'lib/docscribe/infer/returns.rb', line 357

def handle_begin_node(node, **opts)
  run_last_expr_type(node.children.last, **opts)
end

.handle_block_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_block_node (visibility: private)

Handle :block node for last_expr_type.

Parameters:

  • node (Parser::AST::Node)

    the :return AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


611
612
613
614
615
616
617
618
619
# File 'lib/docscribe/infer/returns.rb', line 611

def handle_block_node(node, **opts)
  send_node = node.children[0]
  if send_node&.type == :send
    type = send_rbs_type(send_node.children[0], send_node.children[1], **opts)
    return type if type
  end

  run_last_expr_type(node.children[2], **opts)
end

.handle_case_match_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_case_match_node (visibility: private)

Handle :case_match node (case x; in pat; expr; end) for last_expr_type.

Similar to :case — unifies all in_pattern branch types and the optional else clause.

Parameters:

  • node (Parser::AST::Node)

    the :case_match AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


553
554
555
556
557
558
559
560
561
562
563
# File 'lib/docscribe/infer/returns.rb', line 553

def handle_case_match_node(node, **opts)
  branches = process_pattern_branches(node, **opts)
  if branches.empty?
    opts[:fallback_type]
  else
    branches.reduce do |a, b|
      unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
                        nil_as_optional: opts.fetch(:nil_as_optional, true))
    end
  end
end

.handle_case_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_case_node (visibility: private)

Handle :case node for last_expr_type.

Parameters:

  • node (Parser::AST::Node)

    the :return AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


384
385
386
387
388
389
390
391
392
393
394
# File 'lib/docscribe/infer/returns.rb', line 384

def handle_case_node(node, **opts)
  branches = process_case_branches(node, **opts)
  if branches.empty?
    opts[:fallback_type]
  else
    branches.reduce do |a, b|
      unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
                        nil_as_optional: opts.fetch(:nil_as_optional, true))
    end
  end
end

.handle_cvar_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_cvar_node (visibility: private)

Handle :cvar node for last_expr_type — look up class variable in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :cvar AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


272
273
274
275
# File 'lib/docscribe/infer/returns.rb', line 272

def handle_cvar_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
end

.handle_cvasgn_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_cvasgn_node (visibility: private)

Handle :cvasgn node for last_expr_type — look up class var assignment in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :cvasgn AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


322
323
324
325
326
327
# File 'lib/docscribe/infer/returns.rb', line 322

def handle_cvasgn_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) ||
    run_last_expr_type(node.children[1], **opts) ||
    opts[:fallback_type]
end

.handle_defined_node(_node, **opts) ⇒ String?

Note:

module_function: defines #handle_defined_node (visibility: private)

Handle :defined? node (defined?(expr)) for last_expr_type.

Returns nil if the expression is not defined, or a String description if it is defined. The union type is String?.

Parameters:

  • _node (Parser::AST::Node)

    the :defined? AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


501
502
503
504
# File 'lib/docscribe/infer/returns.rb', line 501

def handle_defined_node(_node, **opts)
  nil_as_optional = opts.fetch(:nil_as_optional, true)
  nil_as_optional ? 'String?' : 'String, nil'
end

.handle_ensure_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_ensure_node (visibility: private)

Handle :ensure node (begin; expr; ensure; cleanup; end) for last_expr_type.

The ensure clause's result is discarded by Ruby; only the body type is returned.

Parameters:

  • node (Parser::AST::Node)

    the :ensure AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


488
489
490
# File 'lib/docscribe/infer/returns.rb', line 488

def handle_ensure_node(node, **opts)
  run_last_expr_type(node.children[0], **opts)
end

.handle_gvar_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_gvar_node (visibility: private)

Handle :gvar node for last_expr_type — look up global variable in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :gvar AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


261
262
263
264
# File 'lib/docscribe/infer/returns.rb', line 261

def handle_gvar_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
end

.handle_gvasgn_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_gvasgn_node (visibility: private)

Handle :gvasgn node for last_expr_type — look up global var assignment in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :gvasgn AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


309
310
311
312
313
314
# File 'lib/docscribe/infer/returns.rb', line 309

def handle_gvasgn_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) ||
    run_last_expr_type(node.children[1], **opts) ||
    opts[:fallback_type]
end

.handle_if_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_if_node (visibility: private)

Handle :if node for last_expr_type.

Parameters:

  • node (Parser::AST::Node)

    the :return AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


367
368
369
370
371
372
373
374
375
376
# File 'lib/docscribe/infer/returns.rb', line 367

def handle_if_node(node, **opts)
  t = run_last_expr_type(node.children[1], **opts)
  e = if node.children[2]
        run_last_expr_type(node.children[2], **opts)
      else
        'nil'
      end
  unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
                    nil_as_optional: opts.fetch(:nil_as_optional, true))
end

.handle_in_pattern_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_in_pattern_node (visibility: private)

Handle :in_pattern node (pattern inside case...in) for last_expr_type.

Extracts the body expression from the pattern and recurses.

Parameters:

  • node (Parser::AST::Node)

    the :in_pattern AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


573
574
575
# File 'lib/docscribe/infer/returns.rb', line 573

def handle_in_pattern_node(node, **opts)
  run_last_expr_type(node.children[2], **opts)
end

.handle_ivar_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_ivar_node (visibility: private)

Handle :ivar node for last_expr_type — look up instance variable in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :ivar AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


250
251
252
253
# File 'lib/docscribe/infer/returns.rb', line 250

def handle_ivar_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
end

.handle_ivasgn_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_ivasgn_node (visibility: private)

Handle :ivasgn node for last_expr_type — look up ivar assignment in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :ivasgn AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


296
297
298
299
300
301
# File 'lib/docscribe/infer/returns.rb', line 296

def handle_ivasgn_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) ||
    run_last_expr_type(node.children[1], **opts) ||
    opts[:fallback_type]
end

.handle_kwbegin_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_kwbegin_node (visibility: private)

Handle :kwbegin node (begin; expr; end) for last_expr_type.

Unwraps the explicit begin node and delegates to the inner expression, which may be a :rescue or :ensure node.

Parameters:

  • node (Parser::AST::Node)

    the :kwbegin AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


437
438
439
# File 'lib/docscribe/infer/returns.rb', line 437

def handle_kwbegin_node(node, **opts)
  run_last_expr_type(node.children.first, **opts)
end

.handle_lvar_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_lvar_node (visibility: private)

Handle :lvar node for last_expr_type — look up the variable in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :lvar AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


239
240
241
242
# File 'lib/docscribe/infer/returns.rb', line 239

def handle_lvar_node(node, **opts)
  name = node.children[0].to_s
  lookup_lvar_type(name, opts[:local_var_types], opts[:param_types]) || opts[:fallback_type]
end

.handle_lvasgn_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_lvasgn_node (visibility: private)

Handle :lvasgn node for last_expr_type — look up local var assignment in local_var_types.

Parameters:

  • node (Parser::AST::Node)

    the :lvasgn AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


283
284
285
286
287
288
# File 'lib/docscribe/infer/returns.rb', line 283

def handle_lvasgn_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) ||
    run_last_expr_type(node.children[1], **opts) ||
    opts[:fallback_type]
end

.handle_op_asgn_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_op_asgn_node (visibility: private)

Handle :op_asgn node (compound assignment: x += 1, @var -= 2, etc.).

Infers the result type from the operator and the right operand's type. Uses RBS to resolve when available (e.g., Integer#+ -> Integer).

Parameters:

  • node (Parser::AST::Node)

    the :op_asgn AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/docscribe/infer/returns.rb', line 338

def handle_op_asgn_node(node, **opts)
  meth = node.children[1]
  return nil unless %i[+ - * / % ** << | & ^].include?(meth)
  return nil unless opts[:core_rbs_provider]

  arg = node.children[2]
  arg_type = type_from_literal_safe(arg)
  return nil unless arg_type

  rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
  rbs unless rbs == FALLBACK_TYPE
end

.handle_or_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_or_node (visibility: private)

Handle :or node (a || b) for last_expr_type.

The result type is the union of both sides, since either may be returned depending on the truthiness of the left operand.

Parameters:

  • node (Parser::AST::Node)

    the :or AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


405
406
407
408
409
410
# File 'lib/docscribe/infer/returns.rb', line 405

def handle_or_node(node, **opts)
  t = run_last_expr_type(node.children[0], **opts)
  e = run_last_expr_type(node.children[1], **opts)
  unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
                    nil_as_optional: opts.fetch(:nil_as_optional, true))
end

.handle_rescue_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_rescue_node (visibility: private)

Handle :rescue node for last_expr_type.

Supports both inline rescue (expr rescue default) and block rescue (begin; expr; rescue; e; end).

Parameters:

  • node (Parser::AST::Node)

    the :rescue AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


450
451
452
453
454
455
456
# File 'lib/docscribe/infer/returns.rb', line 450

def handle_rescue_node(node, **opts)
  branches = collect_rescue_branches(node, **opts)
  branches.reduce do |a, b|
    unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
                      nil_as_optional: opts.fetch(:nil_as_optional, true))
  end
end

.handle_return_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_return_node (visibility: private)

Extract the return type from an explicit :return node.

Parameters:

  • node (Parser::AST::Node)

    the :return AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


896
897
898
# File 'lib/docscribe/infer/returns.rb', line 896

def handle_return_node(node, **opts)
  Literals.type_from_literal(node.children.first, fallback_type: opts[:fallback_type])
end

.handle_send_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_send_node (visibility: private)

Handle :send node for last_expr_type.

Parameters:

  • node (Parser::AST::Node)

    the :return AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


627
628
629
630
631
632
633
634
635
636
637
638
# File 'lib/docscribe/infer/returns.rb', line 627

def handle_send_node(node, **opts)
  recv = node.children[0]
  meth = node.children[1]

  rbs_type = send_rbs_type(recv, meth, **opts) if opts[:core_rbs_provider]
  return rbs_type if rbs_type

  compound_type = infer_from_compound_assign(node, **opts)
  return compound_type if compound_type

  Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
end

.handle_super_node(_node, **opts) ⇒ String?

Note:

module_function: defines #handle_super_node (visibility: private)

Handle :super node (super(args)) for last_expr_type.

Returns the super method's return type if resolvable via RBS, or the fallback type otherwise.

Parameters:

  • _node (Parser::AST::Node)

    the :super AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


528
529
530
# File 'lib/docscribe/infer/returns.rb', line 528

def handle_super_node(_node, **opts)
  opts[:fallback_type]
end

.handle_yield_node(_node, **opts) ⇒ String?

Note:

module_function: defines #handle_yield_node (visibility: private)

Handle :yield node (yield / yield(args)) for last_expr_type.

Returns the block's return type if resolvable via RBS (Proc#call), or the fallback type otherwise.

Parameters:

  • _node (Parser::AST::Node)

    the :yield AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


541
542
543
# File 'lib/docscribe/infer/returns.rb', line 541

def handle_yield_node(_node, **opts)
  opts[:fallback_type]
end

.handle_zsuper_node(_node, **opts) ⇒ String?

Note:

module_function: defines #handle_zsuper_node (visibility: private)

Handle :zsuper node (super with no arguments) for last_expr_type.

Returns the super method's return type if resolvable via RBS, or the fallback type otherwise.

Parameters:

  • _node (Parser::AST::Node)

    the :zsuper AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


515
516
517
# File 'lib/docscribe/infer/returns.rb', line 515

def handle_zsuper_node(_node, **opts)
  opts[:fallback_type]
end

.infer_from_compound_assign(node, **opts) ⇒ String?

Note:

module_function: defines #infer_from_compound_assign (visibility: private)

Infer return type from a compound-assignment-like :send by reading the first literal argument's type — only fires when core_rbs_provider is present and the argument's RBS return type can be resolved.

Enables @var += 123 -> Integer (via Integer#+) and similar patterns.

Parameters:

  • node (Parser::AST::Node)

    the :send AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
# File 'lib/docscribe/infer/returns.rb', line 774

def infer_from_compound_assign(node, **opts)
  return nil unless opts[:core_rbs_provider]

  meth = node.children[1]
  return nil unless %i[+ - * / % ** << | & ^].include?(meth)

  first_arg = node.children[2]
  return nil unless first_arg

  arg_type = type_from_literal_safe(first_arg)
  return nil unless arg_type

  rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
  rbs unless rbs == FALLBACK_TYPE
end

.infer_normal_return_type(body, **opts) ⇒ String

Note:

module_function: defines #infer_normal_return_type (visibility: private)

Infer the normal (non-rescue) return type from a method body node.

Parameters:

  • body (Parser::AST::Node)

    the method body AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String)


120
121
122
# File 'lib/docscribe/infer/returns.rb', line 120

def infer_normal_return_type(body, **opts)
  run_last_expr_type(body, **opts) || FALLBACK_TYPE
end

.infer_return_type(method_source) ⇒ String, FALLBACK_TYPE

Note:

module_function: defines #infer_return_type (visibility: private)

Infer a return type from a full method definition source string.

The source must parse to a :def or :defs node. If parsing fails or inference is uncertain, the fallback type is returned.

Parameters:

  • method_source (String?)

    full method definition source

Returns:

Raises:

  • (Parser::SyntaxError)


19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/docscribe/infer/returns.rb', line 19

def infer_return_type(method_source)
  return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty?

  root = parse_method_source(method_source)
  return FALLBACK_TYPE unless root && %i[def defs].include?(root.type)

  body = root.children.last
  local_var_types = build_local_variable_types(body)
  run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
                           local_var_types: local_var_types) || FALLBACK_TYPE
rescue Parser::SyntaxError # steep:ignore
  FALLBACK_TYPE
end

.infer_return_type_from_node(node) ⇒ String

Note:

module_function: defines #infer_return_type_from_node (visibility: private)

Infer a method's normal return type from an already parsed def/defs node.

Parameters:

  • node (Parser::AST::Node)

    :def or :defs node

Returns:

  • (String)


49
50
51
52
53
54
55
56
# File 'lib/docscribe/infer/returns.rb', line 49

def infer_return_type_from_node(node)
  body = extract_def_body(node)
  return FALLBACK_TYPE unless body

  local_var_types = build_local_variable_types(body)
  run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
                           local_var_types: local_var_types) || FALLBACK_TYPE
end

.last_expr_type(node, **opts) ⇒ String?

Note:

module_function: defines #last_expr_type (visibility: private)

Infer the type of the last expression in a node.

Supports:

  • begin groups
  • if branches
  • case expressions
  • explicit return
  • literal-like expressions via Literals.type_from_literal
  • method calls with RBS core type lookup

Parameters:

  • node (Parser::AST::Node, nil)

    expression node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


868
869
870
# File 'lib/docscribe/infer/returns.rb', line 868

def last_expr_type(node, **opts)
  run_last_expr_type(node, **opts)
end

.lookup_lvar_type(lvar_name, local_var_types, param_types) ⇒ String?

Note:

module_function: defines #lookup_lvar_type (visibility: private)

Look up a local variable's inferred type from local or parameter type maps.

Parameters:

  • lvar_name (Object)

    the local variable name

  • local_var_types (Hash<Object, Object>, nil)

    inferred local variable type map

  • param_types (Hash<String, String>, nil)

    parameter name to type map

Returns:

  • (String, nil)


828
829
830
831
832
833
# File 'lib/docscribe/infer/returns.rb', line 828

def lookup_lvar_type(lvar_name, local_var_types, param_types)
  return local_var_types[lvar_name.to_s] if local_var_types&.key?(lvar_name.to_s)
  return param_types[lvar_name.to_s] if param_types&.key?(lvar_name.to_s)

  nil
end

.parse_method_source(method_source) ⇒ Parser::AST::Node?

Note:

module_function: defines #parse_method_source (visibility: private)

Parse a Ruby source string into an AST using the Parser gem.

Parameters:

  • method_source (String)

    the method definition source string to parse

Returns:

  • (Parser::AST::Node, nil)


38
39
40
41
42
# File 'lib/docscribe/infer/returns.rb', line 38

def parse_method_source(method_source)
  buffer = Parser::Source::Buffer.new('(method)')
  buffer.source = method_source
  Docscribe::Parsing.parse_buffer(buffer)
end

.populate_returns_spec(spec, body, local_var_types, **opts) ⇒ Object

Note:

module_function: defines #populate_returns_spec (visibility: private)

Populate the spec hash with normal and/or rescue return types from the body.

Parameters:

  • spec (Object)

    the return spec hash to populate

  • body (Parser::AST::Node)

    the method body AST node

  • local_var_types (Hash<Object, Object>, nil)

    inferred local variable type map

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (Object)


106
107
108
109
110
111
112
# File 'lib/docscribe/infer/returns.rb', line 106

def populate_returns_spec(spec, body, local_var_types, **opts)
  if body.type == :rescue
    process_rescue_body(spec, body, **opts)
  else
    spec[:normal] = infer_normal_return_type(body, **opts, local_var_types: local_var_types)
  end
end

.process_case_branches(node, **opts) ⇒ Array<String>

Note:

module_function: defines #process_case_branches (visibility: private)

Extract inferred return types from all branches of a :case expression.

Parameters:

  • node (Parser::AST::Node)

    the :case AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (Array<String>)

    list of inferred types from each branch



595
596
597
598
599
600
601
602
603
# File 'lib/docscribe/infer/returns.rb', line 595

def process_case_branches(node, **opts)
  (node.children[1..] || []).compact.flat_map do |child|
    if child.type == :when
      run_last_expr_type(child.children.last, **opts)
    else
      run_last_expr_type(child, **opts)
    end
  end.compact
end

.process_pattern_branches(node, **opts) ⇒ Array<String>

Note:

module_function: defines #process_pattern_branches (visibility: private)

Extract inferred return types from all in_pattern branches of a :case_match expression.

Parameters:

  • node (Parser::AST::Node)

    the :case_match AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (Array<String>)

    list of inferred types from each branch



583
584
585
586
587
# File 'lib/docscribe/infer/returns.rb', line 583

def process_pattern_branches(node, **opts)
  (node.children[1..] || []).compact.filter_map do |child|
    run_last_expr_type(child, **opts) if child.is_a?(Parser::AST::Node)
  end
end

.process_rescue_body(spec, body, **opts) ⇒ Object

Note:

module_function: defines #process_rescue_body (visibility: private)

Process a :rescue body node and populate spec with normal + rescue return types.

Parameters:

  • spec (Object)

    the return spec hash to populate

  • body (Parser::AST::Node)

    the :rescue AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (Object)


131
132
133
134
135
136
137
138
139
# File 'lib/docscribe/infer/returns.rb', line 131

def process_rescue_body(spec, body, **opts)
  main_body = body.children[0]
  local_var_types = build_local_variable_types(body,
                                               core_rbs_provider: opts[:core_rbs_provider],
                                               param_types: opts[:param_types])
  rescue_opts = opts.merge(local_var_types: local_var_types)
  spec[:normal] = run_last_expr_type(main_body, **rescue_opts) || FALLBACK_TYPE
  process_rescue_branches(spec, body, **rescue_opts)
end

.process_rescue_branches(spec, body, **opts) ⇒ Array<Object>

Note:

module_function: defines #process_rescue_branches (visibility: private)

Extract return types from each :resbody child and append to spec.

Parameters:

  • spec (Object)

    the return spec hash to populate

  • body (Parser::AST::Node)

    the :rescue AST node

  • opts (Object)

    additional keyword options forwarded to type inference

Returns:

  • (Array<Object>)

    the list of rescue type entries



148
149
150
151
152
153
154
155
156
157
# File 'lib/docscribe/infer/returns.rb', line 148

def process_rescue_branches(spec, body, **opts)
  body.children.each do |ch|
    next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody

    exc_list, _asgn, rescue_body = *ch
    exc_names = Raises.exception_names_from_rescue_list(exc_list)
    rtype = run_last_expr_type(rescue_body, **opts) || opts[:fallback_type]
    spec[:rescues] << [exc_names, rtype]
  end
end

.receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types) ⇒ String?

Note:

module_function: defines #receiver_rbs_type_name (visibility: private)

Map receiver AST node to RBS type name.

Parameters:

  • recv (Parser::AST::Node, nil)

    the receiver AST node

  • core_rbs_provider (Object, nil)

    core RBS type provider

  • local_var_types (Hash<Object, Object>, nil)

    inferred local variable types

  • param_types (Hash<String, String>, nil)

    parameter name-to-type map

Returns:

  • (String, nil)


751
752
753
754
755
756
757
758
759
760
761
762
# File 'lib/docscribe/infer/returns.rb', line 751

def receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
  return unless recv
  return LITERAL_RBS_TYPES[recv.type] if LITERAL_RBS_TYPES.key?(recv.type)
  return lookup_lvar_type(recv.children.first, local_var_types, param_types) if %i[lvar ivar gvar
                                                                                   cvar].include?(recv.type)
  return unless recv.type == :send

  run_last_expr_type(recv, fallback_type: FALLBACK_TYPE, nil_as_optional: false,
                           core_rbs_provider: core_rbs_provider,
                           param_types: param_types,
                           local_var_types: local_var_types)
end

.resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types) ⇒ String?

Note:

module_function: defines #resolve_chained_send_rbs (visibility: private)

Resolve RBS return type for a chained :send receiver.

Parameters:

  • recv (Parser::AST::Node?)

    the receiver node of the send

  • meth (Symbol)

    the method name being called

  • core_rbs_provider (Object, nil)

    core RBS type lookup provider

  • local_var_types (Hash<Object, Object>, nil)

    pre-built local variable types map

  • param_types (Hash<String, String>, nil)

    parameter name -> type map for lvar resolution

Returns:

  • (String, nil)


844
845
846
847
848
849
850
851
852
# File 'lib/docscribe/infer/returns.rb', line 844

def resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
  inner_type = run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
                                        core_rbs_provider: core_rbs_provider, param_types: param_types,
                                        local_var_types: local_var_types)
  return nil unless inner_type

  rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
  rbs_type unless rbs_type == FALLBACK_TYPE
end

.resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types) ⇒ String?

Note:

module_function: defines #resolve_lvar_rbs (visibility: private)

Resolve RBS return type for an :lvar receiver.

Parameters:

  • recv (Parser::AST::Node?)

    the receiver node of the send

  • meth (Symbol)

    the method name being called

  • core_rbs_provider (Object, nil)

    core RBS type lookup provider

  • local_var_types (Hash<Object, Object>, nil)

    pre-built local variable types map

  • param_types (Hash<String, String>, nil)

    parameter name -> type map for lvar resolution

Returns:

  • (String, nil)


812
813
814
815
816
817
818
819
# File 'lib/docscribe/infer/returns.rb', line 812

def resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
  lvar_name = recv&.children&.first
  recv_type = lookup_lvar_type(lvar_name, local_var_types, param_types)
  return nil unless recv_type

  rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
  rbs_type unless rbs_type == FALLBACK_TYPE
end

.resolve_rbs_for_send(recv, meth, core_rbs_provider, local_var_types, param_types) ⇒ String?

Note:

module_function: defines #resolve_rbs_for_send (visibility: private)

Resolve RBS return type for a send node's receiver, if possible.

Handles :lvar, chained :send, literal (:int, :str, etc.), and variable (:ivar, :gvar, :cvar) receivers.

Parameters:

  • recv (Parser::AST::Node, nil)

    the receiver node of the send

  • meth (Symbol)

    the method name being called

  • core_rbs_provider (Object, nil)

    optional RBS provider for core type lookup

  • local_var_types (Hash<Object, Object>, nil)

    inferred local variable type map

  • param_types (Hash<String, String>, nil)

    parameter name to type map

Returns:

  • (String, nil)

    resolved type or nil if unresolvable



671
672
673
674
675
676
677
678
679
680
681
# File 'lib/docscribe/infer/returns.rb', line 671

def resolve_rbs_for_send(recv, meth, core_rbs_provider, local_var_types, param_types)
  recv_type = receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
  return nil unless recv_type

  if core_rbs_provider
    rbs = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
    return rbs unless rbs == FALLBACK_TYPE
  end

  nil
end

.resolve_rbs_for_send_with_signature_provider(recv, meth, **opts) ⇒ String?

Note:

module_function: defines #resolve_rbs_for_send_with_signature_provider (visibility: private)

Resolve RBS return type via signature_provider fallback.

Tries project-level RBS when core_rbs_provider fails.

Parameters:

  • recv (Parser::AST::Node, nil)

    the receiver node

  • meth (Symbol)

    the method name

  • opts (Object)

    additional keyword options

Returns:

  • (String, nil)


692
693
694
695
696
697
698
699
700
# File 'lib/docscribe/infer/returns.rb', line 692

def resolve_rbs_for_send_with_signature_provider(recv, meth, **opts)
  return nil unless opts[:signature_provider]

  recv_type = receiver_rbs_type_name(recv, opts[:core_rbs_provider], opts[:local_var_types],
                                     opts[:param_types])
  return nil unless recv_type

  opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)&.return_type
end

.resolve_rbs_return_type(container_type, method_name, core_rbs_provider) ⇒ String

Note:

module_function: defines #resolve_rbs_return_type (visibility: private)

Resolve an RBS return type for a method call.

Parameters:

  • container_type (String)

    class or module name

  • method_name (String, Symbol)

    method name

  • core_rbs_provider (Object, nil)

    core RBS type lookup provider

Returns:

  • (String)

    inferred return type



907
908
909
910
911
912
913
914
915
916
917
# File 'lib/docscribe/infer/returns.rb', line 907

def resolve_rbs_return_type(container_type, method_name, core_rbs_provider)
  return FALLBACK_TYPE unless core_rbs_provider

  sig = core_rbs_provider.signature_for(
    container: container_type,
    scope: :instance,
    name: method_name
  )

  sig&.return_type || FALLBACK_TYPE
end

.returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, param_types: nil, container: nil, signature_provider: nil) ⇒ Object

Note:

module_function: defines #returns_spec_from_node (visibility: private)

Return a structured return-type spec for a method node.

The result includes:

  • :normal => normal/happy-path return type
  • :rescues => array of [exception_names, return_type] pairs for rescue branches

Parameters:

  • node (Parser::AST::Node)

    :def or :defs node

  • fallback_type (String) (defaults to: FALLBACK_TYPE)

    type used when inference is uncertain

  • nil_as_optional (Boolean) (defaults to: true)

    whether nil unions should be rendered as optional types

  • core_rbs_provider (Object?) (defaults to: nil)

    core RBS type lookup provider

  • param_types (Hash<String, String>?) (defaults to: nil)

    parameter name -> type map

  • container (String?) (defaults to: nil)
  • signature_provider (Docscribe::Types::ProviderChain?) (defaults to: nil)

Returns:

  • (Object)


73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/docscribe/infer/returns.rb', line 73

def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, # rubocop:disable Metrics/ParameterLists
                           param_types: nil, container: nil, signature_provider: nil)
  body = extract_def_body(node)
  spec = { normal: FALLBACK_TYPE, rescues: [] } #: Hash[Symbol, untyped]
  return spec unless body

  types = build_local_variable_types(body, core_rbs_provider: core_rbs_provider, param_types: param_types)
  populate_returns_spec(spec, body, types, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
                                           core_rbs_provider: core_rbs_provider, param_types: param_types,
                                           container: container, signature_provider: signature_provider)
  spec
end

.run_last_expr_type(node, **opts) ⇒ String?

Note:

module_function: defines #run_last_expr_type (visibility: private)

Dispatch last_expr_type based on node type.

Parameters:

  • node (Parser::AST::Node, nil)

    the :return AST node

  • opts (Object)

    options passed through as keyword args

Returns:

  • (String, nil)


878
879
880
881
882
883
884
885
886
887
888
# File 'lib/docscribe/infer/returns.rb', line 878

def run_last_expr_type(node, **opts)
  return unless node

  type = node.type == :defined? ? :defined : node.type
  method_name = :"handle_#{type}_node"
  if respond_to?(method_name, true)
    send(method_name, node, **opts)
  else
    Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
  end
end

.send_rbs_type(recv, meth, **opts) ⇒ String?

Note:

module_function: defines #send_rbs_type (visibility: private)

Resolve RBS return type for a send node, trying explicit receiver first, then falling back to the method's container for implicit self calls.

Parameters:

  • recv (Parser::AST::Node, nil)

    the receiver node

  • meth (Symbol)

    the method name

  • opts (Object)

    additional keyword options

Returns:

  • (String, nil)


648
649
650
651
652
653
654
655
656
657
# File 'lib/docscribe/infer/returns.rb', line 648

def send_rbs_type(recv, meth, **opts)
  rbs_type = resolve_rbs_for_send(recv, meth, opts[:core_rbs_provider], opts[:local_var_types],
                                  opts[:param_types])
  return rbs_type if rbs_type

  rbs_type = resolve_rbs_for_send_with_signature_provider(recv, meth, **opts)
  return rbs_type if rbs_type

  container_rbs_return_type(meth, **opts) if recv.nil?
end

.type_from_literal_safe(node) ⇒ String?

Note:

module_function: defines #type_from_literal_safe (visibility: private)

Safely get a type string from a literal node, returning nil if the node is not a literal or yields no type.

Parameters:

  • node (Parser::AST::Node, nil)

    literal AST node

Returns:

  • (String, nil)


796
797
798
799
800
801
# File 'lib/docscribe/infer/returns.rb', line 796

def type_from_literal_safe(node)
  return nil unless node

  t = Literals.type_from_literal(node, fallback_type: FALLBACK_TYPE)
  t unless t == FALLBACK_TYPE
end

.unify_nil_types(type_a, type_b, nil_as_optional:) ⇒ String

Note:

module_function: defines #unify_nil_types (visibility: private)

Unify two types where one may be nil, producing optional or union type.

Parameters:

  • type_a (String)

    first type string

  • type_b (String)

    second type string

  • nil_as_optional (Boolean)

    whether to render nil unions as optional types

Returns:

  • (String)


947
948
949
950
951
952
953
954
# File 'lib/docscribe/infer/returns.rb', line 947

def unify_nil_types(type_a, type_b, nil_as_optional:)
  if type_a == 'nil' || type_b == 'nil'
    non_nil = (type_a == 'nil' ? type_b : type_a)
    return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil"
  end

  "#{type_a}, #{type_b}"
end

.unify_types(type_a, type_b, fallback_type:, nil_as_optional:) ⇒ String

Note:

module_function: defines #unify_types (visibility: private)

Unify two inferred types into a single type string.

Rules:

  • identical types remain unchanged
  • nil unions may become optional types if enabled
  • otherwise falls back conservatively to fallback_type

Parameters:

  • type_a (String, nil)

    first type to unify

  • type_b (String, nil)

    second type to unify

  • fallback_type (String)

    type used when neither is nil

  • nil_as_optional (Boolean)

    whether to render nil unions as optional types

Returns:

  • (String)


932
933
934
935
936
937
938
# File 'lib/docscribe/infer/returns.rb', line 932

def unify_types(type_a, type_b, fallback_type:, nil_as_optional:)
  type_a ||= fallback_type
  type_b ||= fallback_type
  return type_a if type_a == type_b

  unify_nil_types(type_a, type_b, nil_as_optional: nil_as_optional)
end