class YARD::Handlers::RBS::MethodHandler

Handles RBS method definitions (def name: signature).

Creates a {YARD::CodeObjects::MethodObject} for each declaration and infers @param, @return, @yield, and @yieldparam tags from the RBS type signature when those tags are absent from the docstring.

Public Class Methods

rbs_type_to_yard_types(rbs) click to toggle source

Convert an RBS type string to an array of YARD type strings.

@param rbs [String] e.g. “String | Integer”, “Array”, “bool” @return [Array<String>]

# File lib/yard/handlers/rbs/method_handler.rb, line 30
def self.rbs_type_to_yard_types(rbs)
  rbs = rbs.strip
  return ['void']    if rbs == 'void'
  return ['Boolean'] if rbs == 'bool'
  return ['Object']  if rbs == 'untyped'
  return ['nil']     if rbs == 'nil'

  # Strip outer parentheses: `(String | Integer)` → recurse on inner.
  if rbs.start_with?('(') && rbs.end_with?(')') && bracket_depth(rbs[1..-2]) == 0
    return rbs_type_to_yard_types(rbs[1..-2])
  end

  # `Type?` is shorthand for `Type | nil` when the ? is outermost.
  if rbs =~ /\A(.+)\?\z/ && bracket_depth($1) == 0
    return rbs_type_to_yard_types($1) + ['nil']
  end

  split_on_pipe(rbs).map { |t| t.strip }
end

Private Class Methods

bracket_depth(str) click to toggle source

Return the bracket depth of the full string (should be 0 for well-formed types).

# File lib/yard/handlers/rbs/method_handler.rb, line 317
def self.bracket_depth(str)
  depth = 0
  str.each_char do |c|
    case c
    when '(', '[', '{' then depth += 1
    when ')', ']', '}' then depth -= 1
    end
  end
  depth
end
split_on_pipe(str) click to toggle source

