If you’d like to see the syntax tree that Ripper generates for Ruby code, you can try Ripper in irb. The ::sexp_raw method shows the syntax tree that is constructed from the scanner and parser events, while ::lex shows the scanner events. You can also use the ::sexp method to group events in a more readable way:

$ irb
irb(main):001:0> require "ripper"
=> false
irb(main):002:0> pp Ripper.sexp_raw("a=7")
  [:assign, [:var_field, [:@ident, "a", [1, 0]]], [:@int, "7", [1, 2]]]]]
irb(main):003:0> pp Ripper.lex("a=7")
[[[1, 0], :on_ident, "a", CMDARG],
 [[1, 1], :on_op, "=", BEG],
 [[1, 2], :on_int, "7", END]]
irb(main):004:0> pp Ripper.sexp("a=7")
 [[:assign, [:var_field, [:@ident, "a", [1, 0]]], [:@int, "7", [1, 2]]]]]

Scanner events tend to include the location where the token starts as an array of two integers, such as [1,2] above. The first number is a 1-indexed line number (in this case line 1) and a 0-indexed character number within the line (for [1,2], 2 means the third character of that line.)

Ripper works in an evented style internally. To use it that way, you’ll usually want to inherit from the Ripper parent class, define some event handlers, and parse your code. Here’s a very simple example using the heredoc_beg scanner event:

require "ripper"

class HeredocFinder < Ripper
  def on_heredoc_beg(value)
    if value.start_with?("<<") && !value.start_with?("<<~")
      puts "Warning: use only indenting heredocs!"
      puts "Problem in #{filename} at location #{lineno}:#{column}"
end, __FILE__, DATA.lineno + 1).parse

puts <<EXAMPLE
  This is a multiline
  heredoc in Ruby

Parser events can be handed in the same way. Remember that if you return different values from handlers (like on_args_new/on_args_add), this will change the values other handlers receive (like on_method_add_arg.)

The Ruby parser can be complicated, and can generate a variety of nodes and structures. You should probably check the source code you care about using Ripper.sexp_raw and Ripper.lex to see what events occur for that source code. Ripper::PARSER_EVENT_TABLE contains a mapping of all events to their arity (number of arguments). The parser table can be useful when detecting many or all events at once, or when forwarding all unrecognised events to a different piece of code.

Here is a more complicated event-based parser example:

require "ripper"

class CallArgListPrinter < Ripper
  attr_reader :calls

  def initialize(*)
    @calls = []


  def on_method_add_arg(called, call_item)
    @calls << [called, call_item]
    [called, call_item]

  # If you don't return a value for args_new/args_add, you won't get argument
  # data for on_method_add_arg.
  def on_args_new
    { values: [], lineno: lineno, column: column }

  def on_args_add(args, arg)
    args[:values] << arg

puts, __FILE__, DATA.lineno + 1).tap(&:parse).calls

method(1, 2, 3)

object.method(4, 5, 6)

# Not detected because it uses call instead of fcall and method_add_arg

# Detected but generates a significantly different AST, which confuses our simple logic