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

.apply_generic_mapping(rbs, mapping, recv_type) ⇒ String

Note:

module_function: defines #apply_generic_mapping (visibility: private)

Parameters:

  • rbs (String)
  • mapping (Hash<String, String>)
  • recv_type (String)

Returns:

  • (String)


2285
2286
2287
2288
2289
2290
2291
2292
2293
# File 'lib/docscribe/infer/returns.rb', line 2285

def apply_generic_mapping(rbs, mapping, recv_type)
  stripped = rbs.delete_suffix('?').strip
  optional = rbs.end_with?('?')
  return optional ? "#{mapping[stripped]}?" : mapping[stripped] if mapping.key?(stripped)

  new_rbs = rbs.dup
  mapping.each { |var, val| new_rbs = new_rbs.gsub(/\b#{Regexp.escape(var)}\b/, val) }
  new_rbs.include?('self') ? new_rbs.gsub(/\bself\b/, recv_type) : new_rbs
end

.assignment_inferred_type(value, types, **opts) ⇒ String?

Note:

module_function: defines #assignment_inferred_type (visibility: private)

Parameters:

  • value (Parser::AST::Node)
  • types (Hash<String, String>)
  • opts (Hash)

Returns:

  • (String, nil)


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

def assignment_inferred_type(value, types, **opts)
  run_last_expr_type(value, fallback_type: FALLBACK_TYPE, nil_as_optional: false, local_var_types: types,
                            core_rbs_provider: opts[:core_rbs_provider], param_types: opts[:param_types],
                            signature_provider: opts[:signature_provider], container: opts[:container])
end

.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, :or_asgn)

Returns:

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


257
258
259
260
261
262
263
264
265
266
267
# File 'lib/docscribe/infer/returns.rb', line 257

def assignment_name_and_value(node)
  return [nil, nil] unless node.is_a?(Parser::AST::Node)

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

.assignment_op_asgn_type(node, types, **opts) ⇒ String?

Note:

module_function: defines #assignment_op_asgn_type (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • types (Hash<String, String>)
  • opts (Hash)

Returns:

  • (String, nil)


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

def assignment_op_asgn_type(node, types, **opts)
  run_last_expr_type(node, fallback_type: FALLBACK_TYPE, nil_as_optional: false, local_var_types: types,
                           core_rbs_provider: opts[:core_rbs_provider], param_types: opts[:param_types],
                           signature_provider: opts[:signature_provider], container: opts[:container])
end

.bare_container_type(rbs_type, inner) ⇒ String?

Note:

module_function: defines #bare_container_type (visibility: private)

Parameters:

  • rbs_type (String, nil)
  • inner (String)

Returns:

  • (String, nil)


1068
1069
1070
1071
1072
1073
1074
1075
1076
# File 'lib/docscribe/infer/returns.rb', line 1068

def bare_container_type(rbs_type, inner)
  return nil unless rbs_type.is_a?(String)

  base = rbs_type&.split(/[<\[ ]/)&.first
  return nil unless %w[Array Set Enumerable Enumerator].include?(base)
  return nil if rbs_type.include?('<') || rbs_type.include?('[')

  "#{base}<#{inner}>"
end

.block_arg_names(node) ⇒ Array<String>

Note:

module_function: defines #block_arg_names (visibility: private)

Names of the block parameters, if any.

Parameters:

  • node (Parser::AST::Node)

    block node

Returns:

  • (Array<String>)


902
903
904
905
906
907
# File 'lib/docscribe/infer/returns.rb', line 902

def block_arg_names(node)
  args = node.children[1]
  return [] unless args&.type == :args

  args.children.filter_map { |a| a.children[0]&.to_s }
end

.block_generic_substitution(rbs_type, inner) ⇒ String?

Note:

module_function: defines #block_generic_substitution (visibility: private)

Parameters:

  • rbs_type (String)
  • inner (String, nil)

Returns:

  • (String, nil)


1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
# File 'lib/docscribe/infer/returns.rb', line 1019

def block_generic_substitution(rbs_type, inner)
  return nil unless generic_placeholder?(rbs_type)

  inner_generic = extract_generic_inner(rbs_type)
  return nil unless inner_generic

  placeholders = placeholder_tokens(inner_generic)
  result = substitute_placeholders(rbs_type, placeholders, inner)
  return result unless result == rbs_type

  fallback_generic_substitution(rbs_type, inner)
end

.block_rbs_with_inner(rbs_type, block_body, **opts) ⇒ String?

Note:

module_function: defines #block_rbs_with_inner (visibility: private)

Parameters:

  • rbs_type (String, nil)
  • block_body (Parser::AST::Node)
  • opts (Hash)

Returns:

  • (String, nil)


1005
1006
1007
1008
1009
1010
1011
1012
1013
# File 'lib/docscribe/infer/returns.rb', line 1005

def block_rbs_with_inner(rbs_type, block_body, **opts)
  inner = run_last_expr_type(block_body, **opts)
  return nil unless inner

  substituted = block_generic_substitution(rbs_type, inner)
  return substituted if substituted

  bare_container_type(rbs_type, inner)
end

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

Note:

module_function: defines #block_receiver_type (visibility: private)

Parameters:

  • recv (Parser::AST::Node)
  • core_rbs_provider (Docscribe::Types::RBS::Provider?)
  • local_var_types (Hash<String, String>?)
  • param_types (Hash<String, String>?)

Returns:

  • (String, nil)


1740
1741
1742
1743
1744
1745
# File 'lib/docscribe/infer/returns.rb', line 1740

def block_receiver_type(recv, core_rbs_provider, local_var_types, param_types)
  run_last_expr_type(recv, fallback_type: FALLBACK_TYPE, nil_as_optional: false,
                           core_rbs_provider: core_rbs_provider, local_var_types: local_var_types,
                           param_types: param_types) ||
    receiver_send_type(recv.children[0], core_rbs_provider, local_var_types, param_types)
end

.block_send_rbs_type(node, send_node, **opts) ⇒ String?

Note:

module_function: defines #block_send_rbs_type (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • send_node (Parser::AST::Node)
  • opts (Hash)

Returns:

  • (String, nil)


993
994
995
996
997
998
# File 'lib/docscribe/infer/returns.rb', line 993

def block_send_rbs_type(node, send_node, **opts)
  rbs_type = send_rbs_type(send_node.children[0], send_node.children[1], **opts)
  return nil unless rbs_type

  block_rbs_with_inner(rbs_type, node.children[2], **opts) || rbs_type
end

.both_clean_equal?(left_clean, right_clean) ⇒ Boolean

Note:

module_function: defines #both_clean_equal? (visibility: private)

Parameters:

  • left_clean (String, nil)
  • right_clean (String, nil)

Returns:

  • (Boolean)


1816
1817
1818
# File 'lib/docscribe/infer/returns.rb', line 1816

def both_clean_equal?(left_clean, right_clean)
  left_clean && right_clean && left_clean == right_clean
end

.build_generic_mapping(recv_type, args) ⇒ Hash<String, String>

Note:

module_function: defines #build_generic_mapping (visibility: private)

Parameters:

  • recv_type (String)
  • args (Array<String>)

Returns:

  • (Hash<String, String>)


2235
2236
2237
2238
2239
2240
# File 'lib/docscribe/infer/returns.rb', line 2235

def build_generic_mapping(recv_type, args)
  base = recv_type.split(/[<\[ ]/).first.to_s.strip
  mapping = {} #: Hash[String, String]
  fill_mapping_for_base(mapping, base, args)
  mapping
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 (Hash)

    additional keyword options forwarded to inference

Returns:

  • (Hash<String, String>, nil)


199
200
201
202
203
204
205
# File 'lib/docscribe/infer/returns.rb', line 199

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

.cleaned_recv_type(raw) ⇒ String?

Note:

module_function: defines #cleaned_recv_type (visibility: private)

Parameters:

  • raw (String, nil)

Returns:

  • (String, nil)


1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
# File 'lib/docscribe/infer/returns.rb', line 1600

def cleaned_recv_type(raw) # rubocop:disable SortedMethodsByCall/Waterfall
  return nil unless raw && raw != FALLBACK_TYPE

  str = raw.to_s.strip
  return nil if str.empty?

  str = stripped_union_type(str) || str if str.include?(',')
  cleaned = str.delete_suffix('?').strip
  cleaned.empty? ? nil : cleaned
end

.coalesce_type(type, fallback_type) ⇒ String

Note:

module_function: defines #coalesce_type (visibility: private)

Parameters:

  • type (String, nil)
  • fallback_type (String)

Returns:

  • (String)


2392
2393
2394
2395
2396
2397
# File 'lib/docscribe/infer/returns.rb', line 2392

def coalesce_type(type, fallback_type)
  normalized = type || fallback_type
  normalized = fallback_type if normalized == 'FALLBACK_TYPE'
  normalized = 'Object' if normalized == 'untyped' && fallback_type == 'Object'
  normalized
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 (Hash)

    additional keyword options forwarded to inference



218
219
220
221
222
223
224
225
226
227
228
# File 'lib/docscribe/infer/returns.rb', line 218

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

  inferred = if %i[op_asgn or_asgn].include?(node.type)
               assignment_op_asgn_type(node, types, **opts)
             else
               assignment_inferred_type(value, types, **opts)
             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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (Array<String, nil>)


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

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_expr_opts(**opts) ⇒ Hash<Symbol, Object>

Note:

module_function: defines #compound_expr_opts (visibility: private)

Parameters:

  • opts (Hash)

Returns:

  • (Hash<Symbol, Object>)


1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
# File 'lib/docscribe/infer/returns.rb', line 1502

def compound_expr_opts(**opts)
  {
    fallback_type: opts[:fallback_type] || FALLBACK_TYPE,
    nil_as_optional: true,
    local_var_types: opts[:local_var_types],
    param_types: opts[:param_types],
    core_rbs_provider: opts[:core_rbs_provider],
    signature_provider: opts[:signature_provider],
    container: opts[:container]
  }
end

.compound_fallback_type(left, right, meth, **opts) ⇒ String?

Note:

module_function: defines #compound_fallback_type (visibility: private)

Parameters:

  • left (String, nil)
  • right (String, nil)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


1552
1553
1554
1555
1556
1557
1558
1559
# File 'lib/docscribe/infer/returns.rb', line 1552

def compound_fallback_type(left, right, meth, **opts)
  fallback = (opts[:fallback_type] || FALLBACK_TYPE).to_s
  return synthesize_shovel_type(left, right, fallback: fallback) if shovel_method?(left, meth, opts[:core_rbs_provider])
  return nil unless %i[+ - * / % ** | & ^].include?(meth)

  fallback_concrete_type(left, right, fallback) ||
    unify_types(left, right, fallback_type: fallback, nil_as_optional: true)
end

.compound_left_type(recv, **opts) ⇒ String?

Note:

module_function: defines #compound_left_type (visibility: private)

Parameters:

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

Returns:

  • (String, nil)


1464
1465
1466
1467
1468
1469
1470
1471
# File 'lib/docscribe/infer/returns.rb', line 1464

def compound_left_type(recv, **opts)
  return nil unless recv

  found = compound_var_lookup(recv, **opts)
  return found if found

  run_last_expr_type(recv, **compound_expr_opts(**opts))
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))


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

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

.compound_rbs_type(recv, left, meth, **opts) ⇒ String?

Note:

module_function: defines #compound_rbs_type (visibility: private)

Parameters:

  • recv (Parser::AST::Node, nil)
  • left (String, nil)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


1520
1521
1522
1523
1524
1525
1526
1527
# File 'lib/docscribe/infer/returns.rb', line 1520

def compound_rbs_type(recv, left, meth, **opts)
  recv_type = cleaned_recv_type(left) ||
              receiver_rbs_type_name(recv, opts[:core_rbs_provider],
                                     opts[:local_var_types], opts[:param_types])
  return nil unless recv_type && meth

  resolve_compound_rbs(recv_type, meth, **opts)
end

.compound_right_type(arg, **opts) ⇒ String?

Note:

module_function: defines #compound_right_type (visibility: private)

Parameters:

  • arg (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1493
1494
1495
1496
1497
# File 'lib/docscribe/infer/returns.rb', line 1493

def compound_right_type(arg, **opts)
  return nil unless arg

  run_last_expr_type(arg, **compound_expr_opts(**opts))
end

.compound_var_lookup(recv, **opts) ⇒ String?

Note:

module_function: defines #compound_var_lookup (visibility: private)

Parameters:

  • recv (Parser::AST::Node)
  • opts (Hash)

Returns:

  • (String, nil)


1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
# File 'lib/docscribe/infer/returns.rb', line 1477

def compound_var_lookup(recv, **opts)
  return nil unless recv.is_a?(Parser::AST::Node)
  return nil unless %i[lvar ivar gvar cvar].include?(recv&.type)

  name = recv&.children&.[](0).to_s
  if recv&.type == :lvar
    lookup_lvar_type(name, opts[:local_var_types], opts[:param_types])
  else
    opts[:local_var_types]&.fetch(name, nil)
  end
end

.const_owner_paths(container, scope_parts, absolute) ⇒ Array<String>

Note:

module_function: defines #const_owner_paths (visibility: private)

Candidate owner paths for a constant lookup, innermost first.

Flat strings keep generic inference (and RubyMine) happy — nested arrays lose a level (first(n) degrades to Elem).

Parameters:

  • container (String, nil)

    lexical container (e.g. "Foo::Bar")

  • scope_parts (Array<String>)

    static scope segments (may be empty)

  • absolute (Boolean)

    whether the reference starts with ::

Returns:

  • (Array<String>)

    owner paths, "" means top level



2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
# File 'lib/docscribe/infer/returns.rb', line 2075

def const_owner_paths(container, scope_parts, absolute)
  return [scope_parts.join('::')] if absolute

  segments = container.to_s.split('::').grep(/\A[A-Z]\w*\z/)
  # NOTE: `take` (not `first`) keeps RBS generic inference precise —
  # `first(n)` loses one nesting level.
  paths = segments.length.downto(1).map { |n| (segments.take(n) + scope_parts).join('::') }
  paths << scope_parts.join('::')
  paths
end

.const_path_segments(node) ⇒ (Array<String>, Boolean)?

Note:

module_function: defines #const_path_segments (visibility: private)

Split a const node into static path segments and absoluteness.

Parameters:

  • node (Parser::AST::Node)

    the :const node

Returns:

  • ((Array<String>, Boolean), nil)

    segments with last element being the constant name plus absolute flag, or nil when dynamic



2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
# File 'lib/docscribe/infer/returns.rb', line 2052

def const_path_segments(node)
  parts = [] #: Array[String]
  current = node
  while current.is_a?(Parser::AST::Node) && current.type == :const
    parts.unshift(current.children[1].to_s)
    current = current.children[0]
  end
  return nil if current && !(current.is_a?(Parser::AST::Node) && current.type == :cbase)

  absolute = !current.nil?
  [parts, absolute]
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))


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

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

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