Split str on ‘|` that are not inside brackets.

# File lib/yard/handlers/rbs/method_handler.rb, line 289
def self.split_on_pipe(str)
  depth = 0
  parts = []
  cur   = String.new('')
  str.each_char do |c|
    case c
    when '(', '[', '{'
      depth += 1
      cur << c
    when ')', ']', '}'
      depth -= 1
      cur << c
    when '|'
      if depth == 0
        parts << cur.strip
        cur = String.new('')
      else
        cur << c
      end
    else
      cur << c
    end
  end
  parts << cur.strip unless cur.strip.empty?
  parts
end

Private Instance Methods

add_overload_tag(obj, meth_name, sig) click to toggle source

Add an @overload tag for one signature overload.

# File lib/yard/handlers/rbs/method_handler.rb, line 86
def add_overload_tag(obj, meth_name, sig)
  parsed     = parse_function_type(sig)
  param_sigs = parsed[:params].reject { |p| p[:block] }.map.with_index do |p, idx|
    p[:name] || "arg#{idx}"
  end

  # Build the overload tag text:  signature line + nested @param/@return lines.
  lines = ["#{meth_name}(#{param_sigs.join(', ')})"]
  parsed[:params].reject { |p| p[:block] }.each_with_index do |p, idx|
    pname = p[:name] || "arg#{idx}"
    lines << "  @param #{pname} [#{p[:types].join(', ')}]"
  end

  if (blk = parsed[:block_param])
    add_yield_tags(obj, blk)
  end

  lines << "  @return [#{parsed[:return_types].join(', ')}]"
  obj.add_tag YARD::Tags::OverloadTag.new(:overload, lines.join("\n"))
end
add_param_return_tags(obj, sig) click to toggle source

Add @param / @return / @yield / @yieldparam from a single overload sig.

# File lib/yard/handlers/rbs/method_handler.rb, line 66
def add_param_return_tags(obj, sig)
  parsed = parse_function_type(sig)

  parsed[:params].each do |p|
    next if p[:block]  # block param handled via @yield below
    tag_name = p[:name] ? p[:name].to_s : nil
    next if tag_name && obj.tags(:param).any? { |t| t.name == tag_name }
    obj.add_tag YARD::Tags::Tag.new(:param, '', p[:types], tag_name)
  end

  if (blk = parsed[:block_param])
    add_yield_tags(obj, blk)
  end

  unless obj.has_tag?(:return)
    obj.add_tag YARD::Tags::Tag.new(:return, '', parsed[:return_types])
  end
end
add_yield_tags(obj, blk) click to toggle source

Add @yield and @yieldparam tags from a parsed block type.

# File lib/yard/handlers/rbs/method_handler.rb, line 108
def add_yield_tags(obj, blk)
  return if obj.has_tag?(:yield) && obj.has_tag?(:yieldparam)
  obj.add_tag YARD::Tags::Tag.new(:yield, '') unless obj.has_tag?(:yield)
  blk[:params].each_with_index do |p, idx|
    pname = p[:name] || "arg#{idx}"
    next if obj.tags(:yieldparam).any? { |t| t.name == pname }
    obj.add_tag YARD::Tags::Tag.new(:yieldparam, '', p[:types], pname)
  end
  unless obj.has_tag?(:yieldreturn)
    obj.add_tag YARD::Tags::Tag.new(:yieldreturn, '', self.class.rbs_type_to_yard_types(blk[:return_type] || 'void'))
  end
end
apply_signature_tags(obj, sigs) click to toggle source

Apply tags from all overload signatures to the method object.

# File lib/yard/handlers/rbs/method_handler.rb, line 53
def apply_signature_tags(obj, sigs)
  return if sigs.nil? || sigs.empty?

  if sigs.length == 1
    # Single signature: add @param and @return directly.
    add_param_return_tags(obj, sigs.first)
  else
    # Multiple signatures: add @overload tags.
    sigs.each { |sig| add_overload_tag(obj, statement.name, sig) }
  end
end
extract_type_and_name(str) click to toggle source

Split a type+name string like “Array names” into [“Array”, “names”]. The name is the trailing lowercase identifier (if any).

# File lib/yard/handlers/rbs/method_handler.rb, line 213
def extract_type_and_name(str)
  str = str.strip
  if str =~ /\A(.*\S)\s+([a-z_]\w*)\z/m
    type_part = $1.strip
    name_part = $2
    # Exclude RBS type keywords from being mistaken for names.
    unless %w[void untyped nil bool top bottom self instance class].include?(name_part)
      return [type_part, name_part] unless type_part.empty?
    end
  end
  [str, nil]
end
find_matching(str, start, open, close) click to toggle source

Find the index of the matching close bracket starting from start. @return [nil] if no matching bracket is found (malformed input).

# File lib/yard/handlers/rbs/method_handler.rb, line 247
def find_matching(str, start, open, close)
  depth = 0
  (start...str.length).each do |i|
    case str[i]
    when open  then depth += 1
    when close
      depth -= 1
      return i if depth == 0
    end
  end
  nil
end
parse_block_type(inner) click to toggle source

Parse the inside of a ‘{ … }` block type, e.g. “(Integer) -> String”.

# File lib/yard/handlers/rbs/method_handler.rb, line 227
def parse_block_type(inner)
  inner = inner.strip
  params = []
  ret    = nil

  if inner.start_with?('(')
    close  = find_matching(inner, 0, '(', ')')
    raise YARD::Parser::UndocumentableError, "malformed block type (unclosed '('): #{inner}" if close.nil?
    params = parse_params_list(inner[1...close])
    rest   = inner[close + 1..-1].lstrip
  else
    rest = inner
  end

  ret = $1.strip if rest =~ /\A->\s*(.*)\z/
  { :params => params, :return_type => ret }
end
parse_function_type(sig) click to toggle source

Parse a single RBS function type string (one overload) into its components.

