Ruby traverser

A DSL for traversing ruby code as an object model (graph), which lets you find parts of the ruby code of interest. The traverser leverages ripper2ruby, a library for generating a model from ruby source code. ripper2ruby leverages ripper which comes with ruby 1.9.

See the unit tests in the test directory for examples of use.

Requirements

  • Ruby 1.9
  • ripper2ruby >= 0.0.2

Finders

  • find_module(name)
  • find_class(name, options = {})
  • find_call(name, options = {})
  • find_block(name, options = {})
  • find_def(name, options = {})
  • find_variable(name, options = {})
  • find_assignment(name, options = {})

Find module


  src = %q{      
    module Xyz::Xxx::Blip
      2
    end    
  }
  
  code = Ripper::RubyBuilder.build(src)          
  module_node = code.find_module('Xyz::Xxx::Blip') 
  assert_equal Ruby::Module, module_node.class 

Find class


# class Monty::Python ... end
clazz_node = code.find_class('Monty::Python')   

Find class inheriting from a certain superclass


# class Monty < Abc::Blip ... end
clazz_node = code.find_class('Monty', :superclass => 'Abc::Blip')   

Find call


# gem 'ripper', :src => 'github' 
gem_call = code.find_call('gem', :args => ['ripper', {:src => 'github'}])   

Find block ##


# my_block do ... end
block_node = code.find_block('my_block')   

# my_block do |v| ... end
block_node = code.find_block('my_block', :block_params => ['v'])   

# my_block 7, 'a' do ... end
block_node = code.find_block('my_block', :args => [7, 'a'])   

# my_block 7, 'a', :k => 32 do |v| ... end
block_node = code.find_block('my_block', :args => [7, 'a', {:k => 32}], :block_params => ['v'])   

# my_block :a => 7, b => 3 do |v| ... end
block_node = code.find_block('my_block', :args => [{:a => 7, 'b' => 3}])   
                                                                      
# my_block ['a', 'b'] do |v| ... end  
block_node = code.find_block('my_block', :args => [{:array =>['a', 'b']}])   

Find variable

Source code:


  def hello_world(a)
    my_var
  end  

Ruby code find DSL:


code = Ripper::RubyBuilder.build(src)               
code.inside_def('hello_world', :params => ['a']) do |b|
  call_node = b.find_variable('my_var')
  assert_equal Ruby::Variable, call_node.class
  puts call_node.to_ruby
end

Find assignment

Source code:


  def hello_world(a)
    my_var = 2
  end    

Ruby code find DSL:


  code = Ripper::RubyBuilder.build(src)               
  code.inside_def('hello_world', :params => ['a']) do |b|
    call_node = b.find_assignment('my_var')
  end  

Inside

The following finder methods have corresponding inside_ functions, which support block DSL constructs as shown below.

  • inside_module
  • inside_class
  • inside_def
  • inside_block

  # source code
  src = %q{      
    gem 'ripper', :src => 'github', :blip => 'blap'       
    group :test do
      gem 'ripper', :src => 'github' 
    end  
  }

  code = Ripper::RubyBuilder.build(src)              
  # chaining finders using 'inside__' DSL block constructs  
  code.inside_block('group', :args => [:test]) do |b|
    call_node = b.find_call('gem', :args => ['ripper', {:src => 'github'}])
    assert_equal Ruby::Call, call_node.class
    puts call_node.to_ruby # output ruby code as string for found node 
  end    
  
  src = %q{    
    def hello_world(b)
      3
    end

    def hello_world(a)
      gem 'ripper', :src => 'github' 
    end

  }  
     
  # chaining finders using 'inside__' DSL block constructs
  code = Ripper::RubyBuilder.build(src)            
  code.inside_def('hello_world', :params => ['a']) do |b|
    call_node = b.find_call('gem', :args => ['ripper', {:src => 'github'}])
    assert_equal Ruby::Call, call_node.class
    puts call_node.to_ruby # output ruby code as string for found node 
  end  

Code Mutation API

The API now also supports a wide variety of code mutations using a DSL. More information will soon be available here or on the github wiki. Check the test/mutate folder for test demonstrating what is currently possible.

Example use of Mutation API

Source BEFORE mutations:


class Monty < Abc::Blip  
end

    src = %q{  class Monty < Abc::Blip 
  end}
    
    def_src = %q{
def my_fun
end}

    def_code = Ripper::RubyBuilder.build(def_src)    
    code = Ripper::RubyBuilder.build(src)

    # append code examples                  
    code.inside_class('Monty', :superclass => 'Abc::Blip') do |b|
      assert_equal Ruby::Class, b.class                                  
      gem_abc = b.append_code("gem 'abc'")
      blip = b.append_code("blip")
      gem_123 = gem_abc.append_code("gem '123'")      
      gem_123.append_comment("hello")      
      my_def = b.append_code(def_src)      
      
      b.prepend_code("gem '789'")
      puts b.to_ruby
    end

Source AFTER mutations:


class Monty < Abc::Blip  
  gem '789'  
  gem 'abc'  
  gem '123'  
  blip  

  def my_fun
  end 
end

Replace example

Source BEFORE mutations:


group :test do
  gem 'ripper', :src 
  my_var = 2  
end  

    src = %q{                 
group :test do
  gem 'ripper', :src 
  my_var = 2  
end  
    }

    code = Ripper::RubyBuilder.build(src) 
    # replace examples                                      
    code.inside_block('group', :args => [:test]) do |b|
      call_node = b.find_call('gem', :args => ['ripper'])
      assert_equal Ruby::Call, call_node.class
      call_node.replace(:arg => :src , :replace_code => "{:src => 'unknown'}")
      call_node.replace(:value => "3")            
      puts b.to_ruby
    end       
  end  

Source AFTER mutations:


group :test do
  gem 'ripper', {:src => 'unknown'} 
  my_var = 3
end  

Note: The mutation API code was developed quickly in a test-driven fashion, but is in need of a major refactoring overhaul sometime soon...

Note on Patches/Pull Requests

  • Fork the project.
  • Make your feature addition or bug fix.
  • Add tests for it. This is important so I don't break it in a future version unintentionally.
  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
  • Send me a pull request. Bonus points for topic branches.

Copyright (c) 2010 Kristian Mandrup. See LICENSE for details.