Returns:

  • (String, nil)

    resolved type or nil if unresolvable



1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
# File 'lib/docscribe/infer/returns.rb', line 1407

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 substitute_rbs_type(rbs, opts[:container]) unless rbs == FALLBACK_TYPE
  end

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

  nil
end

.core_rbs_providerDocscribe::Types::RBS::Provider?

Note:

module_function: defines #core_rbs_provider (visibility: private)

Core RBS provider singleton for shovel/primitive checks (dynamic, not hardcoding).

Returns:

Raises:

  • (LoadError)
  • (StandardError)


1659
1660
1661
1662
1663
1664
1665
1666
# File 'lib/docscribe/infer/returns.rb', line 1659

def core_rbs_provider
  @core_rbs_provider ||= begin
    require_relative '../types/rbs/provider'
    Docscribe::Types::RBS::Provider.new(sig_dirs: ['sig'], collection_dirs: [])
  rescue LoadError, StandardError
    nil
  end
end

.enumerator_elem_from_recv(recv, **opts) ⇒ String?

Note:

module_function: defines #enumerator_elem_from_recv (visibility: private)

Parameters:

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

Returns:

  • (String, nil)


1180
1181
1182
1183
1184
1185
# File 'lib/docscribe/infer/returns.rb', line 1180

def enumerator_elem_from_recv(recv, **opts)
  recv_type = enumerator_recv_type(recv, **opts)
  return nil unless recv_type

  extract_array_elem_strict(recv_type) || enumerator_object_elem(recv_type)
end

.enumerator_object_elem(recv_type) ⇒ String?

Note:

module_function: defines #enumerator_object_elem (visibility: private)

Parameters:

  • recv_type (String)

Returns:

  • (String, nil)


1214
1215
1216
1217
1218
# File 'lib/docscribe/infer/returns.rb', line 1214

def enumerator_object_elem(recv_type)
  return 'Object' if recv_type == 'Array'

  nil
end

.enumerator_recv_type(recv, **opts) ⇒ String?

Note:

module_function: defines #enumerator_recv_type (visibility: private)

Parameters:

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

Returns:

  • (String, nil)


1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
# File 'lib/docscribe/infer/returns.rb', line 1191

def enumerator_recv_type(recv, **opts)
  receiver_rbs_type_name(recv, opts[:core_rbs_provider], opts[:local_var_types],
                         opts[:param_types]) ||
    run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
                             local_var_types: opts[:local_var_types],
                             param_types: opts[:param_types],
                             core_rbs_provider: opts[:core_rbs_provider],
                             signature_provider: opts[:signature_provider],
                             container: opts[:container])
end

.extract_array_elem(type_str) ⇒ String?

Note:

module_function: defines #extract_array_elem (visibility: private)

Parameters:

  • type_str (String)

Returns:

  • (String, nil)


1263
1264
1265
1266
1267
1268
1269
1270
# File 'lib/docscribe/infer/returns.rb', line 1263

def extract_array_elem(type_str)
  return nil unless type_str =~ /\AArray<(.+)>\z/

  cand = Regexp.last_match(1).strip
  return nil if cand.empty? || %w[Object untyped].include?(cand)

  cand
end

.extract_array_elem_strict(type_str) ⇒ String?

Note:

module_function: defines #extract_array_elem_strict (visibility: private)

Parameters:

  • type_str (String)

Returns:

  • (String, nil)


1205
1206
1207
1208
1209
# File 'lib/docscribe/infer/returns.rb', line 1205

def extract_array_elem_strict(type_str)
  return nil unless type_str =~ /\AArray<(.+)>\z/

  Regexp.last_match(1).strip
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)


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

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

.extract_enumerator_elem(type_str) ⇒ String?

Note:

module_function: defines #extract_enumerator_elem (visibility: private)

Parameters:

  • type_str (String)

Returns:

  • (String, nil)


1275
1276
1277
1278
1279
1280
1281
1282
# File 'lib/docscribe/infer/returns.rb', line 1275

def extract_enumerator_elem(type_str)
  return nil unless type_str =~ /\AEnumerator<(.+),\s*Integer>\z/

  cand = Regexp.last_match(1).strip
  return nil if cand.empty?

  cand
end

.extract_generic_inner(type) ⇒ String?

Note:

module_function: defines #extract_generic_inner (visibility: private)

Extract the inner generic args string from a receiver type like Array<String> or Hash<Integer, String>.

Parameters:

  • type (String)

    the concrete type string

Returns:

  • (String, nil)


2300
2301
2302
2303
2304
# File 'lib/docscribe/infer/returns.rb', line 2300

def extract_generic_inner(type)
  return unless type =~ /\A(?:Array|Hash|Set|Enumerable)[<\[](.*)[>\]]\z/m || type =~ /\A[^<\[\]]+[<\[](.*)[>\]]\z/m

  Regexp.last_match(1)
end

.fallback_alias?(type_str, fallback_type) ⇒ Boolean

Note:

module_function: defines #fallback_alias? (visibility: private)

Whether a type string is the fallback alias (FALLBACK_TYPE or the configured fallback type).

Parameters:

  • type_str (String, nil)

    the type string to check

  • fallback_type (String)

    the configured fallback type