@param sig [String] e.g. “(String name, Integer age) -> String” @return [Hash] { :params => […], :block_param => Hash|nil, :return_types => […] }

# File lib/yard/handlers/rbs/method_handler.rb, line 125
def parse_function_type(sig)
  sig = sig.strip
  return { :params => [], :block_param => nil, :return_types => ['void'] } if sig.empty?

  remaining = sig
  params = []
  block_param = nil

  # 1. Extract positional/keyword params: leading `(...)`.
  if remaining.start_with?('(')
    close = find_matching(remaining, 0, '(', ')')
    raise YARD::Parser::UndocumentableError, "malformed signature (unclosed '('): #{sig}" if close.nil?
    params_str = remaining[1...close]
    remaining  = remaining[close + 1..-1].lstrip
    params  = parse_params_list(params_str)
  end

  # 2. Extract block type: `{ ... }`.
  if remaining.start_with?('{')
    close = find_matching(remaining, 0, '{', '}')
    raise YARD::Parser::UndocumentableError, "malformed signature (unclosed '{'): #{sig}" if close.nil?
    block_inner  = remaining[1...close]
    remaining    = remaining[close + 1..-1].lstrip
    block_param  = parse_block_type(block_inner)
  end

  # 3. Return type after `->`.
  return_types = if remaining =~ /\A->\s*(.*)\z/
                   self.class.rbs_type_to_yard_types($1.strip)
                 else
                   ['void']
                 end

  { :params => params, :block_param => block_param, :return_types => return_types }
end
parse_params_list(str) click to toggle source

Parse a comma-separated parameter list (content inside outer parens).

# File lib/yard/handlers/rbs/method_handler.rb, line 162
def parse_params_list(str)
  str = str.strip
  return [] if str.empty?

  split_by_comma(str).map { |p| parse_single_param(p.strip) }.compact
end
parse_single_param(param) click to toggle source

Parse one parameter from an RBS param list.

# File lib/yard/handlers/rbs/method_handler.rb, line 170
def parse_single_param(param)
  return nil if param.empty?

  optional = false
  rest     = false

  # Optional marker `?`.
  if param.start_with?('?') && !param.start_with?('?(')
    optional = true
    param    = param[1..-1].lstrip
  end

  # Double-splat `**` (rest keyword).
  if param.start_with?('**')
    rest  = true
    param = param[2..-1].lstrip
  # Single-splat `*` (rest positional).
  elsif param.start_with?('*') && !param.start_with?('*)')
    rest  = true
    param = param[1..-1].lstrip
  end

  # Block-type proc: `^(...)`.
  if param.start_with?('^')
    return { :name => nil, :types => [param], :optional => false, :rest => false, :block => true }
  end

  # Keyword parameter: `name: Type` or `?name: Type`.
  if param =~ /\A([a-z_]\w*)\s*:\s*(.*)\z/ && !rest
    kw_name = $1
    kw_type = $2.strip
    return { :name => "#{kw_name}:", :types => self.class.rbs_type_to_yard_types(kw_type),
             :optional => optional, :rest => false }
  end

  # Positional: `Type [param_name]`.
  type_str, param_name = extract_type_and_name(param)
  { :name => param_name, :types => self.class.rbs_type_to_yard_types(type_str),
    :optional => optional, :rest => rest }
end
split_by_comma(str) click to toggle source

Split str on commas that are not inside brackets.

# File lib/yard/handlers/rbs/method_handler.rb, line 261
def split_by_comma(str)
  depth = 0
  parts = []
  cur   = String.new('')
  str.each_char do |c|
    case c
    when '(', '[', '{'
      depth += 1
      cur << c
    when ')', ']', '}'
      depth -= 1
      cur << c
    when ','
      if depth == 0
        parts << cur.strip
        cur = String.new('')
      else
        cur << c
      end
    else
      cur << c
    end
  end
  parts << cur.strip unless cur.strip.empty?
  parts
end