class YARD::Parser::Ruby::RipperParser

Internal parser class @since 0.5.6

Constants

AST_TOKENS
COMMENT_SKIP_NODE_TYPES
MAPPINGS
REV_MAPPINGS

Attributes

ast[R]
charno[R]
comments[R]
encoding_line[R]
file[R]
frozen_string_line[R]
root[R]
shebang_line[R]
tokens[R]

Public Class Methods

new(source, filename, *args) click to toggle source
Calls superclass method
# File lib/yard/parser/ruby/ruby_parser.rb, line 32
def initialize(source, filename, *args)
  super
  @last_ns_token = nil
  @file = filename
  @source = source
  @tokens = []
  @comments = {}
  @comments_range = {}
  @comments_flags = {}
  @heredoc_tokens = nil
  @heredoc_state = nil
  @map = {}
  @ns_charno = 0
  @list = []
  @charno = 0
  @shebang_line = nil
  @encoding_line = nil
  @frozen_string_line = nil
  @file_encoding = nil
  @newline = true
  @percent_ary = nil
end

Public Instance Methods

enumerator() click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 64
def enumerator
  ast.children
end
file_encoding() click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 68
def file_encoding
  return nil unless defined?(::Encoding)
  return @file_encoding if @file_encoding
  return Encoding.default_internal unless @encoding_line
  match = @encoding_line.match(SourceParser::ENCODING_LINE)
  @file_encoding = match.captures.last if match
end
parse() click to toggle source
Calls superclass method
# File lib/yard/parser/ruby/ruby_parser.rb, line 55
def parse
  @ast = super
  @ast.full_source = @source
  @ast.file = @file
  freeze_tree
  insert_comments
  self
end

Private Instance Methods

add_comment(line, node = nil, before_node = nil, into = false) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 747
def add_comment(line, node = nil, before_node = nil, into = false)
  comment = @comments[line]
  source_range = @comments_range[line]
  if comment && source_range
    source = @source[source_range]
    last_line = source.lines.to_a.last
    return if last_line && last_line =~ /^\s*\#-\s*$/
  end
  line_range = ((line - comment.count("\n"))..line)
  if node.nil?
    node = CommentNode.new(:comment, [comment], :line => line_range, :char => source_range)
    if into
      before_node.push(node)
      before_node.unfreeze
      node.parent = before_node
    elsif before_node
      parent_node = before_node.parent
      idx = parent_node.index(before_node)
      parent_node.insert(idx, node)
      parent_node.unfreeze
      node.parent = parent_node
    end
  end
  node.docstring = comment
  node.docstring_hash_flag = @comments_flags[line]
  node.docstring_range = line_range
  @comments.delete(line)
  @comments_range.delete(line)
  @comments_flags.delete(line)
end
add_token(token, data) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 286
def add_token(token, data)
  if @percent_ary
    if token == :words_sep && data !~ /\s\z/
      rng = @percent_ary.source_range
      rng = Range.new(rng.begin, rng.end.to_i + data.length)
      @percent_ary.source_range = rng
      @tokens << [token, data, [lineno, charno]]
      @percent_ary = nil
      return
    elsif token == :tstring_end && data =~ /\A\s/
      rng = @percent_ary.source_range
      rng = Range.new(rng.begin, rng.end.to_i + data.length)
      @percent_ary.source_range = rng
      @tokens << [token, data, [lineno, charno]]
      @percent_ary = nil
      return
    end
  end

  if @tokens.last && (@tokens.last[0] == :symbeg ||
      (@tokens.last[0] == :symbol && token.to_s =~ /^tstring/))
    @tokens[-1] = [:symbol, @tokens.last[1] + data, @tokens.last[2]]
  elsif @heredoc_state == :started
    @heredoc_tokens << [token, data, [lineno, charno]]

    # fix ripper encoding of heredoc bug
    # (see https://bugs.ruby-lang.org/issues/6200)
    data.force_encoding(file_encoding) if file_encoding

    @heredoc_state = :ended if token == :heredoc_end
  elsif (token == :nl || token == :comment) && @heredoc_state == :ended
    @heredoc_tokens.unshift([token, data, [lineno, charno]])
    @tokens += @heredoc_tokens
    @heredoc_tokens = nil
    @heredoc_state = nil
  else
    @tokens << [token, data, [lineno, charno]]
    if token == :heredoc_beg
      @heredoc_state = :started
      @heredoc_tokens = []
    end
  end