Returns:

  • (Boolean)


2160
2161
2162
2163
2164
2165
# File 'lib/docscribe/infer/returns.rb', line 2160

def fallback_alias?(type_str, fallback_type)
  return false if type_str.nil?

  s = type_str.to_s.strip.delete_suffix('?').strip
  s == fallback_type || s == 'FALLBACK_TYPE' || (fallback_type == 'Object' && s == 'untyped')
end

.fallback_concrete_type(left, right, fallback) ⇒ String

Note:

module_function: defines #fallback_concrete_type (visibility: private)

Parameters:

  • left (String, nil)
  • right (String, nil)
  • fallback (String)

Returns:

  • (String)


1566
1567
1568
1569
1570
1571
1572
1573
1574
# File 'lib/docscribe/infer/returns.rb', line 1566

def fallback_concrete_type(left, right, fallback)
  left_is_fallback = fallback_type?(left, fallback)
  right_is_fallback = fallback_type?(right, fallback)
  preferred = fallback_preferred_side(left, right, left_is_fallback, right_is_fallback)
  return preferred if preferred
  return fallback if left_is_fallback && right_is_fallback

  nil
end

.fallback_generic_substitution(rbs_type, inner) ⇒ String

Note:

module_function: defines #fallback_generic_substitution (visibility: private)

Parameters:

  • rbs_type (String)
  • inner (String)

Returns:

  • (String)


1059
1060
1061
1062
# File 'lib/docscribe/infer/returns.rb', line 1059

def fallback_generic_substitution(rbs_type, inner)
  rbs_type.gsub(/\bU\b/, inner).gsub(/\bElem\b/, inner).gsub(/\buntyped\b/, inner)
          .gsub(/\bV\b/, inner).gsub(/\bT\b/, inner).gsub(/\bE\b/, inner).gsub(/\bK\b/, inner)
end

.fallback_preferred_side(left, right, left_is_fallback, right_is_fallback) ⇒ String?

Note:

module_function: defines #fallback_preferred_side (visibility: private)

Parameters:

  • left (String, nil)
  • right (String, nil)
  • left_is_fallback (Boolean)
  • right_is_fallback (Boolean)

Returns:

  • (String, nil)


1582
1583
1584
1585
1586
1587
# File 'lib/docscribe/infer/returns.rb', line 1582

def fallback_preferred_side(left, right, left_is_fallback, right_is_fallback)
  return right.to_s if left_is_fallback && !right_is_fallback && right
  return left.to_s if right_is_fallback && !left_is_fallback && left

  nil
end

.fallback_type?(type, fallback) ⇒ Boolean

Note:

module_function: defines #fallback_type? (visibility: private)

Parameters:

  • type (String, nil)
  • fallback (String)

Returns:

  • (Boolean)


1593
1594
1595
# File 'lib/docscribe/infer/returns.rb', line 1593

def fallback_type?(type, fallback)
  type.nil? || fallback_alias?(type, fallback)
end

.file_join_method?(meth, recv) ⇒ Boolean

Note:

module_function: defines #file_join_method? (visibility: private)

Parameters:

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

Returns:

  • (Boolean)


1307
1308
1309
# File 'lib/docscribe/infer/returns.rb', line 1307

def file_join_method?(meth, recv)
  meth == :join && recv&.type == :const && recv&.children&.[](1) == :File
end

.fill_array_mapping(mapping, args) ⇒ void

Note:

module_function: defines #fill_array_mapping (visibility: private)

This method returns an undefined value.

Parameters:

  • mapping (Hash<String, String>)
  • args (Array<String>)


2268
2269
2270
# File 'lib/docscribe/infer/returns.rb', line 2268

def fill_array_mapping(mapping, args)
  %w[Elem T U E].each { |key| mapping[key] = args[0] if args[0] }
end

.fill_hash_mapping(mapping, args) ⇒ void

Note:

module_function: defines #fill_hash_mapping (visibility: private)

This method returns an undefined value.

Parameters:

  • mapping (Hash<String, String>)
  • args (Array<String>)


2259
2260
2261
2262
# File 'lib/docscribe/infer/returns.rb', line 2259

def fill_hash_mapping(mapping, args)
  mapping['K'] = args[0] if args[0]
  mapping['V'] = args[1] if args[1]
end

.fill_mapping_for_base(mapping, base, args) ⇒ void

Note:

module_function: defines #fill_mapping_for_base (visibility: private)

This method returns an undefined value.

Parameters:

  • mapping (Hash<String, String>)
  • base (String)
  • args (Array<String>)


2247
2248
2249
2250
2251
2252
2253
# File 'lib/docscribe/infer/returns.rb', line 2247

def fill_mapping_for_base(mapping, base, args)
  case base
  when 'Hash' then fill_hash_mapping(mapping, args)
  when 'Array', 'Set', 'Enumerable', 'Enumerator' then fill_array_mapping(mapping, args)
  else fill_other_mapping(mapping, args)
  end
end

.fill_other_mapping(mapping, args) ⇒ void

Note:

module_function: defines #fill_other_mapping (visibility: private)

This method returns an undefined value.

Parameters:

  • mapping (Hash<String, String>)
  • args (Array<String>)


2276
2277
2278
# File 'lib/docscribe/infer/returns.rb', line 2276

def fill_other_mapping(mapping, args)
  %w[Elem T U].each { |key| mapping[key] = args[0] if args[0] }
end

.generic_placeholder?(rbs_type) ⇒ Boolean

Note:

module_function: defines #generic_placeholder? (visibility: private)

Parameters:

  • rbs_type (String, nil)

Returns:

  • (Boolean)


1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
# File 'lib/docscribe/infer/returns.rb', line 1081

def generic_placeholder?(rbs_type)
  return false unless rbs_type =~ /[<\[]/

  inner = extract_generic_inner(rbs_type)
  return false unless inner

  split_generic_args(inner).any? do |arg|
    placeholder_token?(arg.strip.delete_suffix('?').strip)
  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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


643
644
645
646
647
648
# File 'lib/docscribe/infer/returns.rb', line 643

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


832
833
834
835
836
837
838
839
840
841
842
# File 'lib/docscribe/infer/returns.rb', line 832

def handle_block_node(node, **opts)
  send_node = node.children[0]
  return run_last_expr_type(node.children[2], **opts) unless send_node&.type == :send

  meth = send_node.children[1]
  handle_then_block(node, meth, **opts) ||
    handle_to_h_block(node, meth, **opts) ||
    block_send_rbs_type(node, send_node, **opts) ||
    handle_map_block(node, meth, **opts) ||
    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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


564
565
566
567
568
569
570
571
572
573
574
# File 'lib/docscribe/infer/returns.rb', line 564

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_const_node(node, **opts) ⇒ String?

Note:

module_function: defines #handle_const_node (visibility: private)

Parameters:

  • node (Parser::AST::Node)

    the :const AST node

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


2008
2009
2010
2011
2012
2013
2014
2015
2016
# File 'lib/docscribe/infer/returns.rb', line 2008

def handle_const_node(node, **opts)
  fallback = (opts[:fallback_type] || FALLBACK_TYPE).to_s #: String
  const_name = node.children.last.to_s #: String
  resolved = Literals.type_from_literal(node, fallback_type: fallback)
  return fallback if fallback_alias?(resolved, fallback)
  return fallback if fallback_alias?(const_name, fallback)

  resolved
end

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

Note:

module_function: defines #handle_csend_node (visibility: private)

Parameters:

  • node (Parser::AST::Node)

    the :csend AST node (safe navigation)

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
# File 'lib/docscribe/infer/returns.rb', line 1323

def handle_csend_node(node, **opts)
  recv = node.children[0]
  meth = node.children[1]
  rbs_type = send_rbs_type(recv, meth, **opts) if opts[:core_rbs_provider] || opts[:signature_provider]
  if rbs_type
    unify_types(rbs_type, 'nil', fallback_type: opts[:fallback_type] || FALLBACK_TYPE,
                                 nil_as_optional: opts.fetch(:nil_as_optional, true))
  else
    opts[:fallback_type] || FALLBACK_TYPE
  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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


341
342
343
344
# File 'lib/docscribe/infer/returns.rb', line 341

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


391
392
393
394
395
396
# File 'lib/docscribe/infer/returns.rb', line 391

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


723
724
725
726
# File 'lib/docscribe/infer/returns.rb', line 723

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


710
711
712
# File 'lib/docscribe/infer/returns.rb', line 710

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


330
331
332
333
# File 'lib/docscribe/infer/returns.rb', line 330

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


378
379
380
381
382
383
# File 'lib/docscribe/infer/returns.rb', line 378

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


547
548
549
550
551
552
553
554
555
556
# File 'lib/docscribe/infer/returns.rb', line 547

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


795
796
797
# File 'lib/docscribe/infer/returns.rb', line 795

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


659
660
661
# File 'lib/docscribe/infer/returns.rb', line 659

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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_map_block(node, meth, **opts) ⇒ String?

Note:

module_function: defines #handle_map_block (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


979
980
981
982
983
984
985
986
# File 'lib/docscribe/infer/returns.rb', line 979

def handle_map_block(node, meth, **opts)
  return nil unless %i[map collect].include?(meth)

  inner = run_last_expr_type(node.children[2], **opts)
  return nil unless inner && inner != 'Object' && inner != 'untyped'

  "Array<#{inner}>"
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.).

RBS -> Infer: try RBS for meth on receiver type, else unify left/right keeping String? via nil_as_optional:true. No hardcoded operator list.

Parameters:

  • node (Parser::AST::Node)

    the :op_asgn AST node

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


407
408
409
410
411
412
413
414
415
416
417
# File 'lib/docscribe/infer/returns.rb', line 407

def handle_op_asgn_node(node, **opts)
  meth = node.children[1]
  lhs = node.children[0]
  rhs = node.children[2]
  left = op_asgn_left_type(lhs, **opts)
  right = op_asgn_right_type(rhs, **opts)
  rbs = op_asgn_rbs_type(lhs, left, meth, **opts)
  return rbs if rbs

  op_asgn_fallback_type(left, right, meth, **opts)
end

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

Note:

