How can I extract variables from a ruby snippet

I have this text:

<%= params['post']['category_id'] %> <%= params['post']['title'] %> <%= params['post']['body'] %> <%= params['post']['extended_body'] %> <%= params['post']['private'] %> <% if params['notify'] then %> <% params['notify'].each do |person_id| %> <%= person_id %> <% end %> <% end %> <% if params['attachments'] then %> <% params['attachments'].each do |attachment| %> <%= attachment['name'] %> <%= attachment['file']['temp_id'] %> <%= attachment['file']['content_type'] %> <%= attachment['file'] ['original_filename'] %> <% end %> <% end %>

What I need to do is extract all of the variables used in order to
learn the variables, and their structure (array/hash vs. simple value)
used to render the view.

So, using the snippet above, I would learn that there is:

  • a “post” variable which is a hash, expected to have the keys
    “category_id”, “title”, “body”, “extended_body” and “private”.
  • a “notify” variable which is an array (indicated by the fact that
    it’s iterated over with the .each) of simple values (i.e. it’s not a
    multi-dimensional array, or an array of hashes)
  • a “attachments” variable which is an array (again indicated by the
    fact that it’s iterated over with the .each) of hashes (indicated by
    the fact that each element of the “attachments” array is referenced
    with hash syntax, i.e. "attachment[‘name’]) and that the value
    corresponding to the key “file” is also a hash expected to have the
    keys “temp_id”, “content_type” and “original_filename”.

Is there a tool available to allow me to do this? I haven’t been able to
find anything.

Laran Evans wrote:

So, using the snippet above, I would learn that there is:

  • a “post” variable which is a hash, expected to have the keys
    “category_id”, “title”, “body”, “extended_body” and “private”.
  • a “notify” variable which is an array (indicated by the fact that
    it’s iterated over with the .each) of simple values (i.e. it’s not a
    multi-dimensional array, or an array of hashes)

You can’t tell that - lots of objects implement an ‘each’ iterator, and
there isn’t really such thing as a ‘simple value’ in Ruby anyway. The
number 1 and the Array [1] are both first-class Objects. e.g.

h = {1=>“one”, 2=>“two”}
=> {1=>“one”, 2=>“two”}

h.each { |elem| p elem }
[1, “one”]
[2, “two”]

The full solution to what you want is probably to compile the ERB
template into Ruby, and then compile the Ruby into an sexp (e.g. using
ParseTree or ripper), and then analyse the sexp. This is quite a lot of
work.

Otherwise, you could execute the template using erb, and use
method_missing to pick up attempts to access local variables. From these
return a dummy object, which again uses method_missing to see what
methods have been called on that dummy object. If the method_missing
passes a block then you will need to yield the block with dummy
arguments.

This may be good enough for what you need, but isn’t guaranteed to catch
all possible execution paths. e.g. what about

<% if foo %>
<%= foo.xxx %>
<% else %>
<%= bar.yyy %>
<% end %>
?

You are going to find it hard to exercise both branches of this
conditional. Similarly with loops, where the nth iteration may behave
differently to the previous ones.

Rails approaches the problem differently: whenever you render a template
you pass in a hash of ‘local_assigns’, and it compiles the template
assuming that these are the variables which will be substituted. It then
makes a method signature using this set of variables. If the template is
rendered again and ‘local_assigns’ contains a different set of variables
to substitute, then it is recompiled. The assumption is that normally
the template will be rendered multiple times using the same set of
local_assigns variables (just with different values, of course)

See actionpack/lib/action_view/renderable.rb

2009/8/18 Brian C. [email protected]:

Otherwise, you could execute the template using erb, and use
method_missing to pick up attempts to access local variables. From these
return a dummy object, which again uses method_missing to see what
methods have been called on that dummy object. If the method_missing
passes a block then you will need to yield the block with dummy
arguments.

I toyed around a bit - this solution is by far not perfect!

#!ruby19 -w

class FillIn < Object
def initialize(name)
@name = name.to_s
end

def method_missing(s,*a,&b)
a.map! {|x| x.inspect}

nn = case s
when :[]
  "#{@name}[#{a.join(', ')}]"
when :[]=
  v = a.slice!(-1, 1)
  "#{@name}[#{a.join(', ')}]=#{v}"
else
  if a.empty?
    "#{@name}.#{s}"
  else
    "#{@name}.#{s}(#{a.join(', ')})"
  end
end

puts nn
self.class.new(nn)

end

def to_s
@name
end

alias inspect to_s
end

root = FillIn.new “root”
p root

root.foo.bar[‘help’].length
root.foo.bar[FillIn.new(“x”)].length

You are going to find it hard to exercise both branches of this
conditional. Similarly with loops, where the nth iteration may behave
differently to the previous ones.

Correct. I feel somehow reminded of
Halting problem - Wikipedia :slight_smile:

Kind regards

robert