end
comment_starts_line?(charno) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 671
def comment_starts_line?(charno)
  return true if @source[charno] == "\n"
  (charno - 1).downto(0) do |i|
    ch = @source[i]
    break if ch == "\n"
    return false if ch != " " && ch != "\t"
  end
  true
end
compile_error(msg)
Alias for: on_parse_error
freeze_tree(node = nil) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 778
def freeze_tree(node = nil)
  @tokens = @tokens.sort_by {|t| t.last }
  nodes = [node || root]
  until nodes.empty?
    p_node = nodes.shift
    p_node.children.each do |child|
      child.parent = p_node
      nodes << child
    end
  end
end
insert_comments() click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 681
def insert_comments
  root.traverse do |node|
    next if COMMENT_SKIP_NODE_TYPES.include?(node.type) || node.parent.type != :list

    # never attach comments to if/unless mod nodes
    if node.type == :if_mod || node.type == :unless_mod
      node = node.then_block
    end

    # check upwards from line before node; check node's line at the end
    if (n_l = node.line)
      ((n_l - 1).downto(n_l - 2).to_a + [n_l]).each do |line|
        comment = @comments[line]
        if comment && !comment.empty?
          add_comment(line, node)
          break
        end
      end
    end

    @comments.keys.each do |line|
      add_comment(line, nil, node) if node.line > line
    end
  end

  # insert any lone unadded comments before node
  root.traverse do |node|
    next if node.type == :list || node.parent.type != :list
    @comments.keys.each do |line|
      next unless node.line_range.include?(line)
      pick = nil
      node.traverse do |subnode|
        next unless subnode.type == :list
        pick ||= subnode
        next unless subnode.line_range.include?(line)
        pick = subnode
      end
      add_comment(line, nil, pick, true) if pick
    end
  end unless @comments.empty?

  # Attach comments that fall within an otherwise empty
  # class or module body. Without this step, a comment used
  # solely for directives (like @!method) would be treated as
  # a top-level comment and its directives would not be scoped
  # to the namespace.
  unless @comments.empty?
    root.traverse do |node|
      next unless [:class, :module, :sclass].include?(node.type)
      body = node.children.last
      next unless body && body.type == :list && body.empty?
      @comments.keys.each do |line|
        next unless node.line_range.include?(line)
        add_comment(line, nil, body, true)
      end
    end
  end

  # insert all remaining comments
  @comments.each do |line, _comment|
    add_comment(line, nil, root, true)
  end

  @comments = {}