module_function: defines #handle_or_asgn_node (visibility: private)

Handle :or_asgn node (x ||= y) for last_expr_type.

Same type semantics as ||: the assignment target counts as the left side, so an unknown receiver with a concrete literal right-hand side (e.g. @h ||= Hash.new) infers the literal type.

Parameters:

  • node (Parser::AST::Node)

    the :or_asgn AST node

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

def handle_or_asgn_node(node, **opts)
  t = run_last_expr_type(node.children[0], **opts)
  e = run_last_expr_type(node.children[1], **opts)
  fallback = opts[:fallback_type] || 'untyped'
  preferred = or_prefer_concrete(t, e, fallback)
  return preferred if preferred

  unify_types(t, e, fallback_type: fallback,
                    nil_as_optional: opts.fetch(:nil_as_optional, true))
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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/docscribe/infer/returns.rb', line 585

def handle_or_node(node, **opts)
  t = run_last_expr_type(node.children[0], **opts)
  e = run_last_expr_type(node.children[1], **opts)
  fallback = opts[:fallback_type] || 'untyped'
  # If one side is the fallback alias (FALLBACK_TYPE / fallback_type) and the other is concrete, prefer the concrete
  # This prevents `sig&.return_type || FALLBACK_TYPE` from becoming `String, Object` when String is known
  preferred = or_prefer_concrete(t, e, fallback)
  return preferred if preferred

  unify_types(t, e, fallback_type: fallback,
                    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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


2000
2001
2002
# File 'lib/docscribe/infer/returns.rb', line 2000

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
# File 'lib/docscribe/infer/returns.rb', line 1106

def handle_send_node(node, **opts)
  recv = node.children[0]
  meth = node.children[1]
  try_synthetic(node, meth, recv, **opts) ||
    try_rbs(meth, recv, **opts) ||
    try_compound(node, **opts) ||
    string_send_type(meth, recv) ||
    rbs_fallback(meth, recv, **opts) ||
    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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


750
751
752
# File 'lib/docscribe/infer/returns.rb', line 750

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

.handle_then_block(node, meth, **opts) ⇒ String?

Note:

module_function: defines #handle_then_block (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


849
850
851
852
853
# File 'lib/docscribe/infer/returns.rb', line 849

def handle_then_block(node, meth, **opts)
  return nil unless %i[then yield_self].include?(meth)

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

.handle_to_h_block(node, meth, **opts) ⇒ String?

Note:

module_function: defines #handle_to_h_block (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


860
861
862
863
864
865
866
867
868
# File 'lib/docscribe/infer/returns.rb', line 860

def handle_to_h_block(node, meth, **opts)
  return nil unless meth == :to_h

  recv = to_h_each_recv(node)
  return nil unless recv

  key, value = to_h_key_value(node, recv, **opts)
  "Hash<#{key}, #{value}>"
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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


763
764
765
# File 'lib/docscribe/infer/returns.rb', line 763

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

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

.hash_elem_from_enumerator(inner_recv, **opts) ⇒ String?

Note:

module_function: defines #hash_elem_from_enumerator (visibility: private)

Parameters:

  • inner_recv (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1238
1239
1240
1241
1242
1243
# File 'lib/docscribe/infer/returns.rb', line 1238

def hash_elem_from_enumerator(inner_recv, **opts)
  inner_type = hash_inner_type(inner_recv, **opts)
  return nil unless inner_type

  extract_array_elem(inner_type) || extract_enumerator_elem(inner_type)
end

.hash_inner_type(inner_recv, **opts) ⇒ String?

Note:

module_function: defines #hash_inner_type (visibility: private)

Parameters:

  • inner_recv (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
# File 'lib/docscribe/infer/returns.rb', line 1249

def hash_inner_type(inner_recv, **opts)
  receiver_rbs_type_name(inner_recv, opts[:core_rbs_provider], opts[:local_var_types],
                         opts[:param_types]) ||
    run_last_expr_type(inner_recv, fallback_type: nil, nil_as_optional: false,
                                   local_var_types: opts[:local_var_types],
                                   param_types: opts[:param_types],
                                   core_rbs_provider: opts[:core_rbs_provider],
                                   signature_provider: opts[:signature_provider],
                                   container: opts[:container])
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.

RBS -> Infer: no hardcoded operator list, always try left/right via Infer, then RBS for meth on receiver, else unify with String? handling.

Parameters:

  • node (Parser::AST::Node)

    the :send AST node

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
# File 'lib/docscribe/infer/returns.rb', line 1448

def infer_from_compound_assign(node, **opts)
  meth = node.children[1]
  recv = node.children[0]
  arg = node.children[2]
  left = compound_left_type(recv, **opts)
  right = compound_right_type(arg, **opts)
  rbs = compound_rbs_type(recv, left, meth, **opts)
  return rbs if rbs

  compound_fallback_type(left, right, meth, **opts)
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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String)


134
135
136
# File 'lib/docscribe/infer/returns.rb', line 134

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

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

Note:

module_function: defines #infer_pair_elem (visibility: private)

Infer one pair element type, normalizing unknown to nil.

Parameters:

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

    element node

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


960
961
962
963
# File 'lib/docscribe/infer/returns.rb', line 960

def infer_pair_elem(node, **opts)
  type = run_last_expr_type(node, **opts)
  type unless unknown_type?(type)
end

.infer_return_type(method_source) ⇒ String

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)
  • (String)

    if Parser::SyntaxError

Raises:

  • (Parser::SyntaxError)


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

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


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

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

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


1972
1973
1974
# File 'lib/docscribe/infer/returns.rb', line 1972

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 (String, Symbol, nil)

    the local variable name

  • local_var_types (Hash<String, String>?)

    inferred local variable type map

  • param_types (Hash<String, String>?)

    parameter name to type map

Returns:

  • (String, nil)


1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
# File 'lib/docscribe/infer/returns.rb', line 1927

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

    return val
  end
  return param_types[lvar_name.to_s] if param_types&.key?(lvar_name.to_s)

  nil
end

.op_asgn_expr_opts(**opts) ⇒ Hash<Symbol, Object>

Note:

module_function: defines #op_asgn_expr_opts (visibility: private)

Parameters:

  • opts (Hash)

Returns:

  • (Hash<Symbol, Object>)


473
474
475
476
477
478
479
480
481
482
483
# File 'lib/docscribe/infer/returns.rb', line 473

def op_asgn_expr_opts(**opts)
  {
    fallback_type: opts[:fallback_type] || FALLBACK_TYPE,
    nil_as_optional: true,
    local_var_types: opts[:local_var_types],
    param_types: opts[:param_types],
    core_rbs_provider: opts[:core_rbs_provider],
    signature_provider: opts[:signature_provider],
    container: opts[:container]
  }
end

.op_asgn_fallback_type(left, right, meth, **opts) ⇒ String?

Note:

module_function: defines #op_asgn_fallback_type (visibility: private)

Parameters:

  • left (String, nil)
  • right (String, nil)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


523
524
525
526
527
528
529
# File 'lib/docscribe/infer/returns.rb', line 523

def op_asgn_fallback_type(left, right, meth, **opts)
  fallback = (opts[:fallback_type] || FALLBACK_TYPE).to_s
  return synthesize_shovel_type(left, right, fallback: fallback) if shovel_method?(left, meth, opts[:core_rbs_provider])

  fallback_concrete_type(left, right, fallback) ||
    unify_types(left, right, fallback_type: fallback, nil_as_optional: true)
end

.op_asgn_left_type(lhs, **opts) ⇒ String?

Note:

module_function: defines #op_asgn_left_type (visibility: private)

Parameters:

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

    the lhs target node

  • opts (Hash)

Returns:

  • (String, nil)


423
424
425
426
427
428
429
430
# File 'lib/docscribe/infer/returns.rb', line 423

def op_asgn_left_type(lhs, **opts)
  name = op_asgn_var_name(lhs)
  if name
    found = op_asgn_lookup_type(lhs, name, **opts)
    return found if found
  end
  run_last_expr_type(lhs, **op_asgn_expr_opts(**opts))
end

.op_asgn_lookup_type(lhs, name, **opts) ⇒ String?

Note:

module_function: defines #op_asgn_lookup_type (visibility: private)

Parameters:

  • lhs (Parser::AST::Node)
  • name (String)
  • opts (Hash)

Returns:

  • (String, nil)


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

def op_asgn_lookup_type(lhs, name, **opts)
  return nil unless lhs.is_a?(Parser::AST::Node)

  case lhs&.type
  when :lvasgn
    lookup_lvar_type(name, opts[:local_var_types], opts[:param_types])
  when :ivasgn, :gvasgn, :cvasgn, :casgn
    opts[:local_var_types]&.fetch(name, nil)
  end
end

.op_asgn_rbs_type(lhs, left, meth, **opts) ⇒ String?

Note:

module_function: defines #op_asgn_rbs_type (visibility: private)

Parameters:

  • lhs (Parser::AST::Node, nil)
  • left (String, nil)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


491
492
493
494
495
496
497
498
# File 'lib/docscribe/infer/returns.rb', line 491

def op_asgn_rbs_type(lhs, left, meth, **opts)
  recv = cleaned_recv_type(left) ||
         receiver_rbs_type_name(lhs, opts[:core_rbs_provider],
                                opts[:local_var_types], opts[:param_types])
  return nil unless recv && meth

  resolve_op_asgn_rbs(recv, meth, **opts)
end

.op_asgn_right_type(rhs, **opts) ⇒ String?

Note:

module_function: defines #op_asgn_right_type (visibility: private)

Parameters:

  • rhs (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


464
465
466
467
468
# File 'lib/docscribe/infer/returns.rb', line 464

def op_asgn_right_type(rhs, **opts)
  return nil unless rhs

  run_last_expr_type(rhs, **op_asgn_expr_opts(**opts))
end

.op_asgn_var_name(lhs) ⇒ String?

Note:

module_function: defines #op_asgn_var_name (visibility: private)

Parameters:

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

Returns:

  • (String, nil)


435
436
437
438
439
440
441
442
# File 'lib/docscribe/infer/returns.rb', line 435

def op_asgn_var_name(lhs)
  return nil unless lhs.is_a?(Parser::AST::Node)

  case lhs&.type
  when :lvasgn, :ivasgn, :gvasgn, :cvasgn then lhs&.children&.[](0).to_s
  when :casgn then lhs&.children&.[](1).to_s
  end
end

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

Note:

module_function: defines #or_asgn_name_and_value (visibility: private)

Extract the name and value from an :or_asgn (||=) node.

Unlike :op_asgn (three children: target, operator, value), :or_asgn carries only target and value.

Parameters:

  • node (Parser::AST::Node)

    the :or_asgn AST node

Returns:

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


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

def or_asgn_name_and_value(node)
  target = node.children[0]
  return [nil, nil] unless target.is_a?(Parser::AST::Node)

  [target.children.first.to_s, node.children[1]]
end

.or_prefer_concrete(left_type, right_type, fallback) ⇒ String?

Note:

module_function: defines #or_prefer_concrete (visibility: private)

Prefer the concrete side when the other is a fallback alias.

Parameters:

  • left_type (String, nil)

    left side inferred type

  • right_type (String, nil)

    right side inferred type

  • fallback (String)

    fallback type name

Returns:

  • (String, nil)

    preferred side or nil when neither applies



626
627
628
629
630
631
632
# File 'lib/docscribe/infer/returns.rb', line 626

def or_prefer_concrete(left_type, right_type, fallback)
  if fallback_alias?(left_type, fallback) && !fallback_alias?(right_type, fallback)
    right_type
  elsif fallback_alias?(right_type, fallback) && !fallback_alias?(left_type, fallback)
    left_type
  end
end

.pair_elem_type(node, arg_names, position, **opts) ⇒ String?

Note:

module_function: defines #pair_elem_type (visibility: private)

Infer one pair element, honoring block parameter positions.

Parameters:

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

    element node

  • arg_names (Array<String>)

    block parameter names

  • position (Integer)

    0 for key, 1 for value

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (String, nil)


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

def pair_elem_type(node, arg_names, position, **opts)
  if node&.type == :lvar && node.children[0].to_s == arg_names[position]
    return position == 1 ? 'Integer' : nil
  end

  infer_pair_elem(node, **opts)
end

.pair_literal?(body) ⇒ Boolean

Note:

module_function: defines #pair_literal? (visibility: private)

Whether the block body is a [k, v] pair literal.

Parameters:

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

    block body node

Returns:

  • (Boolean)


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

def pair_literal?(body)
  body = body.children.last if body&.type == :begin
  body&.type == :array && body.children.size == 2
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)


40
41
42
43
44
# File 'lib/docscribe/infer/returns.rb', line 40

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

.placeholder_token?(token) ⇒ Boolean

Note:

module_function: defines #placeholder_token? (visibility: private)

Parameters:

  • token (String)

Returns:

  • (Boolean)


1095
1096
1097
1098
# File 'lib/docscribe/infer/returns.rb', line 1095

def placeholder_token?(token)
  !!(token == 'untyped' || token.include?('::') || token =~ /\A[a-z]/ ||
    (token =~ /\A[A-Z][A-Za-z0-9_]*\z/ && !Docscribe::Types::Primitive.primitive?(token)))
end

.placeholder_tokens(inner_generic) ⇒ Array<String>

Note:

module_function: defines #placeholder_tokens (visibility: private)

Parameters:

  • inner_generic (String)

Returns:

  • (Array<String>)


1035
1036
1037
1038
1039
# File 'lib/docscribe/infer/returns.rb', line 1035

def placeholder_tokens(inner_generic)
  split_generic_args(inner_generic).select do |arg|
    placeholder_token?(arg.strip.delete_suffix('?').strip)
  end
end

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

Note:

module_function: defines #populate_returns_spec (visibility: private)

This method returns an undefined value.

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

Parameters:

  • spec (Hash<Symbol, Object>)

    the return spec hash to populate

  • body (Parser::AST::Node)

    the method body AST node

  • local_var_types (Hash<String, String>?)

    inferred local variable type map

  • opts (Hash)

    additional keyword options forwarded to type inference



120
121
122
123
124
125
126
# File 'lib/docscribe/infer/returns.rb', line 120

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

.populate_spec_with_types(spec, body, fallback_type: FALLBACK_TYPE, nil_as_optional: true, **opts) ⇒ void

Note:

module_function: defines #populate_spec_with_types (visibility: private)

This method returns an undefined value.

Parameters:

  • spec (Hash<Symbol, Object>)
  • body (Parser::AST::Node)
  • fallback_type (String?) (defaults to: FALLBACK_TYPE)
  • nil_as_optional (Boolean) (defaults to: true)
  • opts (Hash)


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

def populate_spec_with_types(spec, body, fallback_type: FALLBACK_TYPE, nil_as_optional: true, **opts)
  core_rbs_provider = opts[:core_rbs_provider]
  types = build_local_variable_types(body, core_rbs_provider: core_rbs_provider,
                                           param_types: opts[: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: opts[:param_types],
                                           container: opts[:container], signature_provider: opts[:signature_provider])
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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (Array<String>)

    list of inferred types from each branch



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

def process_case_branches(node, **opts)
  children = (node.children[1..] || []).compact
  branches = children.flat_map do |child|
    child.type == :when ? run_last_expr_type(child.children.last, **opts) : run_last_expr_type(child, **opts)
  end.compact
  branches << 'nil' unless children.last && children.last.type != :when
  branches
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 (Hash)

    additional keyword options forwarded to type inference

Returns:

  • (Array<String>)

    list of inferred types from each branch



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

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

Note:

module_function: defines #process_rescue_body (visibility: private)

This method returns an undefined value.

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

Parameters:

  • spec (Hash<Symbol, Object>)

    the return spec hash to populate

  • body (Parser::AST::Node)

    the :rescue AST node

  • opts (Hash)

    additional keyword options forwarded to type inference



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

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

Note:

module_function: defines #process_rescue_branches (visibility: private)

This method returns an undefined value.

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

Parameters:

  • spec (Hash<Symbol, Object>)

    the return spec hash to populate

  • body (Parser::AST::Node)

    the :rescue AST node

  • opts (Hash)

    additional keyword options forwarded to type inference



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

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 = rescue_branch_type(rescue_body, **opts) || opts[:fallback_type]
    spec[:rescues] << [exc_names, rtype]
  end
end

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

Note:

module_function: defines #rbs_fallback (visibility: private)

Parameters:

  • meth (Symbol)
  • recv (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1155
1156
1157
1158
1159
# File 'lib/docscribe/infer/returns.rb', line 1155

def rbs_fallback(meth, recv, **opts)
  return nil unless opts[:core_rbs_provider]

  send_rbs_type(recv, meth, **opts)
end

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

Note:

module_function: defines #receiver_begin_type (visibility: private)

Parameters:

  • recv (Parser::AST::Node, nil)
  • core_rbs_provider (Docscribe::Types::RBS::Provider?)
  • local_var_types (Hash<String, String>?)
  • param_types (Hash<String, String>?)

Returns:

  • (String, nil)


1753
1754
1755
1756
1757
1758
1759
1760
# File 'lib/docscribe/infer/returns.rb', line 1753

def receiver_begin_type(recv, core_rbs_provider, local_var_types, param_types)
  return nil unless recv.is_a?(Parser::AST::Node)

  inner = recv&.children&.[](0)
  return unless inner && recv&.children&.size == 1

  receiver_rbs_type_name(inner, core_rbs_provider, local_var_types, param_types)
end

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

Note:

module_function: defines #receiver_dispatch_type (visibility: private)

Parameters:

  • recv (Parser::AST::Node, nil)
  • core_rbs_provider (Docscribe::Types::RBS::Provider?)
  • local_var_types (Hash<String, String>?)
  • param_types (Hash<String, String>?)

Returns:

  • (String, nil)


1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
# File 'lib/docscribe/infer/returns.rb', line 1723

def receiver_dispatch_type(recv, core_rbs_provider, local_var_types, param_types)
  return nil unless recv.is_a?(Parser::AST::Node)

  case recv.type
  when :send, :csend then receiver_send_type(recv, core_rbs_provider, local_var_types, param_types)
  when :block then block_receiver_type(recv, core_rbs_provider, local_var_types, param_types)
  when :or, :and then receiver_or_and_type(recv, core_rbs_provider, local_var_types, param_types)
  when :begin then receiver_begin_type(recv, core_rbs_provider, local_var_types, param_types)
  end
end

.receiver_literal_type(recv) ⇒ String?

Note:

module_function: defines #receiver_literal_type (visibility: private)

Parameters:

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

Returns:

  • (String, nil)


1823
1824
1825
1826
1827
# File 'lib/docscribe/infer/returns.rb', line 1823

def receiver_literal_type(recv)
  return nil unless recv.is_a?(Parser::AST::Node)

  LITERAL_RBS_TYPES[recv&.type] if LITERAL_RBS_TYPES.key?(recv&.type)
end

.receiver_or_and_preference(left_clean, right_clean, left, right) ⇒ String?

Note:

module_function: defines #receiver_or_and_preference (visibility: private)

Parameters:

  • left_clean (String, nil)
  • right_clean (String, nil)
  • left (String, nil)
  • right (String, nil)

Returns:

  • (String, nil)


1793
1794
1795
1796
1797
1798
1799
# File 'lib/docscribe/infer/returns.rb', line 1793

def receiver_or_and_preference(left_clean, right_clean, left, right)
  preferred = single_clean_preference(left_clean, right_clean)
  return preferred if preferred
  return left_clean if both_clean_equal?(left_clean, right_clean)

  left_clean || right_clean || left || right
end

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

Note:

module_function: defines #receiver_or_and_type (visibility: private)

Parameters:

  • recv (Parser::AST::Node, nil)
  • core_rbs_provider (Docscribe::Types::RBS::Provider?)
  • local_var_types (Hash<String, String>?)
  • param_types (Hash<String, String>?)

Returns:

  • (String, nil)


1768
1769
1770
1771
1772
1773
1774
1775
1776
# File 'lib/docscribe/infer/returns.rb', line 1768

def receiver_or_and_type(recv, core_rbs_provider, local_var_types, param_types)
  return nil unless recv.is_a?(Parser::AST::Node)

  left = receiver_rbs_type_name(recv&.children&.[](0), core_rbs_provider, local_var_types, param_types)
  right = receiver_rbs_type_name(recv&.children&.[](1), core_rbs_provider, local_var_types, param_types)
  left_clean = resolve_cleaned_type(left)
  right_clean = resolve_cleaned_type(right)
  receiver_or_and_preference(left_clean, right_clean, left, right)
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 (Docscribe::Types::RBS::Provider?)

    core RBS type provider

  • local_var_types (Hash<String, String>?)

    inferred local variable types

  • param_types (Hash<String, String>?)

    parameter name-to-type map

Returns:

  • (String, nil)


1707
1708
1709
1710
1711
1712
1713
1714
1715
# File 'lib/docscribe/infer/returns.rb', line 1707

def receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
  return unless recv

  literal = receiver_literal_type(recv)
  return literal if literal
  return receiver_var_type(recv, local_var_types, param_types) if var_receiver?(recv)

  receiver_dispatch_type(recv, core_rbs_provider, local_var_types, param_types)
end

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

Note:

module_function: defines #receiver_send_type (visibility: private)

Parameters:

  • recv (Parser::AST::Node, nil)
  • core_rbs_provider (Docscribe::Types::RBS::Provider?)
  • local_var_types (Hash<String, String>?)
  • param_types (Hash<String, String>?)

Returns:

  • (String, nil)


1883
1884
1885
1886
1887
# File 'lib/docscribe/infer/returns.rb', line 1883

def receiver_send_type(recv, core_rbs_provider, local_var_types, param_types)
  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

.receiver_var_type(recv, local_var_types, param_types) ⇒ String?

Note:

module_function: defines #receiver_var_type (visibility: private)

Parameters:

  • recv (Parser::AST::Node, nil)
  • local_var_types (Hash<String, String>?)
  • param_types (Hash<String, String>?)

Returns:

  • (String, nil)


1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
# File 'lib/docscribe/infer/returns.rb', line 1843

def receiver_var_type(recv, local_var_types, param_types)
  raw = lookup_lvar_type(recv.children.first, local_var_types, param_types)
  return nil unless raw

  cleaned = raw.include?(',') ? stripped_union_type(raw) : raw
  cleaned = cleaned.to_s.strip.delete_suffix('?').strip
  return nil if cleaned.empty? || cleaned == 'FALLBACK_TYPE'

  cleaned
end

.rescue_branch_type(rescue_body, **opts) ⇒ String?

Note:

module_function: defines #rescue_branch_type (visibility: private)

Infer a rescue branch type, resolving data constants precisely.

Rescue branches form the documented contract (@return [X] if Error), so a bare data constant (e.g., FALLBACK_TYPE, whose runtime value is the String 'Object') infers as its value type instead of the bare fallback. Other nodes use the standard path unchanged.

Parameters:

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

    rescue branch body node

  • opts (Hash)

    additional keyword options forwarded to inference

Returns:

  • (String, nil)

    inferred branch type or nil



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

def rescue_branch_type(rescue_body, **opts)
  if rescue_body.is_a?(Parser::AST::Node) && rescue_body.type == :const
    resolve_const_value_type(rescue_body, opts[:container]) ||
      run_last_expr_type(rescue_body, **opts)
  else
    run_last_expr_type(rescue_body, **opts)
  end
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 (Docscribe::Types::RBS::Provider?)

    core RBS type lookup provider

  • local_var_types (Hash<String, String>?)

    pre-built local variable types map

  • param_types (Hash<String, String>?)

    parameter name -> type map for lvar resolution

Returns:

  • (String, nil)


1948
1949
1950
1951
1952
1953
1954
1955
1956
# File 'lib/docscribe/infer/returns.rb', line 1948

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_cleaned_type(type) ⇒ String?

Note:

module_function: defines #resolve_cleaned_type (visibility: private)

Parameters:

  • type (String, nil)

Returns:

  • (String, nil)


1781
1782
1783
1784
1785
# File 'lib/docscribe/infer/returns.rb', line 1781

def resolve_cleaned_type(type)
  return nil unless type

  cleaned_recv_type(type) || type
end

.resolve_compound_rbs(recv_type, meth, **opts) ⇒ String?

Note:

module_function: defines #resolve_compound_rbs (visibility: private)

Parameters:

  • recv_type (String)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
# File 'lib/docscribe/infer/returns.rb', line 1534

def resolve_compound_rbs(recv_type, meth, **opts)
  if opts[:core_rbs_provider]
    rbs = resolve_rbs_return_type(recv_type, meth, opts[:core_rbs_provider])
    return substitute_rbs_type(rbs, recv_type) unless rbs == FALLBACK_TYPE
  end
  if opts[:signature_provider]
    sig = opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)
    return substitute_rbs_type(sig.return_type, recv_type) if sig
  end
  nil
end

.resolve_const_value_type(node, container) ⇒ String?

Note:

module_function: defines #resolve_const_value_type (visibility: private)

Resolve a const node to the YARD type of its runtime value.

Looks the constant up in this process through the analyzed lexical scope (innermost container outward, then top level) and maps its value's class to a type name. Only constants actually present here resolve — user code is never loaded, so unknown names safely fall through to the fallback path. Classes and modules are skipped so references like String keep name-based inference.

Parameters:

  • node (Parser::AST::Node)

    the :const node

  • container (String, nil)

    lexical container (e.g. "Foo::Bar")

Returns:

  • (String, nil)

    YARD type of the value, or nil when unresolvable



2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
# File 'lib/docscribe/infer/returns.rb', line 2031

def resolve_const_value_type(node, container)
  segments, absolute = const_path_segments(node)
  return nil if segments.empty?

  *scope_parts, const_name = segments
  const_owner_paths(container, scope_parts, absolute).each do |owner_path|
    found, value = runtime_const_lookup(owner_path, const_name)
    next unless found

    type = yard_type_for_const_value(value)
    return type if type
  end
  nil
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 (Docscribe::Types::RBS::Provider?)

    core RBS type lookup provider

  • local_var_types (Hash<String, String>?)

    pre-built local variable types map

  • param_types (Hash<String, String>?)

    parameter name -> type map for lvar resolution

Returns:

  • (String, nil)


1911
1912
1913
1914
1915
1916
1917
1918
# File 'lib/docscribe/infer/returns.rb', line 1911

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_op_asgn_rbs(recv_type, meth, **opts) ⇒ String?

Note:

module_function: defines #resolve_op_asgn_rbs (visibility: private)

Parameters:

  • recv_type (String)
  • meth (Symbol)
  • opts (Hash)

Returns:

  • (String, nil)


505
506
507
508
509
510
511
512
513
514
515
# File 'lib/docscribe/infer/returns.rb', line 505

def resolve_op_asgn_rbs(recv_type, meth, **opts)
  if opts[:core_rbs_provider]
    rbs = resolve_rbs_return_type(recv_type, meth, opts[:core_rbs_provider])
    return substitute_rbs_type(rbs, recv_type) unless rbs == FALLBACK_TYPE
  end
  if opts[:signature_provider]
    sig = opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)
    return substitute_rbs_type(sig.return_type, recv_type) if sig
  end
  nil
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 (Docscribe::Types::RBS::Provider?)

    optional RBS provider for core type lookup

  • local_var_types (Hash<String, String>?)

    inferred local variable type map

  • param_types (Hash<String, String>?)

    parameter name to type map

Returns:

  • (String, nil)

    resolved type or nil if unresolvable



1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
# File 'lib/docscribe/infer/returns.rb', line 1366

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 substitute_rbs_type(rbs, recv_type) 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 (Hash)

    additional keyword options

Returns:

  • (String, nil)


1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
# File 'lib/docscribe/infer/returns.rb', line 1387

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

  rbs = opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)&.return_type
  rbs ? substitute_rbs_type(rbs, recv_type) : nil
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 (Docscribe::Types::RBS::Provider?)

    core RBS type lookup provider

