123456789_123456789_123456789_123456789_123456789_

Class: YARD::Handlers::RBS::MethodHandler

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
Instance Chain:
Inherits: YARD::Handlers::RBS::Base
Defined in: lib/yard/handlers/rbs/method_handler.rb

Overview

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 ::YARD::Handlers::RBS type signature when those tags are absent from the docstring.

Constant Summary

::YARD::CodeObjects - Included

BUILTIN_ALL, BUILTIN_CLASSES, BUILTIN_EXCEPTIONS, BUILTIN_EXCEPTIONS_HASH, BUILTIN_MODULES, CONSTANTMATCH, CONSTANTSTART, CSEP, CSEPQ, ISEP, ISEPQ, METHODMATCH, METHODNAMEMATCH, NAMESPACEMATCH, NSEP, NSEPQ, PROXY_MATCH

Class Attribute Summary

::YARD::Handlers::Base - Inherited

.namespace_only

Declares that the handler should only be called when inside a ::YARD::CodeObjects::NamespaceObject, not a method body.

.namespace_only?

Class Method Summary

Base - Inherited

::YARD::Handlers::Base - Inherited

.clear_subclasses

Clear all registered subclasses.

.handlers,
.handles

Declares the statement type which will be processed by this handler.

.handles?
.in_file

Declares that a handler should only be called when inside a filename by its basename or a regex match for the full path.

.inherited, .matches_file?, .new,
.process

Generates a #process method, equivalent to +def process; ...

.subclasses

Returns all registered handler subclasses.

Instance Attribute Summary

::YARD::Handlers::Base - Inherited

#extra_state

Share state across different handlers inside of a file.

#globals

::YARD::Handlers can share state for the entire post processing stage through this attribute.

#namespace, #namespace=, #owner, #owner=, #parser, #scope, #scope=, #statement, #visibility, #visibility=

Instance Method Summary

Base - Inherited

#parse_block

Recurse into the body of a namespace statement.

::YARD::Handlers::Base - Inherited

#abort!

Aborts a handler by raising ::YARD::Handlers::HandlerAborted.

#call_params, #caller_method,
#ensure_loaded!

Ensures that a specific object has been parsed and loaded into the registry.

#parse_block

Parses the semantic "block" contained in the statement node.

#process

The main handler method called by the parser on a statement that matches the handles declaration.

#push_state

Executes a given block with specific state values for #owner, #namespace and #scope.

#register

Do some post processing on a list of code objects.

#register_docstring

Registers any docstring found for the object and expands macros.

#register_dynamic

Registers the object as dynamic if the object is defined inside a method or block (owner != namespace).

#register_ensure_loaded

Ensures that the object's namespace is loaded before attaching it to the namespace.

#register_file_info

Registers the file/line of the declaration with the object.

#register_group

Registers the object as being inside a specific group.

#register_module_function

Registers the same method information on the module function, if the object was defined as a module function.

#register_source,
#register_transitive_tags

Registers any transitive tags from the namespace on the object.

#register_visibility

Registers visibility on a method object.

Constructor Details

This class inherits a constructor from YARD::Handlers::Base

Class Method Details

.bracket_depth(str) (private)

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

[ GitHub ]

  
# 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

.rbs_type_to_yard_types(rbs) ⇒ Array<String>

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

Parameters:

  • rbs (String)

    e.g. "String | Integer", "Array[String]", "bool"

[ GitHub ]

  
# 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

.split_on_pipe(str) (private)

Split str on {|} that are not inside brackets.

[ GitHub ]

  
# 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

Instance Method Details

#add_overload_tag(obj, meth_name, sig) (private)

Add an @overload tag for one signature overload.

[ GitHub ]

  
# 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) (private)

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

[ GitHub ]

  
# 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) (private)

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

[ GitHub ]

  
# 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) (private)

Apply tags from all overload signatures to the method object.

[ GitHub ]

  
# 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) (private)

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

[ GitHub ]

  
# 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) ⇒ nil (private)

Find the index of the matching close bracket starting from start.

Returns:

  • (nil)

    if no matching bracket is found (malformed input).

[ GitHub ]

  
# 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) (private)

Parse the inside of a { ... } block type, e.g. "(Integer) -> String".

[ GitHub ]

  
# 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) ⇒ Hash (private)

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

Parameters:

  • sig (String)

    e.g. "(String name, Integer age) -> String"

Returns:

  • (Hash)

    { :params => [...], :block_param => Hash|nil, :return_types => [...] }

[ GitHub ]

  
# 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) (private)

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

[ GitHub ]

  
# 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) (private)

Parse one parameter from an ::YARD::Handlers::RBS param list.

[ GitHub ]

  
# 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

#processvoid

This method returns an undefined value.

Main processing callback

[ GitHub ]

  
# File 'lib/yard/handlers/rbs/method_handler.rb', line 10

process do
  meth_scope = statement.visibility == :class ? :class : :instance
  obj = register MethodObject.new(namespace, statement.name, meth_scope)
  apply_signature_tags(obj, statement.signatures)

  # For initialize, ensure the return type is the class, not void.
  if statement.name == 'initialize'
    ret_tags = obj.tags(:return)
    if ret_tags.none? || (ret_tags.length == 1 && ret_tags.first.types == ['void'])
      obj.docstring.delete_tags(:return)
      obj.add_tag YARD::Tags::Tag.new(:return, "a new instance of #{namespace.name}",
                                      [namespace.name.to_s])
    end
  end
end

#split_by_comma(str) (private)

Split str on commas that are not inside brackets.

[ GitHub ]

  
# 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