end
on_aref(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 407
def on_aref(*args)
  @map[:lbracket].pop
  ll, lc = *@map[:aref].shift
  sr = args.first.source_range.begin..lc
  lr = args.first.line_range.begin..ll
  AstNode.new(:aref, args, :char => sr, :line => lr)
end
on_aref_field(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 415
def on_aref_field(*args)
  @map[:lbracket].pop
  ll, lc = *@map[:aref].shift
  sr = args.first.source_range.begin..lc
  lr = args.first.line_range.begin..ll
  AstNode.new(:aref_field, args, :char => sr, :line => lr)
end
on_array(other) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 423
def on_array(other)
  node = AstNode.node_class_for(:array).new(:array, [other])
  map = @map[MAPPINGS[node.type]] if other.nil? || other.type == :list
  if map && !map.empty?
    lstart, sstart = *map.pop
    node.source_range = Range.new(sstart, @ns_charno - 1)
    node.line_range = Range.new(lstart, lineno)
  else
    sstart = other.source_range.begin
    lstart = other.line_range.begin
    node.source_range = Range.new(sstart, @ns_charno - 1)
    node.line_range = Range.new(lstart, lineno)
    node.source_range = other.source_range
    node.line_range = other.line_range
  end
  node
end
on_aryptn(*args) click to toggle source

Ruby 3.0+ pattern matching: array patterns (SomeClass[a, b]) and find patterns (SomeClass[*pre, val, *post]) use […] brackets, which fire on_lbracket and on_rbracket scanner events. The corresponding parser events are on_aryptn/on_fndptn (not on_aref), so we must clean up the bracket maps to prevent stale entries from corrupting source ranges of later array indexing expressions.

# File lib/yard/parser/ruby/ruby_parser.rb, line 446
def on_aryptn(*args)
  (@map[:lbracket] ||= []).pop
  (@map[:aref] ||= []).shift
  # Source range is intentionally not set; no handler is registered for
  # pattern-match nodes, so they produce no documentation output.
  AstNode.new(:aryptn, args)
end
on_assoc_new(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 368
def on_assoc_new(*args)
  AstNode.new(:assoc, args)
end
on_assoclist_from_args(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 394
def on_assoclist_from_args(*args)
  args.first
end
on_bare_assoc_hash(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 390
def on_bare_assoc_hash(*args)
  AstNode.new(:list, args.first)
end
on_body_stmt(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 363
def on_body_stmt(*args)
  args.compact.size == 1 ? args.first : AstNode.new(:list, args)
end
Also aliased as: on_bodystmt
on_bodystmt(*args)
Alias for: on_body_stmt
on_comment(comment) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 602
def on_comment(comment)
  not_comment = false
  if @last_ns_token.nil? || @last_ns_token.empty?
    if comment =~ SourceParser::SHEBANG_LINE && !@encoding_line
      @shebang_line = comment
      not_comment = true
    elsif comment =~ SourceParser::ENCODING_LINE
      @encoding_line = comment
      not_comment = true
    elsif comment =~ SourceParser::FROZEN_STRING_LINE
      @frozen_string_line = comment
      not_comment = true
    end
  end

  ch = charno
  visit_ns_token(:comment, comment)
  if not_comment
    @last_ns_token = nil
    return
  end

  source_range = ch..(charno - 1)
  comment = comment.gsub(/^(\#+)\s{0,1}/, '').chomp
  append_comment = @comments[lineno - 1]

  hash_flag = $1 == '##' ? true : false

  if append_comment && @comments_last_column &&
     @comments_last_column == column && comment_starts_line?(ch)
    @comments.delete(lineno - 1)
    @comments_flags[lineno] = @comments_flags[lineno - 1]
    @comments_flags.delete(lineno - 1)
    range = @comments_range.delete(lineno - 1)
    source_range = range.begin..source_range.end
    comment = append_comment + "\n" + comment
  end

  @comments[lineno] = comment
  @comments_range[lineno] = source_range
  @comments_flags[lineno] = hash_flag unless append_comment
  @comments_last_column = column
end
on_const_path_ref(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 501
def on_const_path_ref(*args)
  ReferenceNode.new(:const_path_ref, args, :listline => lineno..lineno, :listchar => charno..charno)
end
on_dyna_symbol(sym) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 479
def on_dyna_symbol(sym)
  rng = if sym.source_range.to_a.size == 0 # rubocop:disable Style/ZeroLengthPredicate
          (sym.source_range.begin - 3)...sym.source_range.end
        else
          (sym.source_range.begin - 2)..(sym.source_range.end + 1)
        end
  AstNode.new(:dyna_symbol, [sym], :line => lineno..lineno,
    :listline => lineno..lineno, :listchar => rng, :char => rng)
end
on_embdoc(text) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 652
def on_embdoc(text)
  visit_ns_token(:embdoc, text)
  @embdoc << text
end
on_embdoc_beg(text) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 646
def on_embdoc_beg(text)
  visit_ns_token(:embdoc_beg, text)
  @embdoc_start = charno - text.length
  @embdoc = String.new("")
end
on_embdoc_end(text) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 657
def on_embdoc_end(text)
  visit_ns_token(:embdoc_end, text)
  @comments_last_column = nil
  @comments[lineno] = @embdoc
  @comments_range[lineno] = @embdoc_start...charno
  @embdoc_start = nil
  @embdoc = nil
end
on_fndptn(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 454
def on_fndptn(*args)
  (@map[:lbracket] ||= []).pop
  (@map[:aref] ||= []).shift
  # Source range is intentionally not set; no handler is registered for
  # pattern-match nodes, so they produce no documentation output.
  AstNode.new(:fndptn, args)
end
on_hash(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 372
def on_hash(*args)
  visit_event AstNode.new(:hash, args.first || [])
end
on_hshptn(*args) click to toggle source

Ruby 3.0+ pattern matching: braced hash patterns ({key: val} syntax) fire on_lbrace and on_rbrace scanner events. The corresponding parser event is on_hshptn (not on_hash), so we must clean up the brace maps to prevent stale entries from corrupting source ranges of later hash literals and brace blocks. Bare hash patterns (key: val without braces) fire no brace scanner events, so we only clean up when @map confirms a closing brace was scanned.

# File lib/yard/parser/ruby/ruby_parser.rb, line 382
def on_hshptn(*args)
  if (@map[:rbrace] ||= []).any?
    (@map[:lbrace] ||= []).pop
    @map[:rbrace].shift
  end
  AstNode.new(:hshptn, args)
end
on_label(data) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 594
def on_label(data)
  add_token(:label, data)
  ch = charno
  @charno += data.length
  @ns_charno = charno
  AstNode.new(:label, [data[0...-1]], :line => lineno..lineno, :char => ch..charno - 1, :token => true)
end
on_lambda(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 557
def on_lambda(*args)
  visit_event_arr AstNode.new(:lambda, args)
end
on_lbracket(tok) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 462
def on_lbracket(tok)
  (@map[:lbracket] ||= []) << [lineno, charno]
  visit_ns_token(:lbracket, tok, false)
end
on_params(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 575
def on_params(*args)
  args.map! do |arg|
    next arg unless arg.class == Array

    if arg.first.class == Array
      arg.map! do |sub_arg|
        next sub_arg unless sub_arg.class == Array
        type = sub_arg[0].type == :label ?
          :named_arg : :unnamed_optional_arg
        AstNode.new(type, sub_arg, :listline => lineno..lineno, :listchar => charno..charno)
      end
    end

    AstNode.new(:list, arg, :listline => lineno..lineno, :listchar => charno..charno)
  end

  ParameterNode.new(:params, args, :listline => lineno..lineno, :listchar => charno..charno)
end
on_parse_error(msg) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 666
def on_parse_error(msg)
  raise ParserSyntaxError, "syntax error in `#{file}`:(#{lineno},#{column}): #{msg}"
end
Also aliased as: compile_error
on_program(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 359
def on_program(*args)
  args.first
end
on_rbrace(tok) click to toggle source

Maintained explicitly (unlike on_lbracket/on_rbracket) so on_hshptn can distinguish braced from bare hash patterns in Ruby 3.0+ pattern matching.

# File lib/yard/parser/ruby/ruby_parser.rb, line 474
def on_rbrace(tok)
  (@map[:rbrace] ||= []) << [lineno, charno]
  visit_ns_token(:rbrace, tok, false)
end
on_rbracket(tok) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 467
def on_rbracket(tok)
  (@map[:aref] ||= []) << [lineno, charno]
  visit_ns_token(:rbracket, tok, false)
end
on_rescue(exc, *args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 566
def on_rescue(exc, *args)
  exc = AstNode.new(:list, exc) if exc
  visit_event AstNode.new(:rescue, [exc, *args])
end
on_sp(tok) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 232
def on_sp(tok)
  add_token(:sp, tok)
  @charno += tok.length
end
on_string_content(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 561
def on_string_content(*args)
  chr_rng = args.empty? ? charno...charno : charno..charno
  AstNode.new(:string_content, args, :listline => lineno..lineno, :listchar => chr_rng)
end
on_string_literal(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 544
def on_string_literal(*args)
  node = visit_event_arr(LiteralNode.new(:string_literal, args))
  if args.size == 1
    r = args[0].source_range
    if node.source_range != Range.new(r.begin - 1, r.end + 1)
      klass = AstNode.node_class_for(node[0].type)
      r = Range.new(node.source_range.begin + 1, node.source_range.end - 1)
      node[0] = klass.new(node[0].type, [@source[r]], :line => node.line_range, :char => r)
    end
  end
  node
end
on_top_const_ref(*args) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 489
def on_top_const_ref(*args)
  type = :top_const_ref
  node = AstNode.node_class_for(type).new(type, args)
  mapping = @map[MAPPINGS[type]]
  extra_op = mapping.last[1] + 2 == charno ? mapping.pop : nil
  lstart, sstart = *mapping.pop
  node.source_range = Range.new(sstart, args.last.source_range.last)
  node.line_range = Range.new(lstart, args.last.line_range.last)
  mapping.push(extra_op) if extra_op
  node
end
on_unary(op, val) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 398
def on_unary(op, val)
  map = @map[op.to_s[0, 1]]
  lstart, sstart = *(map ? map.pop : [lineno, @ns_charno - 1])
  node = AstNode.node_class_for(:unary).new(:unary, [op, val])
  node.source_range = Range.new(sstart, @ns_charno - 1)
  node.line_range = Range.new(lstart, lineno)
  node
end
on_void_stmt() click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 571
def on_void_stmt
  AstNode.new(:void_stmt, [], :line => lineno..lineno, :char => charno...charno)
end
visit_event(node) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 237
def visit_event(node)
  map = @map[MAPPINGS[node.type]]

  # Pattern matching and `in` syntax creates :case nodes without 'case' tokens,
  # fall back to the first child node.
  if node.type == :case && (!map || map.empty?) && (child_node = node[0])
    lstart = child_node.line_range.first
    sstart = child_node.source_range.first
  else
    lstart, sstart = *(map ? map.pop : [lineno, @ns_charno - 1])
    (@map[:rbrace] ||= []).shift if map && MAPPINGS[node.type] == :lbrace
  end

  raise "Cannot determine start of node #{node} around #{file}:#{lineno}" if lstart.nil? || sstart.nil?

  node.source_range = Range.new(sstart, @ns_charno - 1)
  node.line_range = Range.new(lstart, lineno)
  if node.respond_to?(:block)
    sr = node.block.source_range
    lr = node.block.line_range
    node.block.source_range = Range.new(sr.begin, @tokens.last[2][1] - 1)
    node.block.line_range = Range.new(lr.begin, @tokens.last[2][0])
  end
  node
end
visit_event_arr(node) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 263
def visit_event_arr(node)
  mapping = MAPPINGS[node.type].find {|k| @map[k] && !@map[k].empty? }
  lstart, sstart = *@map[mapping].pop
  node.source_range = Range.new(sstart, @ns_charno - 1)
  node.line_range = Range.new(lstart, lineno)
  node
end
visit_ns_token(token, data, ast_token = false) click to toggle source
# File lib/yard/parser/ruby/ruby_parser.rb, line 271
def visit_ns_token(token, data, ast_token = false)
  add_token(token, data)
  ch = charno

  # For purposes of tracking parsing state, don't treat keywords as such
  # where used as a symbol identifier.
  @last_ns_token = [@last_ns_token && @last_ns_token.first == :symbeg ? :symbol : token, data]
  @charno += data.length
  @ns_charno = charno
  @newline = [:semicolon, :comment, :kw, :op, :lparen, :lbrace].include?(token)
  if ast_token
    AstNode.new(token, [data], :line => lineno..lineno, :char => ch..charno - 1, :token => true)
  end
end