Returns:

  • (String)

    inferred return type



2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
# File 'lib/docscribe/infer/returns.rb', line 2174

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

.resolve_self_via_rbs?(base, meth, provider) ⇒ Boolean

Note:

module_function: defines #resolve_self_via_rbs? (visibility: private)

Parameters:

Returns:

  • (Boolean)


1647
1648
1649
1650
1651
# File 'lib/docscribe/infer/returns.rb', line 1647

def resolve_self_via_rbs?(base, meth, provider)
  return false unless provider

  resolve_rbs_return_type(base, meth, provider) == 'self'
end

.returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, **opts) ⇒ Hash<Symbol, 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 (Docscribe::Types::RBS::Provider?) (defaults to: nil)

    core RBS type lookup provider

  • opts (Hash)

Returns:

  • (Hash<Symbol, Object>)


73
74
75
76
77
78
79
80
81
82
# 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, **opts)
  body = extract_def_body(node)
  spec = { normal: FALLBACK_TYPE, rescues: [] } #: Hash[Symbol, untyped]
  return spec unless body

  populate_spec_with_types(spec, body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
                                       core_rbs_provider: core_rbs_provider, **opts)
  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 (Hash)

    options passed through as keyword args

Returns:

  • (String, nil)


1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
# File 'lib/docscribe/infer/returns.rb', line 1982

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

