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))


199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/docscribe/infer/returns.rb', line 199

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)


162
163
164
165
166
167
168
# File 'lib/docscribe/infer/returns.rb', line 162

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



181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/docscribe/infer/returns.rb', line 181

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>)


464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/docscribe/infer/returns.rb', line 464

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))


226
227
228
# File 'lib/docscribe/infer/returns.rb', line 226

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))


217
218
219
# File 'lib/docscribe/infer/returns.rb', line 217

def constant_name_and_value(node)
  [node.children[0].to_s, node.children[2]]
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)


88
89
90
91
92
93
# File 'lib/docscribe/infer/returns.rb', line 88

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)


418
419
420
421
422
423
# File 'lib/docscribe/infer/returns.rb', line 418

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)


354
355
356
# File 'lib/docscribe/infer/returns.rb', line 354

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)


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

def handle_block_node(node, **opts)
  send_node = node.children[0]
  if send_node&.type == :send
    recv = send_node.children[0]
    meth = send_node.children[1]
    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
  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)


550
551
552
553
554
555
556
557
558
559
560
# File 'lib/docscribe/infer/returns.rb', line 550

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)


381
382
383
384
385
386
387
388
389
390
391
# File 'lib/docscribe/infer/returns.rb', line 381

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)


269
270
271
272
# File 'lib/docscribe/infer/returns.rb', line 269

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)


319
320
321
322
323
324
# File 'lib/docscribe/infer/returns.rb', line 319

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)


498
499
500
501
# File 'lib/docscribe/infer/returns.rb', line 498

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)


485
486
487
# File 'lib/docscribe/infer/returns.rb', line 485

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)


258
259
260
261
# File 'lib/docscribe/infer/returns.rb', line 258

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)


306
307
308
309
310
311
# File 'lib/docscribe/infer/returns.rb', line 306

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)


364
365
366
367
368
369
370
371
372
373
# File 'lib/docscribe/infer/returns.rb', line 364

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)


570
571
572
# File 'lib/docscribe/infer/returns.rb', line 570

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)


247
248
249
250
# File 'lib/docscribe/infer/returns.rb', line 247

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)


293
294
295
296
297
298
# File 'lib/docscribe/infer/returns.rb', line 293

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)


434
435
436
# File 'lib/docscribe/infer/returns.rb', line 434

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)


236
237
238
239
# File 'lib/docscribe/infer/returns.rb', line 236

def handle_lvar_node(node, **opts)
  name = node.children[0].to_s
  opts[:local_var_types]&.fetch(name, nil) || 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)


280
281
282
283
284
285
# File 'lib/docscribe/infer/returns.rb', line 280

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)


335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/docscribe/infer/returns.rb', line 335

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)


402
403
404
405
406
407
# File 'lib/docscribe/infer/returns.rb', line 402

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)


447
448
449
450
451
452
453
# File 'lib/docscribe/infer/returns.rb', line 447

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)


834
835
836
# File 'lib/docscribe/infer/returns.rb', line 834

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
639
640
641
# File 'lib/docscribe/infer/returns.rb', line 627

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

  if opts[:core_rbs_provider]
    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
  end

  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)


525
526
527
# File 'lib/docscribe/infer/returns.rb', line 525

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)


538
539
540
# File 'lib/docscribe/infer/returns.rb', line 538

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)


512
513
514
# File 'lib/docscribe/infer/returns.rb', line 512

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)


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

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)


117
118
119
# File 'lib/docscribe/infer/returns.rb', line 117

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:

  • (String)

    if Parser::SyntaxError

  • (FALLBACK_TYPE)

    if Parser::SyntaxError

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)


806
807
808
# File 'lib/docscribe/infer/returns.rb', line 806

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)


766
767
768
769
770
771
# File 'lib/docscribe/infer/returns.rb', line 766

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)


103
104
105
106
107
108
109
# File 'lib/docscribe/infer/returns.rb', line 103

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



592
593
594
595
596
597
598
599
600
# File 'lib/docscribe/infer/returns.rb', line 592

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



580
581
582
583
584
# File 'lib/docscribe/infer/returns.rb', line 580

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)


128
129
130
131
132
133
134
135
136
# File 'lib/docscribe/infer/returns.rb', line 128

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



145
146
147
148
149
150
151
152
153
154
# File 'lib/docscribe/infer/returns.rb', line 145

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)


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

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)


782
783
784
785
786
787
788
789
790
# File 'lib/docscribe/infer/returns.rb', line 782

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)


750
751
752
753
754
755
756
757
# File 'lib/docscribe/infer/returns.rb', line 750

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



655
656
657
658
659
660
661
662
663
# File 'lib/docscribe/infer/returns.rb', line 655

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

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

  rbs = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
  rbs unless rbs == FALLBACK_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



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

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) ⇒ 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

Returns:

  • (Object)


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

def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
                           param_types: 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)
  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)


816
817
818
819
820
821
822
823
824
825
826
# File 'lib/docscribe/infer/returns.rb', line 816

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

.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)


734
735
736
737
738
739
# File 'lib/docscribe/infer/returns.rb', line 734

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)


885
886
887
888
889
890
891
892
# File 'lib/docscribe/infer/returns.rb', line 885

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)


870
871
872
873
874
875
876
# File 'lib/docscribe/infer/returns.rb', line 870

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