123456789_123456789_123456789_123456789_123456789_

Class: YARD::Parser::RBS::RbsParser

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
Instance Chain:
Inherits: YARD::Parser::Base
Defined in: lib/yard/parser/rbs/rbs_parser.rb

Overview

Parses RBS (Ruby type signature) files and produces a list of Statement objects for post-processing by handlers.

::YARD::Parser::RBS is Ruby's official type signature format (introduced in ::YARD::Parser::Ruby 3.0). This parser handles: class/module/interface declarations, method signatures, attribute accessors, mixins, and constants.

No external gem dependencies are used; the parser is hand-written.

Class Method Summary

::YARD::Parser::Base - Inherited

.new

This default constructor does nothing.

.parse

Convenience method to create a new parser and #parse

Instance Method Summary

::YARD::Parser::Base - Inherited

#enumerator

This method should be implemented to return a list of semantic tokens representing the source code to be post-processed.

#parse

This method should be implemented to parse the source and return itself.

#tokenize

This method should be implemented to tokenize given source.

Constructor Details

.new(source, filename) ⇒ RbsParser

Parameters:

  • source (String)

    source code to parse

  • filename (String)

    path to the source file

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 16

def initialize(source, filename)
  @source   = source
  @filename = filename
  @statements = nil
end

Instance Method Details

#enumeratorArray<Statement>

Returns:

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 36

def enumerator
  @statements
end

#parseRbsParser

Parses the source and returns self.

Returns:

  • (RbsParser)

    self

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 24

def parse
  lines = @source.lines.map { |l| l.chomp }
  @statements, = parse_body(lines, 0, false)
  self
end

#parse_attr(type, lines, i, docs, crange) (private)

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 296

def parse_attr(type, lines, i, docs, crange)
  stripped = strip_inline_comment(lines[i]).strip
  line_num = i + 1
  keyword  = type.to_s

  # attr_reader [self.] name : Type
  if stripped =~ /\A#{Regexp.escape(keyword)}\s+(self\.)?(\w+)\s*:\s*(.*)\z/
    is_class  = !$1.nil?
    attr_name = $2
    attr_type = $3.strip

    stmt = Statement.new(
      :type           => type,
      :name           => attr_name,
      :attr_rbs_type  => attr_type,
      :line           => line_num,
      :source         => stripped,
      :comments       => docs,
      :comments_range => crange,
      :visibility     => is_class ? :class : :instance
    )
    [stmt, i + 1]
  else
    [nil, i + 1]
  end
end

#parse_body(lines, start, stop_at_end) ⇒ Array(Array<Statement>, Integer) (private)

Parse a sequence of lines, returning statements and the index after the last consumed line.

Parameters:

  • lines (Array<String>)

    source lines

  • start (Integer)

    index to start from (0-based)

  • stop_at_end (Boolean)

    when true, stop parsing when we see a bare end

Returns:

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 48

def parse_body(lines, start, stop_at_end)
  statements        = []
  i                 = start
  pending_comments  = []
  pending_start_1   = nil  # 1-indexed line number of first pending comment

  while i < lines.length
    raw      = lines[i]
    stripped = raw.strip

    if stripped =~ /\A#(.*)/
      # Comment line – accumulate into pending docstring.
      # Strip at most one leading space (conventional RBS doc style).
      pending_comments << $1.sub(/\A /, '')
      pending_start_1 ||= i + 1
      i += 1

    elsif stripped.empty?
      # Blank line resets pending comments.
      pending_comments  = []
      pending_start_1   = nil
      i += 1

    elsif stop_at_end && stripped == 'end'
      # End of enclosing block.
      return [statements, i + 1]

    else
      stmt, i = parse_statement(lines, i, pending_comments, pending_start_1)
      statements << stmt if stmt
      pending_comments  = []
      pending_start_1   = nil
    end
  end

  [statements, i]
end

#parse_method_def(sanitized, lines, i, docs, crange) (private)

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 253

def parse_method_def(sanitized, lines, i, docs, crange)
  stripped = sanitized.fetch(i, lines[i]).strip
  line_num = i + 1

  # def method_name: overload1
  #                | overload2
  # Also handles: def self.method_name: ...
  unless stripped =~ /\Adef\s+(self\.)?(\S+?)\s*:\s*(.*)\z/
    return [nil, i + 1]
  end

  is_class_side = !$1.nil?
  meth_name     = $2
  first_sig     = $3.strip

  sigs = [first_sig]
  j    = i + 1

  # Collect `| overload` continuation lines.
  while j < lines.length
    cont = sanitized.fetch(j, lines[j]).strip
    if cont =~ /\A\|\s*(.*)\z/
      sigs << $1.strip
      j += 1
    else
      break
    end
  end

  stmt = Statement.new(
    :type           => :method_def,
    :name           => meth_name,
    :line           => line_num,
    :source         => lines[i...j].join("\n"),
    :comments       => docs,
    :comments_range => crange,
    :signatures     => sigs,
    :visibility     => is_class_side ? :class : :instance
  )

  [stmt, j]
end

#parse_namespace(type, lines, i, docs, crange) (private)

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 199