.runtime_const_lookup(owner_path, const_name) ⇒ (Boolean, Object), Array

Note:

module_function: defines #runtime_const_lookup (visibility: private)

Look up a constant in this process without side effects.

Parameters:

  • owner_path (String)

    owner namespace path ("" is top level)

  • const_name (String)

    constant name to look up

Returns:

  • ((Boolean, Object))

    found flag with value (nil value is valid)

  • (Array)

    if StandardError

Raises:

  • (StandardError)


2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
# File 'lib/docscribe/infer/returns.rb', line 2094

def runtime_const_lookup(owner_path, const_name)
  owner = owner_path.empty? ? Object : safe_const_path(owner_path.split('::'))
  return [false, nil] unless owner.is_a?(Module)
  return [false, nil] unless owner.const_defined?(const_name, false)
  return [false, nil] if owner.autoload?(const_name)

  [true, owner.const_get(const_name, false)]
rescue StandardError
  [false, nil]
end

.safe_const_path(parts) ⇒ Module?

Note:

module_function: defines #safe_const_path (visibility: private)

Resolve a namespace path in this process without side effects.

Parameters:

  • parts (Array<String>)

    namespace segments

Returns:

  • (Module, nil)

    the namespace or nil when unresolvable



2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
# File 'lib/docscribe/infer/returns.rb', line 2110

def safe_const_path(parts)
  mod = Object #: Module
  parts.each do |name|
    fetched = safe_const_step(mod, name)
    return nil unless fetched

    mod = fetched
  end
  mod
end

.safe_const_step(mod, name) ⇒ Module?

Note:

module_function: defines #safe_const_step (visibility: private)

Resolve one namespace step without side effects.

Parameters:

  • mod (Module)

    current namespace

  • name (String)

    nested constant name

Returns:

  • (Module, nil)

    nested namespace or nil when unresolvable

  • (nil)

    if StandardError

Raises:

  • (StandardError)


2129
2130
2131
2132
2133
2134
2135
2136
# File 'lib/docscribe/infer/returns.rb', line 2129

def safe_const_step(mod, name)
  return nil unless mod.const_defined?(name, false) && !mod.autoload?(name)

  fetched = mod.const_get(name, false)
  fetched if fetched.is_a?(Module)
rescue StandardError
  nil
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 (Hash)

    additional keyword options

Returns:

  • (String, nil)


1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
# File 'lib/docscribe/infer/returns.rb', line 1343

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

.shovel_array_type(left_str, right_str, base, fallback) ⇒ String

Note:

module_function: defines #shovel_array_type (visibility: private)

Parameters:

  • left_str (String)
  • right_str (String)
  • base (String)
  • fallback (String)

Returns:

  • (String)


1674
1675
1676
1677
1678
1679
1680
1681
1682
# File 'lib/docscribe/infer/returns.rb', line 1674

def shovel_array_type(left_str, right_str, base, fallback)
  return left_str if shovel_left_generic?(left_str)
  return left_str if shovel_right_invalid?(right_str, fallback)

  cleaned = right_str.to_s.strip.delete_suffix('?').strip
  return left_str if cleaned == 'nil' || cleaned.empty? || cleaned == FALLBACK_TYPE

  "#{base}<#{cleaned}>"
end

.shovel_left_generic?(str) ⇒ Boolean

Note:

module_function: defines #shovel_left_generic? (visibility: private)

Parameters:

  • str (String)

Returns:

  • (Boolean)


1687
1688
1689
# File 'lib/docscribe/infer/returns.rb', line 1687

def shovel_left_generic?(str)
  str.include?('<') || str.include?('[')
end

.shovel_method?(left, meth, provider) ⇒ Boolean

Note:

module_function: defines #shovel_method? (visibility: private)

Whether left#meth is shovel (returns self) via RBS dynamically, not hardcoding :<<.

Parameters:

Returns:

  • (Boolean)

    true if shovel



1632
1633
1634
1635
1636
1637
1638
1639
1640
# File 'lib/docscribe/infer/returns.rb', line 1632