def parse_namespace(type, lines, i, docs, crange)
  # Strip trailing inline comment from the declaration line.
  decl     = lines[i].strip.sub(/\s*#.*\z/, '')
  line_num = i + 1

  name       = nil
  superclass = nil

  case type
  when :class
    # class Foo[T] < Bar[String]
    if decl =~ /\Aclass\s+([^\s<\[]+)(\[[^\]]*\])?(?:\s*<\s*(.+))?\z/
      name       = $1.strip
      superclass = $3 ? $3.strip : nil
      # Strip generic params from superclass, e.g. "Array[String]" -> "Array"
      superclass.sub!(/\[.*\]\z/, '') if superclass
    else
      return [nil, i + 1]
    end

  when :module
    # module Foo[T] : SelfType
    if decl =~ /\Amodule\s+([^\s\[(:]+)/
      name = $1.strip
    else
      return [nil, i + 1]
    end

  when :interface
    # interface _Foo[T]
    if decl =~ /\Ainterface\s+([^\s\[]+)/
      name = $1.strip
    else
      return [nil, i + 1]
    end
  end

  children, new_i = parse_body(lines, i + 1, true)
  source = lines[i...new_i].join("\n")

  stmt = Statement.new(
    :type           => type,
    :name           => name,
    :superclass     => superclass,
    :line           => line_num,
    :source         => source,
    :comments       => docs,
    :comments_range => crange,
    :block          => children
  )

  [stmt, new_i]
end

#parse_statement(lines, i, comments, comment_start_1) (private)

Dispatch a single declaration line.

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 125

def parse_statement(lines, i, comments, comment_start_1)
  sanitized     = sanitized_statement_lines(lines, i)
  stripped      = sanitized.fetch(i, lines[i]).strip
  line_num        = i + 1  # 1-indexed

  docs   = comments.empty? ? nil : comments.join("\n")
  crange = comment_start_1 ? (comment_start_1)..(line_num - 1) : nil

  case stripped
  when /\Aclass\s/
    parse_namespace(:class, lines, i, docs, crange)
  when /\Amodule\s/
    parse_namespace(:module, lines, i, docs, crange)
  when /\Ainterface\s/
    parse_namespace(:interface, lines, i, docs, crange)
  when /\Adef\s/
    parse_method_def(sanitized, lines, i, docs, crange)
  when /\Aattr_reader\s/
    parse_attr(:attr_reader, lines, i, docs, crange)
  when /\Aattr_writer\s/
    parse_attr(:attr_writer, lines, i, docs, crange)
  when /\Aattr_accessor\s/
    parse_attr(:attr_accessor, lines, i, docs, crange)
  when /\A(include|extend|prepend)\s+(\S+)/
    kind = $1.to_sym
    name = $2.delete(';')
    stmt = Statement.new(
      :type           => kind,
      :name           => name,
      :mixin_name     => name,
      :line           => line_num,
      :source         => stripped,
      :comments       => docs,
      :comments_range => crange
    )
    [stmt, i + 1]
  when /\Aalias\s+(\S+)\s+(\S+)/
    stmt = Statement.new(
      :type           => :alias,
      :name           => $1,
      :line           => line_num,
      :source         => stripped,
      :comments       => docs,
      :comments_range => crange
    )
    [stmt, i + 1]
  when /\A(public|private|protected)\s*(\z|#)/
    # Visibility modifier – skip silently.
    [nil, i + 1]
  when /\Aend\s*(\z|#)/
    # Stray `end` – skip.
    [nil, i + 1]
  when /\Atype\s/
    # Type alias declaration – nothing to document.
    [nil, i + 1]
  else
    # Constant declaration: `NAME: Type`
    if stripped =~ /\A([A-Z][a-zA-Z0-9_]*(?:::[A-Z][a-zA-Z0-9_]*)*)\s*:\s*(.+)\z/
      stmt = Statement.new(
        :type           => :constant,
        :name           => $1,
        :attr_rbs_type  => $2.strip,
        :line           => line_num,
        :source         => stripped,
        :comments       => docs,
        :comments_range => crange
      )
      [stmt, i + 1]
    else
      [nil, i + 1]
    end
  end
end

#sanitized_statement_lines(lines, start_index) (private)

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 112

def sanitized_statement_lines(lines, start_index)
  overrides = { start_index => strip_inline_comment(lines[start_index]) }

  j = start_index + 1
  while j < lines.length && lines[j].lstrip.start_with?('|')
    overrides[j] = strip_inline_comment(lines[j])
    j += 1
  end

  overrides
end

#strip_inline_comment(line) (private)

[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 86

def strip_inline_comment(line)
  in_single = false
  in_double = false
  escaped   = false

  line.each_char.with_index do |char, index|
    if escaped
      escaped = false
      next
    end

    case char
    when "\\"
      escaped = true if in_single || in_double
    when "'"
      in_single = !in_single unless in_double
    when '"'
      in_double = !in_double unless in_single
    when '#'
      return line[0...index].rstrip unless in_single || in_double
    end
  end

  line.rstrip
end

#tokenize

Tokenization is not implemented for ::YARD::Parser::RBS.

Raises:

  • (NotImplementedError)
[ GitHub ]

  
# File 'lib/yard/parser/rbs/rbs_parser.rb', line 31

def tokenize
  raise NotImplementedError, "RBS parser does not support tokenization"
end