def shovel_method?(left, meth, provider)
  return false unless left && meth

  base = left.split(/[<\[ ]/).first.to_s.strip.delete_suffix('?')
  return false if base.empty?
  return true if resolve_self_via_rbs?(base, meth, provider)

  resolve_self_via_rbs?(base, meth, core_rbs_provider)
end

.shovel_right_invalid?(str, fallback) ⇒ Boolean

Note:

module_function: defines #shovel_right_invalid? (visibility: private)

Parameters:

  • str (String)
  • fallback (String)

Returns:

  • (Boolean)


1695
1696
1697
# File 'lib/docscribe/infer/returns.rb', line 1695

def shovel_right_invalid?(str, fallback)
  str == fallback || %w[Object untyped].include?(str)
end

.single_clean_preference(left_clean, right_clean) ⇒ String?

Note:

module_function: defines #single_clean_preference (visibility: private)

Parameters:

  • left_clean (String, nil)
  • right_clean (String, nil)

Returns:

  • (String, nil)


1805
1806
1807
1808
1809
1810
# File 'lib/docscribe/infer/returns.rb', line 1805

def single_clean_preference(left_clean, right_clean)
  return left_clean if left_clean && !right_clean
  return right_clean if right_clean && !left_clean

  nil
end

.split_generic_args(inner) ⇒ Array<String>

Note:

module_function: defines #split_generic_args (visibility: private)

Split a generic inner string like Integer, Array<(Integer, String)> by top-level commas.

Respects nesting of < > [ ] ( ) so tuples are not split.

Parameters:

  • inner (String)

    the raw inner string

Returns:

  • (Array<String>)


2313
2314
2315
2316
2317
2318
2319
# File 'lib/docscribe/infer/returns.rb', line 2313

def split_generic_args(inner)
  state = { parts: [], cur: +'', da: 0, db: 0, dp: 0 } #: Hash[Symbol, untyped]
  inner.each_char { |chr| split_process_char(chr, state, strip: true) }
  last = state[:cur].strip
  state[:parts] << last unless last.empty?
  state[:parts]
end

.split_handle_bracket(chr, state) ⇒ void

Note:

module_function: defines #split_handle_bracket (visibility: private)

This method returns an undefined value.

Parameters:

  • chr (String)

    bracket character

  • state (Hash<Symbol, Object>)

    mutable split state



2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
# File 'lib/docscribe/infer/returns.rb', line 2341

def split_handle_bracket(chr, state)
  case chr
  when '<' then state[:da] += 1
  when '>' then state[:da] -= 1
  when '[' then state[:db] += 1
  when ']' then state[:db] -= 1
  when '(' then state[:dp] += 1
  when ')' then state[:dp] -= 1
  end
  state[:cur] << chr
end

.split_handle_comma(state, strip:) ⇒ void

Note:

module_function: defines #split_handle_comma (visibility: private)

This method returns an undefined value.

Parameters:

  • state (Hash<Symbol, Object>)

    mutable split state

  • strip (Boolean)

    whether to strip



2357
2358
2359
2360
2361
2362
2363
2364
2365
# File 'lib/docscribe/infer/returns.rb', line 2357

def split_handle_comma(state, strip:)
  if state[:da].zero? && state[:db].zero? && state[:dp].zero?
    part = strip ? state[:cur].strip : state[:cur]
    state[:parts] << part
    state[:cur] = +''
  else
    state[:cur] << ','
  end
end

.split_process_char(chr, state, strip:) ⇒ void

Note:

module_function: defines #split_process_char (visibility: private)

This method returns an undefined value.

Parameters:

  • chr (String)

    single character

  • state (Hash<Symbol, Object>)

    mutable split state

  • strip (Boolean)

    whether to strip parts on comma



2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
# File 'lib/docscribe/infer/returns.rb', line 2326

def split_process_char(chr, state, strip:)
  case chr
  when '<', '>', '[', ']', '(', ')'
    split_handle_bracket(chr, state)
  when ','
    split_handle_comma(state, strip: strip)
  else
    state[:cur] << chr
  end
end

.split_top_level_commas(str) ⇒ Array<String>

Note:

module_function: defines #split_top_level_commas (visibility: private)

Split a type string by top-level commas (outside any < > [ ] ( ) nesting).

Used to distinguish union types (String, nil) from generic commas (Hash<Integer, String>).

Parameters:

  • str (String)

    the type string to split

Returns:

  • (Array<String>)


1870
1871
1872
1873
1874
1875
# File 'lib/docscribe/infer/returns.rb', line 1870

def split_top_level_commas(str)
  state = { parts: [], cur: +'', da: 0, db: 0, dp: 0 } #: Hash[Symbol, untyped]
  str.each_char { |chr| split_process_char(chr, state, strip: false) }
  state[:parts] << state[:cur] unless state[:cur].empty?
  state[:parts]
end

.string_like_method?(meth) ⇒ Boolean

Note:

module_function: defines #string_like_method? (visibility: private)

Parameters:

  • meth (Symbol)

Returns:

  • (Boolean)


1299
1300
1301
# File 'lib/docscribe/infer/returns.rb', line 1299

def string_like_method?(meth)
  %i[to_s to_str inspect].include?(meth)
end

.string_send_type(meth, recv) ⇒ String?

Note:

module_function: defines #string_send_type (visibility: private)

Parameters:

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

Returns:

  • (String, nil)


1288
1289
1290
1291
1292
1293
1294
# File 'lib/docscribe/infer/returns.rb', line 1288

def string_send_type(meth, recv)
  return 'String' if string_like_method?(meth)
  return 'String' if file_join_method?(meth, recv)
  return 'String' if sub_string_method?(meth, recv)

  nil
end

.stripped_union_type(raw) ⇒ String?

Note:

module_function: defines #stripped_union_type (visibility: private)

Parameters:

  • raw (String, nil)

Returns:

  • (String, nil)


1857
1858
1859
1860
1861
# File 'lib/docscribe/infer/returns.rb', line 1857

def stripped_union_type(raw)
  parts = split_top_level_commas(raw).map { |p| p.strip.delete_suffix('?').strip }
  non_nil = parts.reject { |p| %w[nil FALLBACK_TYPE].include?(p) }
  (non_nil.first || parts.first).to_s.strip.delete_suffix('?').strip
end

.sub_string_method?(meth, recv) ⇒ Boolean

Note:

module_function: defines #sub_string_method? (visibility: private)

Parameters:

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

Returns:

  • (Boolean)


1315
1316
1317
# File 'lib/docscribe/infer/returns.rb', line 1315

def sub_string_method?(meth, recv)
  meth == :sub && recv && recv&.type != :const
end

.substitute_placeholders(rbs_type, placeholders, inner) ⇒ String

Note:

module_function: defines #substitute_placeholders (visibility: private)

Parameters:

  • rbs_type (String)
  • placeholders (Array<String>)
  • inner (String)

Returns:

  • (String)


1046
1047
1048
1049
1050
1051
1052
1053
# File 'lib/docscribe/infer/returns.rb', line 1046

def substitute_placeholders(rbs_type, placeholders, inner)
  result = rbs_type.dup
  placeholders.each do |ph|
    token = ph.strip.delete_suffix('?').strip
    result = result.gsub(token, inner)
  end
  result
end

.substitute_rbs_type(rbs, recv_type) ⇒ String

Note:

module_function: defines #substitute_rbs_type (visibility: private)

Substitute self and generic type variables in an RBS return type with the concrete receiver type.

Handles Array#<< (self -> Array<Elem>) and Hash#[] (V -> value type). For self returns the concrete receiver type; for K/V/Elem substitutes from generic args.

Parameters:

  • rbs (String)

    the raw RBS return type string

  • recv_type (String)

    the concrete receiver type string

Returns:

  • (String)


2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
# File 'lib/docscribe/infer/returns.rb', line 2195

def substitute_rbs_type(rbs, recv_type)
  self_sub = substitute_self_type(rbs, recv_type)
  return self_sub if self_sub

  inner = extract_generic_inner(recv_type)
  return rbs unless inner

  args = split_generic_args(inner)
  return rbs if args.empty?

  substitute_with_mapping(rbs, recv_type, args)
end

.substitute_self_type(rbs, recv_type) ⇒ String?

Note:

module_function: defines #substitute_self_type (visibility: private)

Parameters:

  • rbs (String)
  • recv_type (String)

Returns:

  • (String, nil)


2212
2213
2214
2215
2216
2217
# File 'lib/docscribe/infer/returns.rb', line 2212

def substitute_self_type(rbs, recv_type)
  return recv_type if rbs == 'self'
  return "#{recv_type}?" if rbs == 'self?'

  nil
end

.substitute_with_mapping(rbs, recv_type, args) ⇒ String

Note:

module_function: defines #substitute_with_mapping (visibility: private)

Parameters:

  • rbs (String)
  • recv_type (String)
  • args (Array<String>)

Returns:

  • (String)


2224
2225
2226
2227
2228
2229
# File 'lib/docscribe/infer/returns.rb', line 2224

def substitute_with_mapping(rbs, recv_type, args)
  mapping = build_generic_mapping(recv_type, args)
  return rbs if mapping.empty?

  apply_generic_mapping(rbs, mapping, recv_type)
end

.synthesize_shovel_type(left, right, fallback:) ⇒ String

Note:

module_function: defines #synthesize_shovel_type (visibility: private)

Parameters:

  • left (String, nil)
  • right (String, nil)
  • fallback (String)

Returns:

  • (String)


1616
1617
1618
1619
1620
1621
1622
1623
# File 'lib/docscribe/infer/returns.rb', line 1616

def synthesize_shovel_type(left, right, fallback:)
  l = left || fallback
  r = right || fallback
  base = l.split(/[<\[ ]/).first.to_s.strip.delete_suffix('?')
  return shovel_array_type(l, r, base, fallback) if %w[Array Set Enumerable Enumerator].include?(base)

  l
end

.synthetic_enumerator_type(node, meth, recv, **opts) ⇒ String?

Note:

module_function: defines #synthetic_enumerator_type (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • meth (Symbol)
  • recv (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1167
1168
1169
1170
1171
1172
1173
1174
# File 'lib/docscribe/infer/returns.rb', line 1167

def synthetic_enumerator_type(node, meth, recv, **opts)
  return unless meth == :each_with_index && node.children.size == 2

  elem = enumerator_elem_from_recv(recv, **opts)
  return "Enumerator<#{elem}, Integer>" if elem

  'Enumerator<Object, Integer>'
end

.synthetic_hash_type(_node, meth, recv, **opts) ⇒ String?

Note:

module_function: defines #synthetic_hash_type (visibility: private)

Parameters:

  • _node (Parser::AST::Node)
  • meth (Symbol)
  • recv (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1226
1227
1228
1229
1230
1231
1232
# File 'lib/docscribe/infer/returns.rb', line 1226

def synthetic_hash_type(_node, meth, recv, **opts)
  return unless meth == :to_h && recv && recv.type == :send && recv.children[1] == :each_with_index

  inner_recv = recv.children[0]
  elem = hash_elem_from_enumerator(inner_recv, **opts) || 'Object'
  "Hash<#{elem}, Integer>"
end

.to_h_block_pair_types(body, arg_names, **opts) ⇒ (String, nil, String, nil)

Note:

module_function: defines #to_h_block_pair_types (visibility: private)

Infer key/value types from a [k, v] pair literal block body.

A bare lvar matching an each_with_index block parameter resolves structurally: first parameter is the element (resolved from the receiver by the caller), second parameter is always the Integer index.

Parameters:

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

    block body node

  • arg_names (Array<String>)

    block parameter names

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • ((String, nil, String, nil))

    inferred key and value types, nil when unavailable



920
921
922
923
924
925
926
# File 'lib/docscribe/infer/returns.rb', line 920

def to_h_block_pair_types(body, arg_names, **opts)
  return [nil, nil] unless pair_literal?(body)

  pair = body.type == :begin ? body.children.last : body
  [pair_elem_type(pair.children[0], arg_names, 0, **opts),
   pair_elem_type(pair.children[1], arg_names, 1, **opts)]
end

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

Note:

module_function: defines #to_h_each_recv (visibility: private)

Receiver of to_h when it chains off each_with_index.

Parameters:

  • node (Parser::AST::Node)

    block node

Returns:

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


875
876
877
878
879
# File 'lib/docscribe/infer/returns.rb', line 875

def to_h_each_recv(node)
  send_node = node.children[0]
  recv = send_node.children[0]
  recv if recv&.type == :send && recv.children[1] == :each_with_index
end

.to_h_key_value(node, recv, **opts) ⇒ (String, String)

Note:

module_function: defines #to_h_key_value (visibility: private)

Resolve key/value types for a to_h block.

Parameters:

  • node (Parser::AST::Node)

    block node

  • recv (Parser::AST::Node)

    each_with_index send node

  • opts (Hash)

    additional keyword options forwarded to type inference

Returns:

  • ((String, String))


888
889
890
891
892
893
894
895
# File 'lib/docscribe/infer/returns.rb', line 888

def to_h_key_value(node, recv, **opts)
  body = node.children[2]
  key, value = to_h_block_pair_types(body, block_arg_names(node), **opts)
  key ||= hash_elem_from_enumerator(recv.children[0], **opts)
  key = 'Object' if unknown_type?(key)
  value ||= pair_literal?(body) ? 'Object' : 'Integer'
  [key, value]
end

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

Note:

module_function: defines #try_compound (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • opts (Hash)

Returns:

  • (String, nil)


1146
1147
1148
# File 'lib/docscribe/infer/returns.rb', line 1146

def try_compound(node, **opts)
  infer_from_compound_assign(node, **opts)
end

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

Note:

module_function: defines #try_rbs (visibility: private)

Parameters:

  • meth (Symbol)
  • recv (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1133
1134
1135
1136
1137
1138
1139
1140
# File 'lib/docscribe/infer/returns.rb', line 1133

def try_rbs(meth, recv, **opts)
  return nil unless opts[:core_rbs_provider]

  rbs = send_rbs_type(recv, meth, **opts)
  return rbs if rbs && rbs != FALLBACK_TYPE && !rbs.include?('Object')

  nil
end

.try_synthetic(node, meth, recv, **opts) ⇒ String?

Note:

module_function: defines #try_synthetic (visibility: private)

Parameters:

  • node (Parser::AST::Node)
  • meth (Symbol)
  • recv (Parser::AST::Node, nil)
  • opts (Hash)

Returns:

  • (String, nil)


1123
1124
1125
1126
# File 'lib/docscribe/infer/returns.rb', line 1123

def try_synthetic(node, meth, recv, **opts)
  synthetic_enumerator_type(node, meth, recv, **opts) ||
    synthetic_hash_type(node, meth, recv, **opts)
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)


1895
1896
1897
1898
1899
1900
# File 'lib/docscribe/infer/returns.rb', line 1895

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)


2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
# File 'lib/docscribe/infer/returns.rb', line 2406

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 non_nil if non_nil.end_with?('?')

    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)


2380
2381
2382
2383
2384
2385
2386
# File 'lib/docscribe/infer/returns.rb', line 2380

def unify_types(type_a, type_b, fallback_type:, nil_as_optional:)
  type_a = coalesce_type(type_a, fallback_type)
  type_b = coalesce_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

.unknown_type?(type) ⇒ Boolean

Note:

module_function: defines #unknown_type? (visibility: private)

Whether a type string means "could not determine".

Parameters:

  • type (String, nil)

    inferred type string

Returns:

  • (Boolean)


970
971
972
# File 'lib/docscribe/infer/returns.rb', line 970

def unknown_type?(type)
  type.nil? || %w[Object untyped nil].include?(type)
end

.var_receiver?(recv) ⇒ Boolean

Note:

module_function: defines #var_receiver? (visibility: private)

Parameters:

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

Returns:

  • (Boolean)


1832
1833
1834
1835
1836
# File 'lib/docscribe/infer/returns.rb', line 1832

def var_receiver?(recv)
  return false unless recv.is_a?(Parser::AST::Node)

  %i[lvar ivar gvar cvar].include?(recv&.type)
end

.yard_type_for_const_value(value) ⇒ String?

Note:

module_function: defines #yard_type_for_const_value (visibility: private)

Map a constant's runtime value to a YARD type name.

Parameters:

  • value (Object)

    the constant's runtime value

Returns:

  • (String, nil)

    YARD type or nil when not mappable



2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
# File 'lib/docscribe/infer/returns.rb', line 2143

def yard_type_for_const_value(value)
  return 'nil' if value.nil?
  return 'Boolean' if value.is_a?(TrueClass) || value.is_a?(FalseClass)
  return nil if value.is_a?(Module)

  name = value.class.name
  return nil unless name.is_a?(String) && name.match?(/\A[A-Z][A-Za-z0-9_:]*\z/)

  name
end